runc 是 docker 捐赠给 OCI 的一个符合标准的 runtime 实现,目前 docker 引擎内部也是基于 runc 构建的。分析 runc 这个项目,加深对 OCI 的理解。

runC的前身实际上是Docker的libcontainer项目演化而来。runC实际上就是libcontainer配上了一个轻型的客户端。

从本质上来说,容器是提供一个与宿主机系统共享内核但与系统中的其它进程资源相隔离的执行环境。Docker通过调用libcontainer包对namespaces、cgroups、capabilities以及文件系统的管理和分配来“隔离”出一个上述执行环境。同样的,runC也是对libcontainer包进行调用,去除了Docker包含的诸如镜像、Volume等高级特性,以最朴素简洁的方式达到符合OCF标准的容器管理实现。

总体而言,从libcontainer项目转变为runC项目至今,其功能和特性并没有太多变化,具体有如下几点。

  1. 把原先的nsinit移除,放到外面,命令名称改为runC,同样使用cli.go实现,一目了然。

  2. 按照开放容器标准把原先所有信息混在一起的一个配置文件拆分成config.json和runtime.json两个。

  3. 增加了按照开放容器标准设定的容器运行前和停止后执行的hook脚本功能。

  4. 相比原先的nsinit时期的指令,增加了runc kill命令,用于发送一个SIG_KILL信号给指定容器ID的init进程。



1.先创建以容器的目录,例如mycontainer

mkdir mycontainer

2.准备一个rootfs的目录,供初始化一个容器所需要的的文件系统

cd mycontainer 
mdkir rootfs

docker export $(docker create busybox) | tar -C rootfs -xvf -
查看下载的文件系统
[root@docker2 mycontainer]# ll rootfs/
总用量 16
drwxr-xr-x 2 root  root  12288 5月   9 2019 bin
drwxr-xr-x 4 root  root     43 3月   6 21:41 dev
drwxr-xr-x 3 root  root    139 3月   6 21:41 etc
drwxr-xr-x 2 65534 65534     6 5月   9 2019 home
drwxr-xr-x 2 root  root      6 3月   6 21:41 proc
drwx------ 2 root  root      6 5月   9 2019 root
drwxr-xr-x 2 root  root      6 3月   6 21:41 sys
drwxrwxrwt 2 root  root      6 5月   9 2019 tmp
drwxr-xr-x 3 root  root     18 5月   9 2019 usr
drwxr-xr-x 4 root  root     30 5月   9 2019 var

3.RunC 是运行容器的运行时,它负责利用符合标准的文件等资源运行容器,但是它不包含 docker 那样的镜像管理功能。所以要用 runC 运行容器,我们先得准备好容器的文件系统。所谓的 OCI bundle 就是指容器的文件系统和一个 config.json 文件。有了容器的文件系统后我们可以通过 runc spec 命令来生成 config.json 文件。

[root@docker2 mycontainer]# runc spec
[root@docker2 mycontainer]# ll
总用量 4
-rw-r--r--  1 root root 2618 3月   6 21:41 config.json
drwxr-xr-x 12 root root  137 3月   6 21:41 rootfs

查看config.json的内容,内容较长就不展示了

more /run/runc/box/state.json  | python  -mjson.tool

4.使用runc运行一个容器 

[root@docker2 mycontainer]# runc run box(容器的名称)
/ # ls
bin   dev   etc   home  proc  root  sys   tmp   usr   var
/ # hostname
runc
/ # whoami
root
/ # ip addr
1: lo:  mtu 65536 qdisc noqueue qlen 1
    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
/ #

5.在其他的虚拟终端查看容器的运行信息

[root@docker2 ~]# runc list

ID          PID         STATUS      BUNDLE              CREATED                          OWNER

box         31158       running     /root/mycontainer   2020-03-06T13:44:24.565967666Z   root

目前,在我的机器上,runc 会把容器的运行信息保存在 /run/runc 目录下:

[root@docker2 ~]# tree  /run/runc/
/run/runc/
└── box
    └── state.json
1 directory, 1 file

