本文接 探索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
了