我们知道,runc是使用init进程作为容器内的第一个进程,来看下面代码:

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
//***构造init进程***//
//***通过process中的信息构建cmd***//
func (c *linuxContainer) commandTemplate(p *Process, childPipe, rootDir *os.File) (*exec.Cmd, error) {
//***Fankang***//
//***initArgs[0]: /proc/self/exe***//
//***initArgs[1:]: [init]***//
cmd := exec.Command(c.initArgs[0], c.initArgs[1:]...)
cmd.Stdin = p.Stdin
cmd.Stdout = p.Stdout
cmd.Stderr = p.Stderr
cmd.Dir = c.config.Rootfs
if cmd.SysProcAttr == nil {
cmd.SysProcAttr = &syscall.SysProcAttr{}
}
//***Fankang***//
//***把childPipe放到cmd的ExtraFiles中***//
cmd.ExtraFiles = append(p.ExtraFiles, childPipe, rootDir)
//***Fankang***//
//***在环境变量中加入_LIBCONTAINER_INITPIPE和_LIBCONTAINER_STATEDIR***//
cmd.Env = append(cmd.Env,
fmt.Sprintf("_LIBCONTAINER_INITPIPE=%d", stdioFdCount+len(cmd.ExtraFiles)-2),
fmt.Sprintf("_LIBCONTAINER_STATEDIR=%d", stdioFdCount+len(cmd.ExtraFiles)-1))
// NOTE: when running a container with no PID namespace and the parent process spawning the container is
// PID1 the pdeathsig is being delivered to the container's init process by the kernel for some reason
// even with the parent still running.
if c.config.ParentDeathSignal > 0 {
cmd.SysProcAttr.Pdeathsig = syscall.Signal(c.config.ParentDeathSignal)
}
return cmd, nil
}

可以看到,init cmd的SysProcAttr并没有加入namespace,也就是说init进程没有自己的namespace。但明明容器起来后是有自己的namespace的, 本次分析就介绍runc创建namespace的主要流程。

bootstrapData

先来看/libcontainer/container_linux.go中的newInitProcess():

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
func (c *linuxContainer) newInitProcess(p *Process, cmd *exec.Cmd, parentPipe, childPipe, rootDir *os.File) (*initProcess, error) {
cmd.Env = append(cmd.Env, "_LIBCONTAINER_INITTYPE="+string(initStandard))
nsMaps := make(map[configs.NamespaceType]string)
for _, ns := range c.config.Namespaces {
if ns.Path != "" {
nsMaps[ns.Type] = ns.Path
}
}
_, sharePidns := nsMaps[configs.NEWPID]
data, err := c.bootstrapData(c.config.Namespaces.CloneFlags(), nsMaps, "")
if err != nil {
return nil, err
}
return &initProcess{
cmd: cmd,
childPipe: childPipe,
parentPipe: parentPipe,
manager: c.cgroupManager,
config: c.newInitConfig(p),
container: c,
process: p,
bootstrapData: data,
sharePidns: sharePidns,
rootDir: rootDir,
}, nil
}

newInitProcess()调用bootstrapData()生成bootstrapData。

