docker reexec源码分析
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,
},
}
}