什么是reexec机制

通常,我们在定义命令行对应的函数时,通常使用第三方包进行。所以这就要求预先规划好该可执行文件支持多少子命令。但在Docker中,有一种需要动态注册子命令机制供第三方包或docker本身使用,这些子命令通常不是面向用户。
Docker提供的注册机制就是reexec机制。

reexec

reexec机制提供了Register()函数注册子命令及对应函数,定义在/pkg/reexec/reexec.go中:

1
2
3
4
5
6
7
8
9
10
var registeredInitializers = make(map[string]func())
// Register adds an initialization func under the specified name
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
}

可以看出,reexec包中有一个全局变量registeredInitializers,记录了注册的子命令及处理函数,这里要注意下,处理函数不能带参数,只能到参数列表中去取相应的内容。而Register()就是向registeredInitializers注册内容。

Init()

那么,Docker是如何把子命令关联到reexec中的呢,毕竟Docker有第三方的子命令管理工具。答案时Init(),定义在/pkg/reexec/reexec.go中:

1
2
3
4
5
6
7
8
9
10
11
12
// Init is called as the first part of the exec process and returns true if an
// initialization function was called.
func Init() bool {
initializer, exists := registeredInitializers[os.Args[0]]
//***执行initializer()***//
if exists {
initializer()
return true
}
return false
}

Init()会去找子命令是否注册,如果注册,那么则立即执行处理函数。所以Docker只要在第三方包命令管理工具执行之前,就调用Init(),就可以很好地处理两种命令。来看下/cmd/dockerd/docker.go中的dockerd启动函数main():

1
2
3
4
5
6
7
func main() {
//***调用reexec.Init()***//
if reexec.Init() {
return
}
......
}

系统中的使用

那么,Docker中有多少地方使用了reexec机制呢?可以来看下:

1
2
3
4
5
6
7
8
9
./pkg/chrootarchive/init_unix.go:15: reexec.Register("docker-applyLayer", applyLayer)
./pkg/chrootarchive/init_unix.go:16: reexec.Register("docker-untar", untar)
./daemon/graphdriver/overlay2/mount.go:18: reexec.Register("docker-mountfrom", mountFromMain)
./daemon/graphdriver/windows/windows.go:44: reexec.Register("docker-windows-write-layer", writeLayer)
./vendor/src/github.com/docker/libnetwork/sandbox_externalkey.go:11: reexec.Register("libnetwork-setkey", processSetKeyReexec)
./vendor/src/github.com/docker/libnetwork/service_linux.go:28: reexec.Register("fwmarker", fwMarker)
./vendor/src/github.com/docker/libnetwork/service_linux.go:29: reexec.Register("redirecter", redirecter)
./vendor/src/github.com/docker/libnetwork/resolver_unix.go:19: reexec.Register("setup-resolver", reexecSetupResolver)
./vendor/src/github.com/docker/libnetwork/osl/namespace_linux.go:59: reexec.Register("netns-create", reexecCreateNamespace)

举个例子,我们可以通过dockerd libnetwork-setkey <container-id> <controller-id>把新建的容器加入到已准备好的network namespace中。这就是Docker先准备好网络,然后runc通过prestart hook调用设置容器网络的方法,这些都定义在config.json的hooks.prestart中:

1
2
3
4
5
6
7
8
9
10
11
12
"hooks": {
"prestart": [
{
"path": "/usr/local/bin/dockerd",
"args": [
"libnetwork-setkey",
"12a3c752fc752f0a9099bea56525a106092f78f411704e47eaf9c32520550025",
"9ca45d808e4c577ae6dceef7259ca41561cf1e990a1ae41d370c5df458aab3f5"
]
}
]
}

关于容器网络,会在以后详细分析。

总结

reexec机制相当简洁,但非常重要,提供了一种不想让用户调用,或可以供第三方包注册的命令行机制,值得在项目中应用。