bootstrapData()同样定义在/libcontainer/container_linux.go中:

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
// bootstrapData encodes the necessary data in netlink binary format
// as a io.Reader.
// Consumer can write the data to a bootstrap program
// such as one that uses nsenter package to bootstrap the container's
// init process correctly, i.e. with correct namespaces, uid/gid
// mapping etc.
func (c *linuxContainer) bootstrapData(cloneFlags uintptr, nsMaps map[configs.NamespaceType]string, consolePath string) (io.Reader, error) {
// create the netlink message
r := nl.NewNetlinkRequest(int(InitMsg), 0)
// write cloneFlags
r.AddData(&Int32msg{
Type: CloneFlagsAttr,
Value: uint32(cloneFlags),
})
// write console path
if consolePath != "" {
r.AddData(&Bytemsg{
Type: ConsolePathAttr,
Value: []byte(consolePath),
})
}
// write custom namespace paths
if len(nsMaps) > 0 {
nsPaths, err := c.orderNamespacePaths(nsMaps)
if err != nil {
return nil, err
}
r.AddData(&Bytemsg{
Type: NsPathsAttr,
Value: []byte(strings.Join(nsPaths, ",")),
})
}
// write namespace paths only when we are not joining an existing user ns
_, joinExistingUser := nsMaps[configs.NEWUSER]
if !joinExistingUser {
// write uid mappings
if len(c.config.UidMappings) > 0 {
b, err := encodeIDMapping(c.config.UidMappings)
if err != nil {
return nil, err
}
r.AddData(&Bytemsg{
Type: UidmapAttr,
Value: b,
})
}
// write gid mappings
if len(c.config.GidMappings) > 0 {
b, err := encodeIDMapping(c.config.GidMappings)
if err != nil {
return nil, err
}
r.AddData(&Bytemsg{
Type: GidmapAttr,
Value: b,
})
// check if we have CAP_SETGID to setgroup properly
pid, err := capability.NewPid(os.Getpid())
if err != nil {
return nil, err
}
if !pid.Get(capability.EFFECTIVE, capability.CAP_SETGID) {
r.AddData(&Boolmsg{
Type: SetgroupAttr,
Value: true,
})
}
}
}
return bytes.NewReader(r.Serialize()), nil
}
// orderNamespacePaths sorts namespace paths into a list of paths that we
// can setns in order.
func (c *linuxContainer) orderNamespacePaths(namespaces map[configs.NamespaceType]string) ([]string, error) {
paths := []string{}
nsTypes := []configs.NamespaceType{
configs.NEWIPC,
configs.NEWUTS,
configs.NEWNET,
configs.NEWPID,
configs.NEWNS,
}
// join userns if the init process explicitly requires NEWUSER
if c.config.Namespaces.Contains(configs.NEWUSER) {
nsTypes = append(nsTypes, configs.NEWUSER)
}
for _, nsType := range nsTypes {
if p, ok := namespaces[nsType]; ok && p != "" {
// check if the requested namespace is supported
if !configs.IsNamespaceSupported(nsType) {
return nil, newSystemError(fmt.Errorf("namespace %s is not supported", nsType))
}
// only set to join this namespace if it exists
if _, err := os.Lstat(p); err != nil {
return nil, newSystemErrorWithCausef(err, "running lstat on namespace path %q", p)
}
// do not allow namespace path with comma as we use it to separate
// the namespace paths
if strings.ContainsRune(p, ',') {
return nil, newSystemError(fmt.Errorf("invalid path %s", p))
}
paths = append(paths, p)
}
}
return paths, nil
}

所以,bootstrapData中存储有各namespace的信息。

initProcess.start()

再来看initProcess的start()方法,在/libcontainer/process_linux.go中:

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
//***在容器外部执行,新建容器执行命令***//
func (p *initProcess) start() error {
defer p.parentPipe.Close()
//***开始运行进程***//
//***&{/proc/self/exe [/proc/self/exe init] [_LIBCONTAINER_INITPIPE=3 _LIBCONTAINER_STATEDIR=4 _LIBCONTAINER_INITTYPE=standard] /home/fankang/mycontainer/rootfs 0xc420026008 0xc420026010 0xc420026018 [0xc420026118 0xc420026120] 0xc420088240 <nil> <nil> <nil> <nil> false [] [] [] [] <nil> <nil>}***//
err := p.cmd.Start()
p.process.ops = p
p.childPipe.Close()
p.rootDir.Close()
if err != nil {
p.process.ops = nil
return newSystemErrorWithCause(err, "starting init process command")
}
//***向parentPipe发送bootstrapData***//
//***nsexec.c会拿到bootstrapData***//
if _, err := io.Copy(p.parentPipe, p.bootstrapData); err != nil {
return err
}
if err := p.execSetns(); err != nil {
return newSystemErrorWithCause(err, "running exec setns process for init")
}
......
//***通过管道发送配置文件给子进程***//
//***此处会阻塞住init_linux.go的newContainerInit()获取config***//
if err := p.sendConfig(); err != nil {
return newSystemErrorWithCause(err, "sending config to init process")
}
......
return nil
}

