我们知道,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
| func (c *linuxContainer) commandTemplate(p *Process, childPipe, rootDir *os.File) (*exec.Cmd, error) { 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{} } cmd.ExtraFiles = append(p.ExtraFiles, childPipe, rootDir) 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)) 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
| func (c *linuxContainer) bootstrapData(cloneFlags uintptr, nsMaps map[configs.NamespaceType]string, consolePath string) (io.Reader, error) { r := nl.NewNetlinkRequest(int(InitMsg), 0) r.AddData(&Int32msg{ Type: CloneFlagsAttr, Value: uint32(cloneFlags), }) if consolePath != "" { r.AddData(&Bytemsg{ Type: ConsolePathAttr, Value: []byte(consolePath), }) } 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, ",")), }) } _, joinExistingUser := nsMaps[configs.NEWUSER] if !joinExistingUser { 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, }) } 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, }) 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 } 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, } if c.config.Namespaces.Contains(configs.NEWUSER) { nsTypes = append(nsTypes, configs.NEWUSER) } for _, nsType := range nsTypes { if p, ok := namespaces[nsType]; ok && p != "" { if !configs.IsNamespaceSupported(nsType) { return nil, newSystemError(fmt.Errorf("namespace %s is not supported", nsType)) } if _, err := os.Lstat(p); err != nil { return nil, newSystemErrorWithCausef(err, "running lstat on namespace path %q", p) } 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() 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") } 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") } ...... 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。