深究Runc源碼-3-Init流程分析
代碼基于1.1.0
runc init并不通過(guò)runc 命令行暴露,而是runc內(nèi)部調(diào)用,如在create流程中parentProcess 通過(guò)Cmd.start,由于runc init.go導(dǎo)入了_ "github.com/opencontainers/runc/libcontainer/nsenter",所以運(yùn)行前首先會(huì)執(zhí)行nsenter,nsenter會(huì)首先執(zhí)行nsexec函數(shù)。
nsenter.go
nsenter的核心業(yè)務(wù)是設(shè)置runc init的Linux namespaces,首先需要了解一下Linux namespaces和相關(guān)的三個(gè)調(diào)用函數(shù),Linux Namespace是Linux提供的一種內(nèi)核級(jí)別環(huán)境隔離的方法,提供了對(duì)UTS、IPC、mount、PID、network、User等的隔離機(jī)制

對(duì)于的path如下:
系統(tǒng)調(diào)用如下:

nsexec核心流程是通過(guò)clone生成3個(gè)進(jìn)程,并通過(guò)管道pipe進(jìn)行進(jìn)程間同步

setup_logpipe根據(jù)_LIBCONTAINTER_LOGPIPE和_LIBCONTAINER_LOGLEVEL確定log fd和level,從啟動(dòng)Cmd環(huán)境變量帶入。update_oom_score_adj 通過(guò)/proc/self/oom_score_adj寫(xiě)入config.oom_score_adj。

clone_parent函數(shù)調(diào)用clone,子進(jìn)程跳轉(zhuǎn)到標(biāo)記處,父進(jìn)程返回clone子進(jìn)程pid
在STAGE_PARENT case中parent進(jìn)程中clone_parent生成child進(jìn)程,獲取child子進(jìn)程pid,child進(jìn)程立刻跳轉(zhuǎn)到STAGE_CHILD case運(yùn)行,同理在CHILD_STAGE中調(diào)用clone_parent生成grandchil進(jìn)程,獲取grandchild pid,grandchild立刻跳轉(zhuǎn)到STAGE_INIT case運(yùn)行。
STAGE_PARENT 完成child進(jìn)程clone后,主要有2段核心邏輯
1 與child進(jìn)程同步,并等待child進(jìn)程完成,在源碼中理解為stage1
2 與grandchild進(jìn)程同步,并等待grandchild完成,源碼中理解為stage2
stage1邏輯包含在parent和child中,分為3段case邏輯
1 SYNC_USERMAP
在child中,如果配置了config.namespaces首先執(zhí)行join_namespaces,通過(guò)setns設(shè)置child的namespaces,創(chuàng)建流程中為null,不執(zhí)行join_namespaces。首先執(zhí)行unshare(CLONE_NEWUSER)設(shè)置新的user namespace,根據(jù)源碼注釋,首先設(shè)置設(shè)置user namespace是因?yàn)槠鋾?huì)對(duì)其他namespaces unshare產(chǎn)生影響,并影響權(quán)限檢查,并且不同時(shí)unshare所有namespaces,是因?yàn)閗ernal存在bug,具體情況待進(jìn)一步深究
完成后發(fā)送SYNC_USERMAP_PLS到parent,parent SYNC_USERMAP_PLS case核心是根據(jù)config設(shè)置/proc/$child_pid/uid_map|gid_map,完成后發(fā)送SYNC_USERMAP_ACK,流程繼續(xù)到child,child執(zhí)行unshare除cgroup namespace之外的所有namespace,然后進(jìn)入另一個(gè)case邏輯
2 SYNC_MOUNTSOURCES
child發(fā)送 SYNC_MOUNTSOURCES_PLS到parent,parent中執(zhí)行send_mountresources分別獲區(qū)父進(jìn)程所在主機(jī)mnt namespace fd /proc/self/ns/mnt, 和child進(jìn)程mnt namespace fd /proc/$child_pid/ns/mnt, 通過(guò)setns將parent mount namespace加入到child mount namespace,然后讀取config.mountsources,并獲區(qū)對(duì)應(yīng)fd發(fā)送到child,child通過(guò)receive_mountsources獲取mount fd, 最后通過(guò)SYNC_MOUNTSOURCES_ACK結(jié)束SYNC_MOUNT流程
mountresources是什么待進(jìn)一步研究
3 SYNC_RECVPID
child通過(guò)clone_parent(STAGE_INIT)生成grandchild,并得到grandchild_pid,parent通過(guò)pipe將child_pid和grandchild_pid發(fā)送到runc create進(jìn)程,然后發(fā)送SYNC_RECVPID_PLS到parent,parent收到后發(fā)送SYNC_RECVPID_ACK到child,child收到后發(fā)送SYNC_CHILD_FINISH后結(jié)束進(jìn)程,parent收到SYNC_CHILD_FINISH結(jié)束stage1流程,進(jìn)入stage2
stage2主要流程是parent發(fā)送SYNC_GRANDCHILD,grandchild收到后調(diào)用unshare設(shè)置cgroup namespace,完成后發(fā)送SYNC_CHILD_FINISH,父進(jìn)程退出,grandchild return,流程返回到go runtime。
返回go runtime流程,代碼進(jìn)入init.go init方法