可以看到,initProcess运行的是”/proc/self/exe init”程序。在程序执行后,把bootstrapData发送给parentPipe。好,现在bootstrapData通过parentPipe发送出去了。那么,谁会接收呢?
答案是nsexec.c。当然,在init程序中,也会接收parentPipe发送的信息,如parent就是通过parentPipe发送config的,即sedConfig()。所以,nsexec.c必须有能力阻塞init进程往下执行,不然消息就乱套了。其实nsexec.c会在init启动时就阻塞了init进程。

nsexec.c

nsexec.c是定义在/libcontainer/nsenter/nsexec.c中的C语言代码,其功能就是依据bootstrapData重新设置init进程的namespace,user等属性。关于nsexec.c的代码,还没作详细地研究,现在只知道只要import该包,代码就生效了:

1
import _ "github.com/opencontainers/runc/libcontainer/nsenter"

nsexec.c会从”_LIBCONTAINER_INITPIPE”环境变量中拿到pipe,并读取bootstrapData。然后,nsexec.c会调用clone()进行复制,在clone()时,传入参数CLONE_PARENT及命令空间参数,使用子进程和父进程成为兄弟关系,且拥有了自己的命名空间。接着调用setns()进行已存在的命名空间的处理。

所以不妨可以这样认为,nsexec.c具有劫持init进程的功能。

来看start()中等待的方法execSetns(),定义在/libcontainer/process_linux.go中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func (p *initProcess) execSetns() error {
//***等待进程执行完成***//
status, err := p.cmd.Process.Wait()
if err != nil {
p.cmd.Wait()
return err
}
if !status.Success() {
p.cmd.Wait()
return &exec.ExitError{ProcessState: status}
}
var pid *pid
if err := json.NewDecoder(p.parentPipe).Decode(&pid); err != nil {
p.cmd.Wait()
return err
}
process, err := os.FindProcess(pid.Pid)
if err != nil {
return err
}
p.cmd.Process = process
p.process.ops = p
return nil
}

可以看到,execSetns()会等cmd的Process执行完成后,从parentPipe中读取新进程的信息,并把新进程赋值给cmd,而这个新进程就是经过nsexec.c处理过的进程。这样,cmd中的进程就是在正确的namespace中的了。所以,在execSetns()中的Process.Wait(),等待的是nsexec.c的完成,nsexec.c执行完后,会自动交还执行权限,即init进程会往下执行。

nsexec.c在/main_unix.go中被import:

1
_ "github.com/opencontainers/runc/libcontainer/nsenter"

疑问

这里还有几个疑问需要慢慢分析:

1 Go语言exec中的cmd, process的关系;

2 为什么要用c语言实现

根据”自己动手写Docker”,这是因为对于Mount Namespace来说,一个具有多线程的进程是无法使用setns调用进入到对应的命名空间的。

3 nsexec.c如何劫持执行流程

可参考https://zhuanlan.zhihu.com/p/23456448
在runc的/libcontainer/nsenter/nsenter.go中:

1
2
3
4
5
6
7
8
/*
#cgo CFLAGS: -Wall
extern void nsexec();
void __attribute__((constructor)) init(void) {
nsexec();
}
*/
import "C"

这段代码就是”劫持”执行流程的代码。

4 nsexec.c的执行流程

先执行clone(),参数有CLONE_PARENT及命名空间参数,使用子进程和父进程是兄弟关系,并拥有自己的命名空间。
然后调用setns()加入存在的namespace。
核心代码是调用SYS_setns。