深究Runc源码-3-Init流程分析
代码基于1.1.0
runc init并不通过runc 命令行暴露,而是runc内部调用,如在create流程中parentProcess 通过Cmd.start,由于runc init.go导入了_ "github.com/opencontainers/runc/libcontainer/nsenter",所以运行前首先会执行nsenter,nsenter会首先执行nsexec函数。
nsenter.go
nsenter的核心业务是设置runc init的Linux namespaces,首先需要了解一下Linux namespaces和相关的三个调用函数,Linux Namespace是Linux提供的一种内核级别环境隔离的方法,提供了对UTS、IPC、mount、PID、network、User等的隔离机制

对于的path如下:
系统调用如下:

nsexec核心流程是通过clone生成3个进程,并通过管道pipe进行进程间同步

setup_logpipe根据_LIBCONTAINTER_LOGPIPE和_LIBCONTAINER_LOGLEVEL确定log fd和level,从启动Cmd环境变量带入。update_oom_score_adj 通过/proc/self/oom_score_adj写入config.oom_score_adj。

clone_parent函数调用clone,子进程跳转到标记处,父进程返回clone子进程pid
在STAGE_PARENT case中parent进程中clone_parent生成child进程,获取child子进程pid,child进程立刻跳转到STAGE_CHILD case运行,同理在CHILD_STAGE中调用clone_parent生成grandchil进程,获取grandchild pid,grandchild立刻跳转到STAGE_INIT case运行。
STAGE_PARENT 完成child进程clone后,主要有2段核心逻辑
1 与child进程同步,并等待child进程完成,在源码中理解为stage1
2 与grandchild进程同步,并等待grandchild完成,源码中理解为stage2
stage1逻辑包含在parent和child中,分为3段case逻辑
1 SYNC_USERMAP
在child中,如果配置了config.namespaces首先执行join_namespaces,通过setns设置child的namespaces,创建流程中为null,不执行join_namespaces。首先执行unshare(CLONE_NEWUSER)设置新的user namespace,根据源码注释,首先设置设置user namespace是因为其会对其他namespaces unshare产生影响,并影响权限检查,并且不同时unshare所有namespaces,是因为kernal存在bug,具体情况待进一步深究
完成后发送SYNC_USERMAP_PLS到parent,parent SYNC_USERMAP_PLS case核心是根据config设置/proc/$child_pid/uid_map|gid_map,完成后发送SYNC_USERMAP_ACK,流程继续到child,child执行unshare除cgroup namespace之外的所有namespace,然后进入另一个case逻辑
2 SYNC_MOUNTSOURCES
child发送 SYNC_MOUNTSOURCES_PLS到parent,parent中执行send_mountresources分别获区父进程所在主机mnt namespace fd /proc/self/ns/mnt, 和child进程mnt namespace fd /proc/$child_pid/ns/mnt, 通过setns将parent mount namespace加入到child mount namespace,然后读取config.mountsources,并获区对应fd发送到child,child通过receive_mountsources获取mount fd, 最后通过SYNC_MOUNTSOURCES_ACK结束SYNC_MOUNT流程
mountresources是什么待进一步研究
3 SYNC_RECVPID
child通过clone_parent(STAGE_INIT)生成grandchild,并得到grandchild_pid,parent通过pipe将child_pid和grandchild_pid发送到runc create进程,然后发送SYNC_RECVPID_PLS到parent,parent收到后发送SYNC_RECVPID_ACK到child,child收到后发送SYNC_CHILD_FINISH后结束进程,parent收到SYNC_CHILD_FINISH结束stage1流程,进入stage2
stage2主要流程是parent发送SYNC_GRANDCHILD,grandchild收到后调用unshare设置cgroup namespace,完成后发送SYNC_CHILD_FINISH,父进程退出,grandchild return,流程返回到go runtime。
返回go runtime流程,代码进入init.go init方法


主要是构建一个initer,create流程中实例化linuxStandInit,然后执行Init方法
initConfig实例为
此时需要结合runc create和runc init流程同时分析,因为流程包含runc create与runc init之间的进程同步和通信,主要通过INITPIPE进行,是一个socket pair


流程来到linuxStandInit.Init,首先根据config设置网络和路由 setupNetwork/setupRoute,然后调用prepareRootfs完成容器rootfs的设置,此逻辑较为关键。

在展开rootfs逻辑前,先补充一下bind mount和shared subtree 两个Mount基础
参考https://man7.org/linux/man-pages/man8/mount.8.html https://man7.org/linux/man-pages/man7/mount_namespaces.7.html
一句话简述bind mount是将olddir挂载到newdir上,shared subtree主要是是控制mount事件的传递性,包括4种参数,简单描述:
MS_SHARD:双向传递
MS_PRIVATE:不传递
MS_SLAVE:向下传递
MS_UNBINDABLE:不能执行bind mount
prepareRoot将rootfs的父挂载点设置为MS_PRIVATE,然后执行bind mount Rootfs,结果如下
mountToRootfs依次挂在configs.Mount到rootfs,默认几个挂在/proc /sys /dev等,结果如下
mountConfig对象示例
configs.Mount对象示例
setUpDev判断中createDevices根据configs.Devices创建设备,例如gpu,默认情况下没有Device设备,setupDevSyslinks设置标准输入输出链接等。
syncParentHooks通知runc create进程 parentProcess.start执行configs.Hooks下配置的Prestart/CreateRuntime Hook。
将目录切换到rootfs执行createContainer Hooks
然后默认请继续执行pivotRoot,pivot_root是linux 系统调用,参考https://man7.org/linux/man-pages/man2/pivot_root.2.html,简单来讲就是切换init进程mount namespace root目录到rootfs,完成后runc init的mountinfo变成如下形态:
finalizeRootfs将/dev /等重新挂为ReadOnly。
Console/Seccomp/AppArmor/Selinux等逻辑再分析,先看主流程。流程来到unix.Open FIFOFd,runc init 进程会在此处被阻塞住,runc create流程到此基本结束,由runc start触发runc init继续执行process定义的命令。
开源的东西,转发不需要出处,就说你自己写的