命名空间Go实现 - reexec

本文的目的是理解reexec包。reexec 是Docker代码的一部分,提供了一个很方便的方法是的可执行文件“自我执行”。在了解详细内容之前,我们先看看reexec帮我们解决了什么问题。

举个例子来说明这个问题。考虑这样一个问题。我们想修改container,使得它在新的UTS命名空间里有一个随机生成的主机名。为了安全的原因,在/bin/sh进程开始前,我们就得把主机名修改了。毕竟,我们不希望在container的shell里可以获取到宿主机的主机名。

就我所知,Go原生不支持这些。exec.Command不但可以通过设置属性来创建命名空间,也可以指定我们想要执行的进程。举个例子:

cmd := exec.Command("/bin/echo", "Process already running")
cmd.SysProcAttr = &syscall.SysProcAttr{
	Cloneflags: syscall.CLONE_NEWUTS,
}
cmd.Run()
1
2
3
4
5

cmd.Run()一旦被调用,那么命名空间就会被创建,进程也就直接开始运行了。其中没有钩子函数,也没有别方法可以帮助我们在命名空间被创建后和进程开始前执行代码。这就是reexce出现的原因。

自我毁灭前让自己重生

打开reexec,看看源码。

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
}
1
2
3
4
5
6
7
8
9
10

Register 函数支持通过名字注册任意方法到内存中。我们将会在container开始后的以后注册一些用于初始化命名空间的方法。

// 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]]
	if exists {
		initializer()

		return true
	}
	return false
}
1
2
3
4
5
6
7
8
9
10
11

接着是Init()函数,该函数可以判断这个进程有没有被执行过(reexeced),并且执行一个我们注册过的方法。它的实现机制是通过判断os.Args[0] 是不是之前注册的方法名之一,如果是则说明被执行过,便执行注册的方法。

// Self returns the path to the current process's binary.
// Returns "/proc/self/exe".
func Self() string {
	return "/proc/self/exe"
}

// Command returns *exec.Cmd which has Path as current binary. Also it setting
// SysProcAttr.Pdeathsig to SIGTERM.
// This will use the in-memory version (/proc/self/exe) of the current binary,
// it is thus safe to delete or replace the on-disk binary (os.Args[0]).
func Command(args ...string) *exec.Cmd {
	return &exec.Cmd{
		Path: Self(),
		Args: args,
		SysProcAttr: &syscall.SysProcAttr{
			Pdeathsig: syscall.SIGTERM,
		},
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

reexec.Command 方法把所有的都联系了在一起,通过创建一个*exex.Cmd结构体,设置其PathSelf()(在Linux系统中,其值为/proc/self/exe,表示的正在执行的可执行程序)。我们可以通过传入的args[0]来选择被某个注册函数被reexec调用。

现在我们理解了reexec的原理了,我们来动手实践一下。

Let's Go

首先创建一个方法并且使用reexec来注册它。

# Git repo: https://github.com/teddyking/container
# Git tag: 3.0
# Filename: ns_process.go
# ...
func init() {
	reexec.Register("nsInitialisation", nsInitialisation)
	if reexec.Init() {
		os.Exit(0)
	}
}
# ...
1
2
3
4
5
6
7
8
9
10
11

这里有两个重要的点。一是我们用“nsInitialisation”注册了nsInitialisation方法。二是我们调用了reexce.Init()方法并且在返回true的时候结束进程(os.Exit(0),这一步是非常重要的,如果不退出将陷入无限循环,程序会不停的reexec它自己。接着我们增加nsInitialisation方法。

# Git repo: https://github.com/bingbig/container
# Git tag: 3.0
# Filename: container.go
# ...
func nsInitialisation() {
	fmt.Printf("\n>> namespace setup code goes here <<\n\n")
	nsRun()
}

func nsRun() {
	cmd := exec.Command(os.Args[1], os.Args[2:]...)

	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	cmd.Env = []string{"PS1=-[container]- # "}

	if err := cmd.Run(); err != nil {
		fmt.Printf("Error running the %s command - %s\n", os.Args[1], err)
		os.Exit(1)
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

调用nsInitialisation 后会调用nsRun(), 而后者会执行/bin/sh进程。

最后我们需要做的就是修改run()方法,用reexecnsInitialisation 来运行/bin/sh,而不是之前那样直接调用。


 



















































func main() {
	if len(os.Args) < 2 {
		panic("pass me an argument please")
	}

	switch os.Args[1] {
	case "run":
		if len(os.Args) < 3 {
			panic("pass me a cmd to run in container please")
		}
		run()
	default:
		panic("pass me an argument please")
	}
}

func run() {
	cmd := reexec.Command(append([]string{"nsInitialisation"},
		os.Args[2:]...)...)

	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	cmd.SysProcAttr = &syscall.SysProcAttr{
		Cloneflags: syscall.CLONE_NEWNS |
			syscall.CLONE_NEWUTS |
			syscall.CLONE_NEWIPC |
			syscall.CLONE_NEWPID |
			syscall.CLONE_NEWNET |
			syscall.CLONE_NEWUSER,
		UidMappings: []syscall.SysProcIDMap{
			{
				ContainerID: 0,
				HostID:      os.Getuid(),
				Size:        1,
			},
		},
		GidMappings: []syscall.SysProcIDMap{
			{
				ContainerID: 0,
				HostID:      os.Getgid(),
				Size:        1,
			},
		},
	}

	if err := cmd.Run(); err != nil {
		fmt.Printf("Error running the reexec.Command - %s\n", err)
		os.Exit(1)
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

nsInitialisation 作为第一个参数传递给Command,本质上是告诉reexec去执行 /proc/self/exe,并且将os.Args[0]参数的值设置为nsInitialisation。最终,一旦程序被重新执行,Init会检测到注册的函数并执行它。我们来实践一下,

$ go build
$ ./container run /bin/sh

>> namespace setup code goes here <<
-[container]- #
1
2
3
4
5

成功了!我们现在有nsInitialisation 方法帮助我们初始化任意的命名空间了。

接下来

现在我们可以配置我们的命名空间了,还有什么需要配置的呢?

最近更新: 2/24/2020, 2:54:06 PM