主要是構(gòu)建一個(gè)initer,create流程中實(shí)例化linuxStandInit,然后執(zhí)行Init方法
initConfig實(shí)例為
此時(shí)需要結(jié)合runc create和runc init流程同時(shí)分析,因?yàn)榱鞒贪瑀unc create與runc init之間的進(jìn)程同步和通信,主要通過(guò)INITPIPE進(jìn)行,是一個(gè)socket pair


流程來(lái)到linuxStandInit.Init,首先根據(jù)config設(shè)置網(wǎng)絡(luò)和路由 setupNetwork/setupRoute,然后調(diào)用prepareRootfs完成容器rootfs的設(shè)置,此邏輯較為關(guān)鍵。

在展開(kāi)rootfs邏輯前,先補(bǔ)充一下bind mount和shared subtree 兩個(gè)Mount基礎(chǔ)
參考https://man7.org/linux/man-pages/man8/mount.8.html https://man7.org/linux/man-pages/man7/mount_namespaces.7.html
一句話簡(jiǎn)述bind mount是將olddir掛載到newdir上,shared subtree主要是是控制mount事件的傳遞性,包括4種參數(shù),簡(jiǎn)單描述:
MS_SHARD:雙向傳遞
MS_PRIVATE:不傳遞
MS_SLAVE:向下傳遞
MS_UNBINDABLE:不能執(zhí)行bind mount
prepareRoot將rootfs的父掛載點(diǎn)設(shè)置為MS_PRIVATE,然后執(zhí)行bind mount Rootfs,結(jié)果如下
mountToRootfs依次掛在configs.Mount到rootfs,默認(rèn)幾個(gè)掛在/proc /sys /dev等,結(jié)果如下
mountConfig對(duì)象示例
configs.Mount對(duì)象示例
setUpDev判斷中createDevices根據(jù)configs.Devices創(chuàng)建設(shè)備,例如gpu,默認(rèn)情況下沒(méi)有Device設(shè)備,setupDevSyslinks設(shè)置標(biāo)準(zhǔn)輸入輸出鏈接等。
syncParentHooks通知runc create進(jìn)程 parentProcess.start執(zhí)行configs.Hooks下配置的Prestart/CreateRuntime Hook。
將目錄切換到rootfs執(zhí)行createContainer Hooks
然后默認(rèn)請(qǐng)繼續(xù)執(zhí)行pivotRoot,pivot_root是linux 系統(tǒng)調(diào)用,參考https://man7.org/linux/man-pages/man2/pivot_root.2.html,簡(jiǎn)單來(lái)講就是切換init進(jìn)程mount namespace root目錄到rootfs,完成后runc init的mountinfo變成如下形態(tài):
finalizeRootfs將/dev /等重新掛為ReadOnly。
Console/Seccomp/AppArmor/Selinux等邏輯再分析,先看主流程。流程來(lái)到unix.Open FIFOFd,runc init 進(jìn)程會(huì)在此處被阻塞住,runc create流程到此基本結(jié)束,由runc start觸發(fā)runc init繼續(xù)執(zhí)行process定義的命令。
開(kāi)源的東西,轉(zhuǎn)發(fā)不需要出處,就說(shuō)你自己寫(xiě)的