美文网首页
docker 学习笔记1:什么是 runc

docker 学习笔记1:什么是 runc

作者: 董泽润 | 来源:发表于2019-12-06 10:26 被阅读0次

    TL;DR runc 是启动容器的最后一步,设置 cgroup, 隔离 namespaces 并启动程序。最早 docker 比较轻量,只有一个单一的 dockerd 进程,后来诞生了 OCI 标准, 用于统一容器运行时接口和镜像文件。而 runc 就是 docker 贡献给社区的一个运行时实现。

    docker 架构

    运行时标准 runtime spec

    当前 OCI 有两个标准:runtime-specimage-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 状态的位置

    docker run nginx

    相关文章

      网友评论

          本文标题:docker 学习笔记1:什么是 runc

          本文链接:https://www.haomeiwen.com/subject/wgklgctx.html