TL;DR runc 是启动容器的最后一步,设置 cgroup, 隔离 namespaces 并启动程序。最早 docker 比较轻量,只有一个单一的 dockerd 进程,后来诞生了 OCI 标准, 用于统一容器运行时接口和镜像文件。而 runc 就是 docker 贡献给社区的一个运行时实现。
docker 架构运行时标准 runtime spec
当前 OCI 有两个标准:runtime-spec 和 image-spec,实际上就是为了兼容性及移植,而规定了镜像制作的标准,和如何启动解压过的 filesystem bundle ,而 runc 就是一种 runtime spec 的实现,其它的实现参见列表。其中有 c 实现的,号称比 go 的快一倍,貌似没啥用
1. bundle
不太好翻译,filesystem bundle
就是一个目录,提供 config.json 文件和 rootfs 文件系统,参照官网可以用如下命令生成
root@myali1:~# mkdir mycontainer; cd mycontainer
root@myali1:~/mycontainer# mkdir rootfs
root@myali1:~/mycontainer# docker export $(docker create busybox) | tar -C rootfs -xvf -
root@myali1:~/mycontainer# runc spec
root@myali1:~/mycontainer# ls -l
total 8
-rw-r--r-- 1 root root 2618 Dec 4 17:44 config.json
drwxr-xr-x 12 root root 4096 Dec 4 17:44 rootfs
2. config
config 描述了当前容器的配置: OCI 版本,启动程序路径与参数,挂载哪些文件系统,平台相关的比如 cgroup, namespaces, cpu quota 等等。具体可以参考 spec config.go
3. 状态
runc 启动的容器,都会把状态文件 state.json 存到一个地方,默认路径是 /run/runc/${container_id}
,通过 runc state
获取的状态来自于这个文件,里面内容非常多,暂时不看。
root@myali1:~/mycontainer# runc state mycontainerid4
{
"ociVersion": "1.0.1-dev",
"id": "mycontainerid4",
"pid": 13246,
"status": "running",
"bundle": "/root/mycontainer",
"rootfs": "/root/mycontainer/rootfs",
"created": "2019-12-04T07:06:58.828453173Z",
"owner": ""
}
Hello World
1. 前台启动
我们先看下刚才生成的 config.json,发现启动了 terminal
,运行命令是 sh
,然后在 bundle 目录下运行命令 runc run mycontainerid
root@myali1:~/mycontainer# runc run mycontainerid
/ # ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
/ # ls
bin dev etc home proc root sys tmp usr var
/ # df -h
Filesystem Size Used Available Use% Mounted on
/dev/vda1 39.2G 4.6G 32.8G 12% /
tmpfs 64.0M 0 64.0M 0% /dev
shm 64.0M 0 64.0M 0% /dev/shm
tmpfs 996.7M 0 996.7M 0% /sys/fs/cgroup
tmpfs 64.0M 0 64.0M 0% /proc/kcore
tmpfs 64.0M 0 64.0M 0% /proc/timer_list
tmpfs 64.0M 0 64.0M 0% /proc/sched_debug
tmpfs 996.7M 0 996.7M 0% /sys/firmware
tmpfs 996.7M 0 996.7M 0% /proc/scsi
可以看到容器内生成了 lo 网卡,文件系统也换成了 rootfs 的
ps axjf
root@myali1:~/mycontainer# lsns
NS TYPE NPROCS PID USER COMMAND
......
4026532273 mnt 1 13610 root sh
4026532274 uts 1 13610 root sh
4026532275 ipc 1 13610 root sh
4026532276 pid 1 13610 root sh
4026532278 net 1 13610 root sh
然后我们在宿主机查看下进程和 ns,可以看到 sh
的父进程是 runc
,并且开启了 uts
, ipc
, pid
, net
namespace,但是细心的发现并没有 user
ns
2. 后台启动
修改下 config.json, 将启动命令换成 sleep
,并且将 terminate
置为 false
root@myali1:~/mycontainer# cat config.json
......
"process": {
"terminal": false,
"args": [
"sleep", "500"
],
......
然后在 bundle 目录下先创建容器,不启动
root@myali1:~/mycontainer# runc create backgroundc
root@myali1:~/mycontainer# runc list
ID PID STATUS BUNDLE CREATED OWNER
backgroundc 13700 created /root/mycontainer 2019-12-04T11:09:10.091216191Z root
然后进入容器的状态目录 /run/runc/backgroundc
root@myali1:~# cd /run/runc/backgroundc
root@myali1:/run/runc/backgroundc# ls
exec.fifo state.json
注意这里多了一个 exec.fifo
文件,这是个很重要的用于同步的,稍后会讲
root@myali1:/run/runc/backgroundc# ps aux | grep runc
root 13700 0.0 0.5 494616 12188 ? Ssl Dec04 0:00 runc init
root 14724 0.0 0.0 16148 1060 pts/0 S+ 10:49 0:00 grep --color=auto runc
root@myali1:/run/runc/backgroundc# lsof -p 13700
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
runc:[2:I 13700 root cwd DIR 252,1 4096 1310876 /
runc:[2:I 13700 root rtd DIR 252,1 4096 1310876 /
runc:[2:I 13700 root txt REG 252,1 13869512 669613 /
runc:[2:I 13700 root 0u CHR 136,1 0t0 4 /dev/pts/1 (deleted)
runc:[2:I 13700 root 1u CHR 136,1 0t0 4 /dev/pts/1 (deleted)
runc:[2:I 13700 root 2u CHR 136,1 0t0 4 /dev/pts/1 (deleted)
runc:[2:I 13700 root 4u FIFO 0,24 0t0 1563 /run/runc/backgroundc/exec.fifo
runc:[2:I 13700 root 5w CHR 1,3 0t0 6 /null
runc:[2:I 13700 root 6u a_inode 0,13 0 9567 [eventpoll]
查看进程,发现当前存在一个 runc init
,并且打的文件描述符 4u 就是上面提到的 exec.fifo
root@myali1:/run/runc/backgroundc# lsns
NS TYPE NPROCS PID USER COMMAND
......
4026532273 mnt 1 13700 root runc init
4026532274 uts 1 13700 root runc init
4026532275 ipc 1 13700 root runc init
4026532276 pid 1 13700 root runc init
4026532278 net 1 13700 root runc init
再查看当前机器的 namespace, 发现己经创建了容器的 ns,只不过没有启动容器的 cmd. 最后我们启动这个容器
root@myali1:/run/runc/backgroundc# runc start backgroundc
root@myali1:/run/runc/backgroundc# ls
state.json
root@myali1:/run/runc/backgroundc# runc list
ID PID STATUS BUNDLE CREATED OWNER
backgroundc 13700 running /root/mycontainer 2019-12-04T11:09:10.091216191Z root
可以看到 exec.fifo
文件没了,再查看下进程
root@myali1:/run/runc/backgroundc# ps axjf | grep -A 4 -B 4 sleep
1 32068 32068 32068 ? -1 Ss 0 0:00 /lib/systemd/systemd --user
32068 32069 32068 32068 ? -1 S 0 0:00 \_ (sd-pam)
1 13700 13700 13700 ? -1 Ss 0 0:00 sleep 500
当前进程启动了,但是发现他的父进程是 1,被 init 托管了。如果是 docker 启动的话,那么父进程应该是 shim
实现原理
一句话总结:runc run
根据提供的 filesystem bundle 生成创建容器所需要各种配置,然后创建子进程 runc init
,同时父进程 runc run
设置子进程 runc init
的 cgroup, namespaces 等等。子进程 runc init
也要做一部份容器内的初始化,比如创建网络接口路由等等,最后 runc init
系统调用 exec
执行真正的 cmd,而 runc run
退出后,cmd 进程要么由操作系统 1 号进程接管,要么在 docker 环境中被 containerd-shim
接管。
如下图所示 docker 启动 nginx 的例子,另外也可以看到 --runtime-root
参数,其实就是保存 runc 状态的位置
网友评论