state.json文件中包含的具体信息需要有:
1.版本信息:存放OCI标准的具体版本号。
2.容器ID:通常是一个哈希值,也可以是一个易读的字符串。在state.json文件中加入容器ID是为了便于之前提到的运行时hooks只需载入state.json就可以定位到容器,然后检测state.json,发现文件不见了就认为容器关停,再执行相应预定义的脚本操作。
3.PID:容器中运行的首个进程在宿主机上的进程号。
4.容器文件目录:存放容器rootfs及相应配置的目录。外部程序只需读取state.json就可以定位到宿主机上的容器文件目录。 标准的容器生命周期应该包含三个基本过程。
5.容器创建:创建包括文件系统、namespaces、cgroups、用户权限在内的各项内容。
6.容器进程的启动:运行容器进程,进程的可执行文件定义在的config.json中,args项。
7.容器暂停:容器实际上作为进程可以被外部程序关停(kill),然后容器标准规范应该包含对容器暂停信号的捕获,并做相应资源回收的处理,避免孤儿进程的出现

除了 run 命令之外,我们也能通过create、start、stop、kill 等命令对容器状态进行更精准的控制。继续实验,因为接下来要在后台模式运行容器,所以需要对 config.json 进行修改。改动有两处,把 terminal 的值改成 false,修改 args 命令行参数为 sleep 20:

        "process": {
                "terminal": false,
                "user": {
                        "uid": 0,
                        "gid": 0
                },
                "args": [
                        "sh"
                ],

接着,用 runc 子命令来控制容器的运行,实现各个容器状态的转换:

kill容器
[root@docker2 mycontainer]# runc kill box KILL
[root@docker2 mycontainer]# runc list
ID          PID         STATUS      BUNDLE      CREATED     OWNER

// 使用 create 创建出容器,此时容器并没有运行,只是准备好了所有的运行环境// 通过 list 命令可以查看此时容器的状态为 `created`➜  runc create mycontainerid➜  runc list
ID              PID         STATUS      BUNDLE                                    CREATED                          OWNER
mycontainerid   15871       created     /home/cizixs/Workspace/runc/mycontainer   2017-11-02T08:05:50.658423519Z   root// 运行容器,此时容器会在后台运行,状态变成了 `running`➜  runc start mycontainerid➜  runc list
ID              PID         STATUS      BUNDLE                                    CREATED                          OWNER
mycontainerid   15871       running     /home/cizixs/Workspace/runc/mycontainer   2017-11-02T08:05:50.658423519Z   root// 等待一段时间(20s)容器退出后,可以看到容器状态变成了 `stopped`➜  runc list
ID              PID         STATUS      BUNDLE                                    CREATED                          OWNER
mycontainerid   0           stopped     /home/cizixs/Workspace/runc/mycontainer   2017-11-02T08:05:50.658423519Z   root// 删除容器,容器的信息就不存在了➜  runc delete mycontainerid➜  runc list
ID          PID         STATUS      BUNDLE      CREATED     OWNER


使用 pause 命令暂停容器中的所有进程
我们先启动容器 mybusybox,然后用 pause 命令暂停它:

$ sudo runc pause mybusybox

使用runc创建一个docker容器案例_第1张图片

执行 pause 命令后,容器的状态由 running 变成了 paused。然后我们再通过 resume 命令恢复容器中进程的执行:

$ sudo runc resume mybusybox

使用runc创建一个docker容器案例_第2张图片

此时容器的状态又恢复到了 running。

使用 events 命令获取容器的资源使用情况
events 命令能够向我们报告容器事件及其资源占用的统计信息:

$ sudo runc events mybusybox

使用runc创建一个docker容器案例_第3张图片

rootless containers

前面我们运行的所有命令都是以 root 权限执行的。能不能以普通用户的权限运行容器呢?答案是可以的,并被称为 rootless。要想以 rootless 的方式运行容器,需要我们在生成容器的配置文件时就为 spec 命令指定 rootless 参数:

$ runc spec --rootless

并且在运行容器时通过 --root 参数指定一个存放容器状态的路径:

$ runc --root /tmp/runc run mybusybox

容器的热迁移操作

RunC 支持容器的热迁移操作,所谓热迁移就是将一个容器进行 checkpoint 操作,并获得一系列文件,使用这一系列文件可以在本机或者其他主机上进行容器的 restore 工作。这也是 checkpoint  和 restore 两个命令存在的原因。热迁移属于比较复杂的操作,目前 runC 使用了 CRIU 作为热迁移的工具。RunC 主要是调用 CRIU(Checkpoint and Restore in Userspace)来完成热迁移操作。CIRU 负责冻结进程,并将作为一系列文件存储在硬盘上。并负责使用这些文件还原这个被冻结的进程。

总结

RunC 作为标准化容器运行时的一个实现目前已经被 docker 内置为默认的容器运行时。相信随着 runC 自身的成熟和完善会有越来越多的大厂把 runC 作为默认的容器运行时。

`