tags: container,源码分析

docker reexec源码分析

本文编写时,最新release为v20.10.6, 因此代码均为v20.10.6分支的代码

1. reexec简介

reexec库位于pkg下,是一个相对独立且可以由外部使用的模块。根据readme, reexec模块是为了提供重新调用docker二进制文件的能力,类似fork的概念。

The reexec package facilitates the busybox style reexec of the docker binary that we require because of the forking limitations of using Go.

fork函数将运行着的程序分成2个(几乎)完全一样的进程,每个进程都启动一个从代码的同一位置开始执行的线程。

在Go语言中,由于屏蔽了进程、线程的概念,而只提供了goroutine的概念,导致我们无法直接操作fork调用。我们常用的os/exec是相当于fork+exec的方案,它启动一个新的子进程,执行不共享任何内存或 Go 运行时的不同可执行文件。

因此docker自行实现了reexec库,来提供类似于fork的能力,但并不是一个特别高深的概念,实现也很简单。实际上,reexec仍然会调用os/exec调用一个子进程,与直接使用os/exec的区别在于这个子进程的二进制文件是docker本身,它执行的代码是docker中的一个函数。

2. reexec库使用介绍

在具体分析reexec前,我们先看一下reexec的使用。

在程序主体逻辑执行前,调用reexec.Init(),这个函数用于判断当前进入程序主体逻辑,还是进入reexec子命令。

https://github.com/moby/moby/blob/v20.10.6/cmd/dockerd/docker.go#L72-L74

func main() {
    if reexec.Init() {
        return
    }
    ...

docker-tar为例,要向reexec,将tar函数注册名为docker-tar的“命令”。

https://github.com/moby/moby/blob/v20.10.6/pkg/chrootarchive/init_unix.go#L17

reexec.Register("docker-tar", tar)

在使用时,调用reexec.Command获得一个os/exec.Cmd实例。后续使用和使用os/exec库一致。

https://github.com/moby/moby/blob/v20.10.6/pkg/chrootarchive/archive_unix.go#L178

cmd := reexec.Command("docker-tar", relSrc, root)

使用时只需增加以上3个调用即可。

3. reexec实现分析

3.1 reexec.Init()

Init函数中判断当前进程的cmdline是否已注册。如果未注册,则返回false;如果已注册,则获取其注册的函数并执行,执行完毕后返回true

https://github.com/moby/moby/blob/v20.10.6/pkg/reexec/reexec.go#L23-L31

var registeredInitializers = make(map[string]func())
...
func Init() bool {
    initializer, exists := registeredInitializers[os.Args[0]]
    if exists {
        initializer()

        return true
    }
    return false
}

例如,在dockerd中,dockerd未注册,则返回false, 后续执行dockerd主逻辑; tar函数注册为docker-tar,则执行tar函数并返回true,后续不再继续执行。

https://github.com/moby/moby/blob/v20.10.6/cmd/dockerd/docker.go#L72-L74

func main() {
    if reexec.Init() {
        return
    }
    ...

3.2 reexec.Register()

Register的实现是很简单的map的读写操作:

https://github.com/moby/moby/blob/v20.10.6/pkg/reexec/reexec.go#L13-L19

var registeredInitializers = make(map[string]func())
...
func Register(name string, initializer func()) {
	if _, exists := registeredInitializers[name]; exists {
		panic(fmt.Sprintf("reexec func already registered under name %q", name))
	}

	registeredInitializers[name] = initializer
}

3.3 reexec.Command()

Command函数对linux,uinx,windows分别有不同的实现,本文仅关注linux的实现。

Command函数返回一个*exec.Cmd实例,其二进制程序的path是/proc/self/exe, 这样就能达到调用同一个二进制程序的效果。

https://github.com/moby/moby/blob/v20.10.6/pkg/reexec/command_linux.go

func Self() string {
	return "/proc/self/exe"
}

func Command(args ...string) *exec.Cmd {
	return &exec.Cmd{
		Path: Self(),
		Args: args,
		SysProcAttr: &syscall.SysProcAttr{
			Pdeathsig: unix.SIGTERM,
		},
	}
}