本文接 探索runC(上) html
前文讲到,newParentProcess() 根据源自 config.json
的配置,最终生成变量 initProcess ,这个 initProcess 包含的信息主要有linux
_LIBCONTAINER_FIFOFD=%d
记录的命名管道exec.fifo
的描述符,名为_LIBCONTAINER_INITPIPE=%d
记录了建立的 SocketPair 的 childPipe 一端的描述符,名为_LIBCONTAINER_INITTYPE="standard"
记录要建立的容器中的进程是初始进程Namespace
。/* libcontainer/container_linux.go */ func (c *linuxContainer) start(process *Process) error { parent, err := c.newParentProcess(process) /* 1. 建立parentProcess (已完成) */ err := parent.start(); /* 2. 启动这个parentProcess */ ......
准备工做完成以后,就要调用 start() 方法启动。git
注意: 此时 sleep 5 线索存储在变量 parent 中
start() 函数实在太长了,所以逐段来看github
/* libcontainer/process_linux.go */ func (p *initProcess) start() error { p.cmd.Start() p.process.ops = p io.Copy(p.parentPipe, p.bootstrapData) ..... }
p.cmd.Start()
启动 cmd 中设置的要执行的可执行文件 /proc/self/exe,参数是 init,这个函数会启动一个新的进程去执行该命令,而且不会阻塞。io.Copy
将 p.bootstrapData 中的数据经过 p.parentPipe 发送给子进程/proc/self/exe 正是runc
程序本身,因此这里至关因而执行runc init
,也就是说,咱们输入的是runc create
命令,隐含着又去建立了一个新的子进程去执行runc init
。为何要额外从新建立一个进程呢?缘由是咱们建立的容器极可能须要运行在一些独立的 namespace
中,好比 user namespace
,这是经过 setns()
系统调用完成的,而在setns man page中写了下面一段话golang
A multi‐threaded process may not change user namespace with setns(). It is not permitted to use setns() to reenter the caller's current user names‐pace
即多线程的进程是不能经过 setns()
改变user namespace
的。而不幸的是 Go runtime 是多线程的。那怎么办呢 ?因此setns()
必需要在Go runtime 启动以前就设置好,这就要用到cgo了,在Go runtime 启动前首先执行嵌入在前面的 C 代码。json
具体的作法在nsenter README描述 在runc init
命令的响应在文件 init.go 开头,导入 nsenter
包bootstrap
/* init.go */ import ( "os" "runtime" "github.com/opencontainers/runc/libcontainer" _ "github.com/opencontainers/runc/libcontainer/nsenter" "github.com/urfave/cli" )
而nsenter
包中开头经过 cgo
嵌入了一段 C 代码, 调用 nsexec()segmentfault
package nsenter /* /* nsenter.go */ #cgo CFLAGS: -Wall extern void nsexec(); void __attribute__((constructor)) init(void) { nsexec(); } */ import "C"
接下来,轮到 nsexec() 完成为容器建立新的 namespace
的工做了, nsexec() 一样很长,逐段来看多线程
/* libcontainer/nsenter/nsexec.c */ void nsexec(void) { int pipenum; jmp_buf env; int sync_child_pipe[2], sync_grandchild_pipe[2]; struct nlconfig_t config = { 0 }; /* * If we don't have an init pipe, just return to the go routine. * We'll only get an init pipe for start or exec. */ pipenum = initpipe(); if (pipenum == -1) return; /* Parse all of the netlink configuration. */ nl_parse(pipenum, &config); ......
上面这段 C 代码中,initpipe() 从环境中读取父进程以前设置的pipe
(_LIBCONTAINER_INITPIPE
记录的的文件描述符),而后调用 nl_parse 从这个管道中读取配置到变量 config ,那么谁会往这个管道写配置呢 ? 固然就是runc create
父进程了。父进程经过这个pipe
,将新建容器的配置发给子进程,这个过程以下图所示:socket
发送的具体数据在 linuxContainer 的 bootstrapData() 函数中封装成netlink msg
格式的消息。忽略大部分配置,本文重点关注namespace
的配置,即要建立哪些类型的namespace
,这些都是源自最初的config.json
文件。
至此,子进程就从父进程处获得了namespace
的配置,继续往下, nsexec() 又建立了两个socketpair
,从注释中了解到,这是为了和它本身的子进程和孙进程进行通讯。
void nsexec(void) { ..... /* Pipe so we can tell the child when we've finished setting up. */ if (socketpair(AF_LOCAL, SOCK_STREAM, 0, sync_child_pipe) < 0) // sync_child_pipe is an out parameter bail("failed to setup sync pipe between parent and child"); /* * We need a new socketpair to sync with grandchild so we don't have * race condition with child. */ if (socketpair(AF_LOCAL, SOCK_STREAM, 0, sync_grandchild_pipe) < 0) bail("failed to setup sync pipe between parent and grandchild"); }
而后就该建立namespace
了,看注释可知这里其实有考虑过三个方案
最终采用的是方案 3,其中原因因为考虑因素太多,因此准备以后另写一篇文章分析
接下来就是一个大的 switch case 编写的状态机,大致结构以下,当前进程经过clone()
系统调用建立子进程,子进程又经过clone()
系统调用建立孙进程,而实际的建立/加入namespace
是在子进程完成的
switch (setjmp(env)) { case JUMP_PARENT:{ ..... clone_parent(&env, JUMP_CHILD); ..... } case JUMP_CHILD:{ ...... if (config.namespaces) join_namespaces(config.namespaces); clone_parent(&env, JUMP_INIT); ...... } case JUMP_INIT:{ }
本文不许备展开分析这个状态机了,而将这个状态机的流程画在了下面的时序图中,须要注意的是如下几点
namespaces
在runc init 2
完成建立runc init 1
和runc init 2
最终都会执行exit(0)
,但runc init 3
不会,它会继续执行runc init
命令的后半部分。所以最终只会剩下runc create
进程和runc init 3
进程再回到runc create
进程
func (p *initProcess) start() error { p.cmd.Start() p.process.ops = p io.Copy(p.parentPipe, p.bootstrapData); p.execSetns() ......
再向 runc init
发送了 bootstrapData 数据后,便调用 execSetns() 等待runc init 1
进程终止,从管道中获得runc init 3
的进程 pid,将该进程保存在 p.process.ops
/* libcontainer/process_linux.go */ func (p *initProcess) execSetns() error { status, err := p.cmd.Process.Wait() var pid *pid json.NewDecoder(p.parentPipe).Decode(&pid) process, err := os.FindProcess(pid.Pid) p.cmd.Process = process p.process.ops = p return nil }
继续 start()
func (p *initProcess) start() error { ...... p.execSetns() fds, err := getPipeFds(p.pid()) p.setExternalDescriptors(fds) p.createNetworkInterfaces() p.sendConfig() parseSync(p.parentPipe, func(sync *syncT) error { switch sync.Type { case procReady: ..... writeSync(p.parentPipe, procRun); sentRun = true case procHooks: ..... // Sync with child. err := writeSync(p.parentPipe, procResume); sentResume = true } return nil }) ......
能够看到,runc create
又开始经过pipe
进行双向通讯了,通讯的对端天然就是runc init 3
进程了,runc init 3
进程在执行完嵌入的 C 代码后(实际是runc init 1
执行的,但runc init 3
也是由runc init 1
间接clone()
出来的),所以将开始运行 Go runtime,开始响应init
命令
sleep 5 经过 p.sendConfig() 发送给了
runc init
进程
init
命令首先经过 libcontainer.New("") 建立了一个 LinuxFactory,这个方法在上篇文章中分析过,这里再也不解释。而后调用 LinuxFactory 的 StartInitialization() 方法。
/* libcontainer/factory_linux.go */ // StartInitialization loads a container by opening the pipe fd from the parent to read the configuration and state // This is a low level implementation detail of the reexec and should not be consumed externally func (l *LinuxFactory) StartInitialization() (err error) { var ( pipefd, fifofd int envInitPipe = os.Getenv("_LIBCONTAINER_INITPIPE") envFifoFd = os.Getenv("_LIBCONTAINER_FIFOFD") ) // Get the INITPIPE. pipefd, err = strconv.Atoi(envInitPipe) var ( pipe = os.NewFile(uintptr(pipefd), "pipe") it = initType(os.Getenv("_LIBCONTAINER_INITTYPE")) // // "standard" or "setns" ) // Only init processes have FIFOFD. fifofd = -1 if it == initStandard { if fifofd, err = strconv.Atoi(envFifoFd); err != nil { return fmt.Errorf("unable to convert _LIBCONTAINER_FIFOFD=%s to int: %s", envFifoFd, err) } } i, err := newContainerInit(it, pipe, consoleSocket, fifofd) // If Init succeeds, syscall.Exec will not return, hence none of the defers will be called. return i.Init() // }
StartInitialization() 方法尝试从环境中读取一系列_LIBCONTAINER_XXX
变量的值,还有印象吗?这些值全是在runc create
命令中打开和设置的,也就是说,runc create
经过环境变量,将这些参数传给了子进程runc init 3
拿到这些环境变量后,runc init 3
调用 newContainerInit 函数
/* libcontainer/init_linux.go */ func newContainerInit(t initType, pipe *os.File, consoleSocket *os.File, fifoFd int) (initer, error) { var config *initConfig /* read config from pipe (from runc process) */ son.NewDecoder(pipe).Decode(&config); populateProcessEnvironment(config.Env); switch t { ...... case initStandard: return &linuxStandardInit{ pipe: pipe, consoleSocket: consoleSocket, parentPid: unix.Getppid(), config: config, // <=== config fifoFd: fifoFd, }, nil } return nil, fmt.Errorf("unknown init type %q", t) }
newContainerInit() 函数首先尝试从 pipe
读取配置存放到变量 config 中,再存储到变量 linuxStandardInit 中返回
runc create runc init 3 | | p.sendConfig() --- config --> NewContainerInit()
sleep 5 线索在 initStandard.config 中
回到 StartInitialization(),在获得 linuxStandardInit 后,便调用其 Init()方法了
/* init.go */ func (l *LinuxFactory) StartInitialization() (err error) { ...... i, err := newContainerInit(it, pipe, consoleSocket, fifofd) return i.Init() }
本文忽略掉 Init() 方法前面的一大堆其余配置,只看其最后
func (l *linuxStandardInit) Init() error { ...... name, err := exec.LookPath(l.config.Args[0]) syscall.Exec(name, l.config.Args[0:], os.Environ()) }
能够看到,这里终于开始执行 用户最初设置的 sleep 5
了