http://www.sel.zju.edu.cn/?p=840
深入理解Docker容器引擎runC執行框架
1 簡介
根據官方的定義:runC是一個根據OCI標准創建並運行容器的CLI tool。
Docker就是基於runC創建的,簡單地說,runC是Docker中最為核心的部分,容器的創建,運行,銷毀等等操作最終都將通過調用runC完成。下面我們將演示如何使用runC,以最精簡的方式創建並運行一個容器。
1.1 利用runC運行busybox容器
下載並編譯runC
1
2
3
4
5
6
7
8
|
# create a 'github.com/opencontainers' in your GOPATH/src
cd github.com/opencontainers
git clone https://github.com/opencontainers/runc
cd runc
make
sudo make install
|
創建容器的根文件系統
1
2
3
4
5
6
7
8
9
10
|
# create the top most bundle directory
mkdir /mycontainer
cd /mycontainer
# create the rootfs directory
mkdir rootfs
# export busybox via Docker into the rootfs directory
docker export $(docker create busybox) | tar -C rootfs -xvf -
|
利用runC的spec命令創建默認的配置文件config.json,其中包含了創建一個容器所需的所有配置信息
1
2
|
runc spec
|
利用runC運行busybox容器
1
2
3
4
5
6
7
|
# run as root
cd /mycontainer
runc run mycontainerid
/ # ls
bin dev etc home proc root sys tmp usr var
|
可以看到,容器成功運行,此時我們打開另一個終端觀察容器的運行狀態
1
2
3
4
|
runc list
ID PID STATUS BUNDLE CREATED OWNER
mycontainerid 1070 running /mycontainer 2017-12-20T12:26:30.159978871Z root
|
事實上,runc run
是一個復合命令,它包含了容器的創建runc create
,啟動runc start
以及在退出之后對容器進行的銷毀runc delete
,從演示的角度看它是最為直觀的。但是如果想要深入理解runC內部的實現機制,將容器的創建,啟動,銷毀三個步驟分開,顯然會讓整個過程的分析更為簡單和易於接受
下面我們就將結合源碼,對整個容器技術最為核心的部分進行探究—— 容器是如何創建並啟動的
2 源碼分析
首先,我們來對runC的整體代碼結構做一個宏觀的把控:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
[root@VM_68_206_centos runc]# tree -L 1 -F --dirsfirst
.
|-- contrib/
|-- libcontainer/
|-- man/
|-- script/
|-- tests/
|-- vendor/
|-- checkpoint.go
|-- CONTRIBUTING.md
|-- create.go
|-- delete.go
|-- Dockerfile
|-- events.go
|-- exec.go
|-- init.go
|-- start.go
|-- run.go
......
|
可以看到在runC的頂層目錄中,有着一系列形如create.go
, start.go
, run.go
…的go文件,它們和runC的子命令,例如runc create...
,runc start...
,runc run
是一致的。 另外,在頂層目錄中還有一個名為libcontainer
的子目錄。對於Docker項目的發展歷史有所了解的同學應該都知道,libcontainer
曾經是Docker中最為核心的包,容器的創建,刪除等一系列工作,最終都是交由它來完成的。
這樣一來,runC的代碼結構就非常清晰了。我們知道,runC是符合OCI標准的容器運行時。不難猜出,它本質上是對libcontainer
的一層薄薄的封裝。它會先讀取符合OCI標准的容器配置,再將其轉換成與libcontainer
兼容的格式,最后將轉換后的配置交由libcontainer
來完成具體的工作。
2.1 容器創建
1
2
3
4
5
6
7
|
// runc/create.go
...
spec, err := setupSpec(context)
...
status, err := startContainer(context, spec, CT_ACT_CREATE, nil)
...
|
create.go的工作主要分為如下兩部分:
- 將容器配置從config.json文件加載到內存中,保存在一個類型為*specs.Spec (Spec即為OCI標准的容器配置在內存中的表現形式)的結構體中
- 調用
startContainer()
完成容器的創建工作,值得注意的是runc run
,runc create
以及runc restore
最終都將調用該函數,只是第三個參數不同而已,對於runc create
,該參數為CT_ACT_CREATE
,表示首次創建容器。接下來程序的執行路徑將因該參數的不同而有所不同。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
// runc/utils_linux.go
func startContainer(context *cli.Context, spec *specs.Spec, action CtAct, criuOpts *libcontainer.CriuOpts) (int, error) {
// 從參數中獲取容器的id
id := context.Args().First()
if id == "" {
return -1, errEmptyID
}
....
// 創建符合libcontainer格式的container數據結構
container, err := createContainer(context, id, spec)
if err != nil {
return -1, err
}
....
// 創建runner對象
r := &runner{
enableSubreaper: !context.Bool("no-subreaper"),
shouldDestroy: true,
container: container,
listenFDs: listenFDs,
notifySocket: notifySocket,
consoleSocket: context.String("console-socket"),
detach: context.Bool("detach"),
pidFile: context.String("pid-file"),
preserveFDs: context.Int("preserve-fds"),
action: action,
criuOpts: criuOpts,
}
return r.run(spec.Process)
}
|
startContainer
的工作由如下三部分組成:
- 從參數中獲取容器的id,例如對於命令
runc create abc
,則獲取的id即為abc
- 調用
createContainer
,根據spec中Container相關的內容,調用libcontainer
創建容器對象,且容器的狀態設置為Stopped
。此時的容器僅僅只是一個內存中的數據結構,並沒有與之對應的進程 - 創建
runner
對象並調用相應的run
方法,該對象會將spec中的Process轉換成libcontainer
兼容的模式,並對容器的IO進行配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// runc/utils_linux.go
func (r *runner) run(config *specs.Process) (int, error) {
...
// 將spec的Process轉換為libcontainer要求的Process配置格式
process, err := newProcess(*config)
if err != nil {
r.destroy()
return -1, err
}
...
tty, err := setupIO(process, rootuid, rootgid, config.Terminal, detach, r.consoleSocket)
...
switch r.action {
// 根據action,創建,恢復或者運行容器
case CT_ACT_CREATE:
err = r.container.Start(process)
case CT_ACT_RESTORE:
err = r.container.Restore(process, r.criuOpts)
case CT_ACT_RUN:
err = r.container.Run(process)
default:
panic("Unknown action")
}
|
run
方法的工作同樣由三部分組成:
- 將OCI標准的進程配置
specs.Process
轉換為符合libcontainer
格式的進程配置libcontainer.Process
- 調用
setupIO
對進程的IO進行配置,因為IO涉及的內容較為復雜,會在另外的文章中詳細敘述 - 根據
startContainer
配置參數的不同,調用不同的方法,分別進行容器的創建,運行或者恢復,本文我們只討論CT_ACT_CREATE
這種情況
到此為止,我們已經將OCI格式的配置,不管是Container還是Process都轉換成了libcontainer
要求的格式。接着我們將深入libcontainer
,真正完成容器實例的創建工作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
// runc/libcontainer/container_linux.go
func (c *linuxContainer) Start(process *Process) error {
...
status, err := c.currentStatus()
...
if status == Stopped {
// 如果容器的狀態為Stopped,則先創建管道exec.fifo
if err := c.createExecFifo(); err != nil {
return err
}
}
if err := c.start(process, status == Stopped); err != nil {
if status == Stopped {
// 如果從Stopped狀態啟動失敗,則刪除管道exec.fifo
c.deleteExecFifo()
}
return err
}
return nil
}
|
Start
方法僅僅只是對start
的一個封裝並且會在容器狀態為Stopped
時(即新建容器時),創建一個路徑為/run/runc/$ID/exec.fifo
的管道文件,它的作用我們會在后文中詳細描述。
值得注意的是start
方法的第二個參數對容器的狀態進行了判斷。事實上,命令runc create
和runc exec
的代碼的執行路徑是類似的,它倆共享了大部分的代碼。因此,這里我們需要對容器的狀態進行判斷,如果容器的狀態為Stopped
說明接下來應當進行容器的創建,否則應當在已有容器中exec一個新進程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
// runc/libcontainer/container_linux.go
func (c *linuxContainer) start(process *Process, isInit bool) error {
...
parent, err := c.newParentProcess(process, isInit)
...
if err := parent.start(); err != nil {
...
}
...
if isInit {
// 設置容器的狀態為created
c.state = &createdState{
c: c,
}
...
if c.config.Hooks != nil {
// 返回bundle以及用戶定義的annotations
bundle, annotations := utils.Annotations(c.config.Labels)
s := configs.HookState{
Version: c.config.Version,
ID: c.id,
Pid: parent.pid(),
Bundle: bundle,
Annotations: annotations,
}
for i, hook := range c.config.Hooks.Poststart {
// 容器啟動完成之后,運行PostStart hook
if err := hook.Run(s); err != nil {
...
}
}
}
} else {
// 如果容器不是第一次啟動,標記的狀態為running
c.state = &runningState{
c: c,
}
}
return nil
}
|
start
方法的工作也可以分為如下三部分:
- 調用
newParentProcess
創建parentProcess
對象 - 調用
parentProcess
的start
方法,它真正完成容器進程的創建以及初始化工作 - 如果
isInit
參數為true,則說明執行的命令為runc create
,更新容器狀態為Created
,並且如果定義了hooks(回調函數),則還需要執行PostStart
類型的hook函數。否則,如果執行的命令為runc exec
,則更新容器狀態為Running
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// runc/libcontainer/container_linux.go
func (c *linuxContainer) newParentProcess(p *Process, doInit bool) (parentProcess, error) {
parentPipe, childPipe, err := utils.NewSockPair("init")
...
// 創建子進程的運行模板
cmd, err := c.commandTemplate(p, childPipe)
...
if !doInit {
// 如果為exec命令,則調用c.newSetnsProcess
return c.newSetnsProcess(p, cmd, parentPipe, childPipe)
}
...
if err := c.includeExecFifo(cmd); err != nil {
...
}
return c.newInitProcess(p, cmd, parentPipe, childPipe)
}
|
newParentProcess
首先創建了一個名為init
的管道,它一方面會在創建容器時給容器的初始化進程傳輸容器的配置信息,另一方面它也會用於runC和容器進程之間的同步。
之后,它會調用commandTemplate
創建容器初始化進程的運行模板,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// runc/libcontainer/container_linux.go
func (c *linuxContainer) commandTemplate(p *Process, childPipe *os.File) (*exec.Cmd, error) {
cmd := exec.Command(c.initPath, c.initArgs[1:]...)
cmd.Args[0] = c.initArgs[0]
cmd.Stdin = p.Stdin
cmd.Stdout = p.Stdout
cmd.Stderr = p.Stderr
cmd.Dir = c.config.Rootfs
...
// 讓子進程獲取init pipe的信息
cmd.Env = append(cmd.Env,
fmt.Sprintf("_LIBCONTAINER_INITPIPE=%d", stdioFdCount+len(cmd.ExtraFiles)-1),
)
...
return cmd, nil
}
|
從上面的代碼中我們可以看出,環境變量也是runC進程和容器初始化進程之間進行交互的一種重要方式。上文中的init
管道的信息就是通過環境變量的方式從runC傳遞給容器初始化進程的。
到這里,我們腦海中可能會浮現出另一個問題:c.initPath
應該就是容器初始化進程的二進制文件的路徑,那么它是一個獨立於runC的二進制文件么?它又是放在哪的呢?事實上,c.initPath
在上文初始化Container對象時會被初始化為/proc/self/exe
,而c.initArgs
被設置為init
,因此我們創建子進程的過程其實相當於執行了runc init
這條命令。
如果執行的命令為runc create
,還需要將前文提到的exec.fifo
這個管道同樣以環境變量的形式傳遞到容器初始化進程中。最后,調用newInitProcess
將所有配置都填充至結構體initProcess
中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
|
// runc/libcontainer/process_linux.go
func (p *initProcess) start() error {
...
// 啟動子進程
err := p.cmd.Start()
...
// 將bootstrapData的數據寫入pipe
if _, err := io.Copy(p.parentPipe, p.bootstrapData); err != nil {
return newSystemErrorWithCause(err, "copying bootstrap data to pipe")
}
...
ierr := parseSync(p.parentPipe, func(sync *syncT) error {
switch sync.Type {
case procReady:
...
// call prestart hooks
// 調用prestart hooks
if !p.config.Config.Namespaces.Contains(configs.NEWNS) {
...
if p.config.Config.Hooks != nil {
...
for i, hook := range p.config.Config.Hooks.Prestart {
if err := hook.Run(s); err != nil {
return newSystemErrorWithCausef(err, "running prestart hook %d", i)
}
}
}
}
// Sync with child.
if err := writeSync(p.parentPipe, procRun); err != nil {
return newSystemErrorWithCause(err, "writing syncT 'run'")
}
...
case procHooks:
// Setup cgroup before prestart hook, so that the prestart hook could apply cgroup permissions.
// 首先設置cgroup
if err := p.manager.Set(p.config.Config); err != nil {
return newSystemErrorWithCause(err, "setting cgroup config for procHooks process")
}
...
if p.config.Config.Hooks != nil {
...
// 執行hooks
for i, hook := range p.config.Config.Hooks.Prestart {
if err := hook.Run(s); err != nil {
return newSystemErrorWithCausef(err, "running prestart hook %d", i)
}
}
}
// Sync with child.
if err := writeSync(p.parentPipe, procResume); err != nil {
return newSystemErrorWithCause(err, "writing syncT 'resume'")
}
...
}
...
})
...
// 關閉init pipe
if err := unix.Shutdown(int(p.parentPipe.Fd()), unix.SHUT_WR); err != nil {
return newSystemErrorWithCause(err, "shutting down init pipe")
}
...
}
|
initProcess
結構的start
方法真正完成了容器進程的創建,並通過init
管道協助其完成初始化工作。該方法首先調用p.cmd.Start()
創建一個獨立的進程,執行命令runc init
。接着通過init
管道將容器配置p.bootstrapData
寫入管道中。然后再調用parseSync()
函數,通過init
管道與容器初始化進程進行同步,待其初始化完成之后,執行PreStart Hook
等一些回調操作。最后,關閉init
管道,容器創建完成。
runC端在創建容器時所做的工作我們已經基本了解了,下面我們來看看runc init
,也就是容器初始化進程具體完成了哪些工作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
// runc/libcontainer/factory_linux.go
func (l *LinuxFactory) StartInitialization() (err error) {
var (
pipefd, fifofd int
consoleSocket *os.File
envInitPipe = os.Getenv("_LIBCONTAINER_INITPIPE")
envFifoFd = os.Getenv("_LIBCONTAINER_FIFOFD")
envConsole = os.Getenv("_LIBCONTAINER_CONSOLE")
)
// Get the INITPIPE.
pipefd, err = strconv.Atoi(envInitPipe)
if err != nil {
return fmt.Errorf("unable to convert _LIBCONTAINER_INITPIPE=%s to int: %s", envInitPipe, err)
}
var (
pipe = os.NewFile(uintptr(pipefd), "pipe")
// 判斷是`runc create`還是`runc exec`
it = initType(os.Getenv("_LIBCONTAINER_INITTYPE"))
)
defer pipe.Close()
// Only init processes have FIFOFD.
// 只有init進程有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 err != nil {
return err
}
// If Init succeeds, syscall.Exec will not return, hence none of the defers will be called.
return i.Init()
}
|
作為容器的初始化進程,必須先通過init
管道獲取配置才能進行下一步的工作。顯然,我們首先要做的就是從環境變量中獲取與runC進程進行交互的管道的信息,包括init
管道。對於runc create
還有管道exec.fifo
,即上方代碼中的fifofd
。緊接着,調用函數newContainerInit
,創建用於初始化的接口對象initer
,該函數的代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
// runc/libcontainer/init_linux.go
func newContainerInit(t initType, pipe *os.File, consoleSocket *os.File, fifoFd int) (initer, error) {
var config *initConfig
// 從管道中讀取config
if err := json.NewDecoder(pipe).Decode(&config); err != nil {
return nil, err
}
...
switch t {
case initSetns:
return &linuxSetnsInit{
pipe: pipe,
consoleSocket: consoleSocket,
config: config,
}, nil
case initStandard:
return &linuxStandardInit{
pipe: pipe,
consoleSocket: consoleSocket,
parentPid: unix.Getppid(),
config: config,
fifoFd: fifoFd,
}, nil
}
return nil, fmt.Errorf("unknown init type %q", t)
}
|
該函數的作用非常明顯,從init
管道中讀取容器配置,解析至initConfig
中。對於runc create
,創建linuxStandardInit
結構,將各種配置信息寫入其中。最后,調用該結構的Init
方法真正對容器進行初始化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
// runc/libcontainer/standard_init_linux.go
func (l *linuxStandardInit) Init() error {
...
// 配置network, 配置路由等等
...
// 准備rootfs
if err := prepareRootfs(l.pipe, l.config); err != nil {
return err
}
// 配置console, hostname, apparmor, process label, sysctl等等
...
// 告訴父進程我們已經准備好Exec了
if err := syncParentReady(l.pipe); err != nil {
return err
}
// 配置seccomp
...
// 設置正確的capability,用戶以及工作目錄
if err := finalizeNamespace(l.config); err != nil {
return err
}
...
// 確定用戶指定的容器進程在容器文件系統中的路徑
name, err := exec.LookPath(l.config.Args[0])
if err != nil {
return err
}
// 關閉管道,告訴runC進程,我們已經完成了初始化工作
l.pipe.Close()
// 在exec用戶進程之前等待exec.fifo管道在另一端被打開
// 我們通過/proc/self/fd/$fd打開它
fd, err := unix.Open(fmt.Sprintf("/proc/self/fd/%d", l.fifoFd), unix.O_WRONLY|unix.O_CLOEXEC, 0)
...
// 向exec.fifo管道寫數據,阻塞,直到用戶調用`runc start`,讀取管道中的數據
if _, err := unix.Write(fd, []byte("0")); err != nil {
return newSystemErrorWithCause(err, "write 0 exec fifo")
}
...
// 調用exec命令,執行用戶進程
if err := syscall.Exec(name, l.config.Args[0:], os.Environ()); err != nil {
return newSystemErrorWithCause(err, "exec user process")
}
return nil
}
|
Init
方法真正完成了對容器的初始化工作,它會對容器的網絡,路由,hostname等一系列屬性進行配置。這些工作一般都是直接通過系統調用設置完成的,因此我們就不再細述了。接下來我們將重點描述容器初始化進程和其父進程,也就是runC進程的同步過程。
我們都知道,每個容器都有自己的根文件系統,到目前為止我們依然還是宿主機文件系統的視角,那么文件系統根目錄的切換是在哪里進行的呢?答案是顯然的,prepareRootfs
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// runc/libcontainer/rootfs_linux.go
func prepareRootfs(pipe io.ReadWriter, iConfig *initConfig) (err error) {
...
// 配置mounts, dev
...
// 通知父進程運行pre-start hooks
if err := syncParentHooks(pipe); err != nil {
return err
}
...
if config.NoPivotRoot {
err = msMoveRoot(config.Rootfs)
} else if config.Namespaces.Contains(configs.NEWNS) {
err = pivotRoot(config.Rootfs)
} else {
err = chroot(config.Rootfs)
}
...
return nil
|
prepareRootfs
先對容器的Mounts和Dev等信息進行配置,之后再調用syncParentHooks
,通過init
管道向runC進程發送procHooks
信號。runC進程接收到procHooks
信號之后,執行容器的PreStart Hook
回調函數,再通過init
管道給容器初始化進程發送信號procResume
,通知其繼續執行。可見容器的PreStart Hook
是在根目錄尚未切換之前執行完成的。最終,調用chroot
函數,切換根目錄。至此,容器的文件系統切換完畢。
在文件系統准備完成之后,Init
方法還會對Console, hostname等屬性進行配置。當一切就緒之后,調用syncParentReady
通過init
管道通知runC進程,獲取響應之后,關閉init
管道,同步結束,准備開始執行用戶指定的容器進程。
不過在找到了用戶指定的容器程序在容器文件系統的執行路徑之后,初始化進程又打開了我們之前多次提到的exec.fifo
這個管道,並且往里面寫入了一個字節,之后才執行Exec
系統調用,切換到用戶程序。既然exec.fifo
是一個管道,那么我們在這一端寫入之后,就必須有消費者在另外一端進行讀取,否則寫進程就會一直處於阻塞狀態。
事實上,此處對exec.fifo
管道的寫阻塞正是runc create
和runc start
執行流的分界點。容器的創建工作,在容器初始化進程往exec.fifo
管道進行寫操作的那一刻,就全部結束了。
2.2 容器啟動
相對於容器的創建,容器的啟動就非常簡單了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// runc/start.go
container, err := getContainer(context)
...
status, err := container.Status()
...
switch status {
case libcontainer.Created:
return container.Exec()
case libcontainer.Stopped:
return errors.New("cannot start a container that has stopped")
case libcontainer.Running:
return errors.New("cannot start an already running container")
default:
return fmt.Errorf("cannot start a container in the %s state\n", status)
}
|
當我們執行runc start
命令時,我們首先會獲取相應容器的狀態。顯然,只有狀態為Created
的容器才是合法的,此時需要調用容器的Exec
方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
func (c *linuxContainer) exec() error {
// 獲取exec.fifo的路徑
path := filepath.Join(c.root, execFifoFilename)
fifoOpen := make(chan struct{})
select {
// 等待fifoOpen發來信號,或者子進程變為僵屍進程
case <-awaitProcessExit(c.initProcess.pid(), fifoOpen):
return errors.New("container process is already dead")
case result := <-awaitFifoOpen(path):
close(fifoOpen)
if result.err != nil {
return result.err
}
f := result.file
defer f.Close()
if err := readFromExecFifo(f); err != nil {
return err
}
return os.Remove(path)
}
}
|
Exec
方法僅僅只是對exec
的簡單封裝。而exec
方法的工作很簡單,找到exec.fifo
管道的路徑,打開它,並調用readFromExecFifo
從管道中將容器初始化進程從另一端寫入的字節讀出。一旦管道中的數據被讀出,容器內的初始化進程將不再被阻塞,緊接着將完成Exec
系統調用,容器初始化進程將被切換為用戶指定的程序。到此為止,一個容器真正啟動成功。
可是這一路分析下來,似乎並沒有對容器的namespace進行配置的操作?事實上,子進程runc init
的執行流在進入Go語言的運行時之前,會被包/runc/libcontainer/nsenter
劫持,先去執行一段C代碼。這段C代碼同樣會從init
管道中讀取容器的配置,主要是namespace的路徑,clone flag等等,並根據這些配置,調用setns
系統調用,將容器進程加入到合適的namespace中。之后再進入Go的運行時,完成上文所述的各種初始化操作。
3 總結
簡而言之,runC創建容器的過程如下圖所示:
1. runc create
命令加載文件config.json
中容器的配置並轉化為與libcontainer
兼容的模式
2. libcontainer
根據配置創建Container
以及ParentProcess
對象
3. Parentproces
創建runc init
子進程,中間會被/runc/libcontainer/nsenter
劫持,使runc init
子進程位於容器配置指定的各個namespace內
4. ParentProcess
用init
管道將容器配置信息傳輸給runc init
進程,runc init
再據此進行容器的初始化操作。初始化完成之后,再向另一個管道exec.fifo
進行寫操作,進入阻塞狀態
5. 執行runc start
命令,從管道exec.fifo
中讀取上一個步驟寫入的字節。runc init
進程不再阻塞,執行Exec
系統調用,切換至用戶指定的容器進程,容器真正創建並啟動完成
注:
- 文中源碼對應的runC版本為
v1.0.0-rc5
,commit:4fc53a81fb7c994640722ac585fa9ca548971871
- 文中引用的代碼因文章效果做了部分刪減,詳細的源碼注釋參見我的Github