首页 > 网络 > 云计算 >

runc源码分析——create和start

2017-03-09

runc源码分析——create和start。根据容器id参数和spec信息用工厂模式创建了一个linux container实例。对listen fd做一些初始化操作,用于socket activation?构建一个runner对象,调用runner run。

Create

create.go#createCommand

utils_linux.go#startContainer

根据容器id参数和spec信息用工厂模式创建了一个linux container实例。对listen fd做一些初始化操作,用于socket activation?构建一个runner对象,调用runner.run

utils_linux.go#run

这个run是 create、start、run三个命令入口公用的,下面主要描述create流程。

根据spec里面process的配置信息调用newProcess创建process对象。将listen fd加入process的环境变量和需要在新进程保持打开的文件列表中(ExtraFiles)。调用setupIO来进行io和tty相关配置,对于create来说,这里就是dup将当前进程的io,chown用户/组权限。创建一个signalHandler来处理tty和signal。调用container.Start(process)来启动process进程。对于create来说,下面处理一下pid-file、tty回收等便返回了。

container_linux.go#Start

封装函数,仅是获取了当前容器状态(目前未创建前是stopped),并调用了容器的start(process, true)。

container_linux.go#start

首先调用newParentProcess来创建init的parent进程。调用parent.start()异步启动parent进程。根据parent进程的状态更新容器的状态为Created。遍历spec里面的Poststart hook,分别调用。

container_linux.go#newParentProcess

创建一个initProcess,里面既有init进程的信息,也有spec里面指定的process的信息。

创建一对pipe——parentPipe和childPipe,打开rootDir。创建一个command,命令为runc init自身(通过/proc/self/exe软链接实现);标准io为当前进程的;工作目录为Rootfs;用ExtraFiles在新进程中保持打开childPipe和rootDir,并添加对应的环境变量。调用newInitProcess进一步将parent process和command封装为initProcess。主要工作为添加初始化类型环境变量,将namespace、uid/gid映射等配置信息用bootstrapData封装为一个io.Reader等。

process_linux.go#initProcess.start()

异步启动cmd.Start()(等同于调用runc init)来启动init进程。将spec中process指定的ops指定为initProcess。将前面创建bootstrapData从parentPipe传出去(init进程会从childPipe接收到这些数据,reverse出写入的内容,进行namespace相关的配置)调用execSetns(),这个方法名看似是进行namespace的配置,实际上则是等待上面init进程的执行,并在parentPipe等待并解析出从childPipe传回的pid(谁的pid),找到该pid对应的进程,并将cmd.Process对应的进程替换为该进程。为checkpoint做准备,保存cmd.Process进程的标准IO文件描述符。应用cgroup配置创建容器中的network interface。将容器的配置文件内容spec从parentPipe发送给init进程。下面与init进程进行同步,一个for循环状态机,通过解析parentPipe传回的sync Type来执行相应的操作。按正常的时间顺序,如下:
procReady,继续配置cgroup(Set与Apply的区别?)、oom、rlimits;如果配置中没有mount namespace(Why?),则执行prestart钩子;往parentPipe写入procRun状态。procHooks,执行prestart钩子,往parentPipe写入procResume状态。(这个应该不是标准create的流程,resume?)procError,just error and exit 进行一些是否成功run和resume的判断,进行错误处理。关闭parentPipe,返回nil or err。

至此,parent端相关的操作分析便结束了,下面从init进程继续分析container的create流程。


factory_linux.go#StartInitialization()

前面稍微省略了几个不重要的步骤,main>initCommand>“here”

从环境变量中解析出childPipe、rootDir的fd以及initType(默认为standard,有时间看一下还有其他什么特别的初始化方式),并清除当前进程的所有环境变量。设置一个trap以及panic recover,如果初始化容器失败,会往childPipe中写入procError。调用newContainerInit创建一个init对象(两种类型,standard or setns,下面以standard为例),首先从childPipe中获取config配置文件,从配置文件中读取环境变量并设置到当前进程。构造一个linuxStandardInit对象,主要包括pipe、parentPid、config和rootDir等字段。调用linuxStandardInit对象的Init方法进行初始化。

standard_init_linux.go#Init

首先是针对Session keyring的一些配置,不是很清楚这里的Session是什么?配置console和tty。如果配置文件中指定有Console字段,则从该字段中获取tty的slave路径创建一个linuxConsole对象,调用其dupStdio打开slave设备,将其fd复制(dup3)到当前进程的标准IO。如果console对象创建好以后,便调用ioctl的TIOCSCTTY分配控制终端,这里应该是和4.3+BSD系统保持兼容。(关于tty和console的进一步内容,有时间转发一篇更详细的或者自己总结一篇也行,对这一部分也挺感兴趣)调用setupNetwork配置容器的网络。奇怪网络不是在前面配置过了吗,还是调用同样的函数。。。存疑?调用setupRoute配置容器的静态路由信息。selinux,调用label.Init()检查selinux是否被启动以及是否检查过,并将结果存入全局变量。此处的label并非是用户label,而是selinux相关的processLabel。如果设置了mount namespace,则调用setupRootfs在新的mount namespace中配置设备、挂载点以及文件系统。根据需要配置hostname、apparmor、processLabel、sysctl、readonlyPath、maskPath。这些都是一些feature,对容器启动本身没有太多影响。获取父进程的退出信号量。通过管道与父进程进行同步,先发出procReady再等待procRun。初始化seccomp。调用finalizeNamespace根据config配置将需要的特权capabilities加入白名单,设置user namespace,关闭不需要的文件描述符。恢复parent进程的death信号量并检查当前父进程pid是否为我们原来记录的。不是的话,kill ourself。。。检查config里面需要执行的命令是否存在。注意:create虽然不会执行命令,但是会检查命令路径是否正确,该错误类型也会在create期间返回。到此,与父进程之间的同步已经完成,关闭pipe。尝试以只写方式打开fifo管道,并往管道中写入“0” 。该操作会一直保持阻塞,直到管道的另一端以读方式打开,并读取内容。至此,create操作流程已经结束。ref : FIFO管道下面实际上是start的时候才会触发的操作了,阻塞清除后,根据config配置初始化seccomp,并调用syscall.Exec执行config里面指定的命令。

Start

start.go#startCommand

用生成的factory调用Load从容器文件夹中载入该容器的配置,生成container对象。获取container的状态,并对其判断。这里只有container状态是Created才是合法的。调用container.Exec

libcontainer/container_linux.go#Exec

这是个对linuxContainer.exec函数的同步封装,下面说的是exec函数.

以只读方式打开fifo管道,读取内容,如果长度大于0,则读取到Create流程中最后写入的“0”,也同时恢复阻塞了Create的init进程,执行最后调用用户进程部分。
相关文章
最新文章
热点推荐