OCI 和 runc:容器标准化和 docker
ROSJacob
7年前
<h2>OCI 和容器标准</h2> <p>容器技术随着 docker 的出现炙手可热,所有的技术公司都积极拥抱容器,促进了 docker 容器的繁荣发展。 <strong>容器</strong> 一词虽然口口相传,但却没有统一的定义,这不仅是个技术概念的问题,也给整个社区带来一个阴影:容器技术的标准到底是什么?由谁来决定?</p> <p>很多人可能觉得 docker 已经成为了容器的事实标准,那我们以它作为标准问题就解决了。事情并没有那么简单,首先是否表示容器完全等同于 docker,不允许存在其他的容器运行时(比如 coreOS 推出的 rkt);其次容器上层抽象(容器集群调度,比如 kubernetes、mesos 等)和 docker 紧密耦合,docker 接口的变化将会导致它们无法使用。</p> <p>总的来说,如果容器以 docker 作为标准,那么 docker 接口的变化将导致社区中所有相关工具都要更新,不然就无法使用;如果没有标准,这将导致容器实现的碎片化,出现大量的冲突和冗余。这两种情况都是社区不愿意看到的事情,OCI(Open Container Initiative) 就是在这个背景下出现的,它的使命就是推动容器标准化,容器能运行在任何的硬件和系统上,相关的组件也不必绑定在任何的容器运行时上。</p> <p>官网上对 OCI 的说明如下:</p> <p>An open governance structure for the express purpose of creating open industry standards around container formats and runtime. – Open Containers Official Site</p> <p>OCI 由 docker、coreos 以及其他容器相关公司创建于 2015 年,目前主要有两个标准文档: <a href="/misc/goto?guid=4959009934197537878" rel="nofollow,noindex">容器运行时标准</a> (runtime spec)和 <a href="/misc/goto?guid=4959009934197537878" rel="nofollow,noindex">容器镜像标准</a> (image spec)。</p> <p>这两个协议通过 OCI runtime filesytem bundle 的标准格式连接在一起,OCI 镜像可以通过工具转换成 bundle,然后 OCI 容器引擎能够识别这个 bundle 来运行容器。</p> <p><img src="https://simg.open-open.com/show/f563abb212868b38bf6ed4704ab546e5.jpg"></p> <p>下面,我们来介绍这两个 OCI 标准。因为标准本身细节很多,而且还在不断维护和更新,如果不是容器的实现者,没有必须对每个细节都掌握。所以我以介绍概要为主,给大家有个主观的认知。</p> <h3>image spec</h3> <p>OCI 容器镜像主要包括几块内容:</p> <ul> <li><a href="/misc/goto?guid=4959755319444463292" rel="nofollow,noindex">文件系统</a> :以 layer 保存的文件系统,每个 layer 保存了和上层之间变化的部分,layer 应该保存哪些文件,怎么表示增加、修改和删除的文件等</li> <li><a href="/misc/goto?guid=4959755319532703459" rel="nofollow,noindex">config 文件</a> :保存了文件系统的层级信息(每个层级的 hash 值,以及历史信息),以及容器运行时需要的一些信息(比如环境变量、工作目录、命令参数、mount 列表),指定了镜像在某个特定平台和系统的配置。比较接近我们使用 docker inspect <image_id> 看到的内容</li> <li><a href="/misc/goto?guid=4959755319630855781" rel="nofollow,noindex">manifest 文件</a> :镜像的 config 文件索引,有哪些 layer,额外的 annotation 信息,manifest 文件中保存了很多和当前平台有关的信息</li> <li><a href="/misc/goto?guid=4959755319708644378" rel="nofollow,noindex">index 文件</a> :可选的文件,指向不同平台的 manifest 文件,这个文件能保证一个镜像可以跨平台使用,每个平台拥有不同的 manifest 文件,使用 index 作为索引</li> </ul> <h3>runtime spec</h3> <p>OCI 对容器 runtime 的标准主要是指定容器的运行状态,和 runtime 需要提供的命令。下图可以是容器状态转换图:</p> <p><img src="https://simg.open-open.com/show/7a1701c06a955a46f8ddb5ce32ea89bf.jpg"></p> <ul> <li>init 状态:这个是我自己添加的状态,并不在标准中,表示没有容器存在的初始状态</li> <li>creating:使用 create 命令创建容器,这个过程称为创建中</li> <li>created:容器创建出来,但是还没有运行,表示镜像和配置没有错误,容器能够运行在当前平台</li> <li>running:容器的运行状态,里面的进程处于 up 状态,正在执行用户设定的任务</li> <li>stopped:容器运行完成,或者运行出错,或者 stop 命令之后,容器处于暂停状态。这个状态,容器还有很多信息保存在平台中,并没有完全被删除</li> </ul> <h2>runc</h2> <p>runc 是 docker 捐赠给 OCI 的一个符合标准的 runtime 实现,目前 docker 引擎内部也是基于 runc 构建的。这部分我们就分析 runc 这个项目,加深对 OCI 的理解。</p> <h3>使用 runc 运行 busybox 容器</h3> <p>先来准备一个工作目录,下面所有的操作都是在这个目录下执行的,比如 mycontainer :</p> <pre> <code class="language-go"># mkdir mycontainer</code></pre> <p>接下来,准备容器镜像的文件系统,我们选择从 docker 镜像中提取:</p> <pre> <code class="language-go"># mkdir rootfs # docker export $(docker create busybox) | tar -C rootfs -xvf - # ls rootfs bin dev etc home proc root sys tmp usr var</code></pre> <p>有了 rootfs 之后,我们还要按照 OCI 标准有一个配置文件 config.json 说明如何运行容器,包括要运行的命令、权限、环境变量等等内容, runc 提供了一个命令可以自动帮我们生成:</p> <pre> <code class="language-go"># runc spec # ls config.json rootfs</code></pre> <p>这样就构成了一个 <a href="/misc/goto?guid=4959755319792883758" rel="nofollow,noindex">OCI runtime bundle</a> 的内容,这个 bundle 非常简单,就上面两个内容:config.json 文件和 rootfs 文件系统。 config.json 里面的内容很长,这里就不贴出来了,我们也不会对其进行修改,直接使用这个默认生成的文件。有了这些信息,runc 就能知道怎么怎么运行容器了,我们先来看看简单的方法 runc run (这个命令需要 root 权限),这个命令类似于 docker run ,它会创建并启动一个容器:</p> <pre> <code class="language-go">➜ runc run simplebusybox / # ls bin dev etc home proc root sys tmp usr var / # hostname runc / # whoami root / # pwd / / # 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 / # ps aux PID USER TIME COMMAND 1 root 0:00 sh 11 root 0:00 ps aux</code></pre> <p>最后一个参数是容器的名字,需要在主机上保证唯一性。运行之后直接进入到了容器的 sh 交互界面,和通过 docker run 看到的效果非常类似。但是这个容器并没有配置网络方面的内容,只是有一个默认的 lo 接口,因此无法和外部通信,但其他功能都正常。</p> <p>此时,另开一个终端,可以查看运行的容器信息:</p> <pre> <code class="language-go">➜ runc list ID PID STATUS BUNDLE CREATED OWNER simplebusybox 18073 running /home/cizixs/Workspace/runc/mycontainer 2017-11-02T06:54:52.023379345Z root</code></pre> <p>目前,在我的机器上,runc 会把容器的运行信息保存在 /run/runc 目录下:</p> <pre> <code class="language-go">➜ tree /run/runc/ /run/runc/ └── simplebusybox └── state.json 1 directory, 1 file</code></pre> <p>除了 run 命令之外,我们也能通过create、start、stop、kill 等命令对容器状态进行更精准的控制。继续实验,因为接下来要在后台模式运行容器,所以需要对 config.json 进行修改。改动有两处,把 terminal 的值改成 false ,修改 args 命令行参数为 sleep 20 :</p> <pre> <code class="language-go">"process": { "terminal": false, "user": { "uid": 0, "gid": 0 }, "args": [ "sleep", "20" ], ... }</code></pre> <p>接着,用 runc 子命令来控制容器的运行,实现各个容器状态的转换:</p> <pre> <code class="language-go">// 使用 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</code></pre> <p>把以上命令分开来虽然让事情变得复杂了,但是也有很多好处。可以类比 unix 系统 fork-exec 模式,在两者动作之间,用户可以做很多工作。比如把 create 和 start 分开,在创建出来容器之后,可以使用插件为容器配置多主机网络,或者准备存储设置等。</p> <h3>runc 代码实现</h3> <p>看完了 runc 命令演示,这部分来深入分析 runc 的代码实现。要想理解 runc 是怎么创建 linux 容器的,需要熟悉namespace 和cgroup、 go 语言 、常见的系统调用。</p> <p>分析的代码对应的 commit id 如下,这个代码是非常接近 v1.0.0 版本的:</p> <pre> <code class="language-go">➜ runc git:(master) git rev-parse HEAD 0232e38342a8d230c2745b67c17050b2be70c6bc</code></pre> <p>runc 的代码结构如下(略去了部分内容):</p> <pre> <code class="language-go">➜ runc git:(master) tree -L 1 -F --dirsfirst . ├── contrib/ ├── libcontainer/ ├── man/ ├── script/ ├── tests/ ├── vendor/ ├── checkpoint.go ├── create.go ├── delete.go ├── Dockerfile ├── events.go ├── exec.go ├── init.go ├── kill.go ├── LICENSE ├── list.go ├── main.go ├── Makefile ├── notify_socket.go ├── pause.go ├── PRINCIPLES.md ├── ps.go ├── README.md ├── restore.go ├── rlimit_linux.go ├── run.go ├── signalmap.go ├── signalmap_mipsx.go ├── signals.go ├── spec.go ├── start.go ├── state.go ├── tty.go ├── update.go ├── utils.go └── utils_linux.go</code></pre> <p>main.go 是入口文件,根目录下很多 .go 文件是对应的命令(比如 run.go 对应 runc run 命令的实现),其他是一些功能性文件。</p> <p>最核心的目录是 libcontainer ,它是启动容器进程的最终执行者, runc 可以理解为对 libcontainer 的封装,以符合 OCI 的方式读取配置和文件,调用 libcontainer 完成真正的工作。如果熟悉 docker 的话,可能会知道 libcontainer 本来是 docker 引擎的核心代码,用以取代之前 lxc driver。</p> <p>我们会追寻 runc run 命令的执行过程,看看代码的调用和实现。</p> <p>main.go 使用 github.com/urfave/cli 库进行命令行解析,主要的思路是先声明各种参数解析、命令执行函数,运行的时候 cli 会解析命令行传过来的参数,把它们变成定义好的变量,调用指定的命令来运行。</p> <pre> <code class="language-go">func main() { app := cli.NewApp() app.Name = "runc" ... app.Commands = []cli.Command{ checkpointCommand, createCommand, deleteCommand, eventsCommand, execCommand, initCommand, killCommand, listCommand, pauseCommand, psCommand, restoreCommand, resumeCommand, runCommand, specCommand, startCommand, stateCommand, updateCommand, } ... if err := app.Run(os.Args); err != nil { fatal(err) } }</code></pre> <p>从上面可以看到命令函数列表,也就是 runc 支持的所有命令,命令行会实现命令的转发,我们关心的 runCommand 定义在 run.go 文件,它的执行逻辑是:</p> <pre> <code class="language-go">Action: func(context *cli.Context) error { if err := checkArgs(context, 1, exactArgs); err != nil { return err } if err := revisePidFile(context); err != nil { return err } spec, err := setupSpec(context) status, err := startContainer(context, spec, CT_ACT_RUN, nil) if err == nil { os.Exit(status) } return err },</code></pre> <p>可以看到整个过程分为了四步:</p> <ol> <li>检查参数个数是否符合要求</li> <li>如果指定了 pid-file,把路径转换为绝对路径</li> <li>根据配置读取 config.json 文件中的内容,转换成 spec 结构对象</li> <li>然后根据配置启动容器</li> </ol> <p>其中 spec 的定义在 github.com/opencontainers/runtime-spec/specs-go/config.go#Spec ,其实就是对应了 OCI bundle 中 config.json 的字段,最重要的内容在 startContainer 函数中:</p> <p>utils_linux.go#startContainer</p> <pre> <code class="language-go">func startContainer(context *cli.Context, spec *specs.Spec, action CtAct, criuOpts *libcontainer.CriuOpts) (int, error) { id := context.Args().First() if id == "" { return -1, errEmptyID } ...... container, err := createContainer(context, id, spec) if err != nil { return -1, err } ...... 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) }</code></pre> <p>这个函数的内容也不多,主要分成两部分:</p> <ol> <li>调用 createContainer 创建出来容器,这个容器只是一个逻辑上的概念,保存了 namespace、cgroups、mounts、capabilities 等所有 Linux 容器需要的配置</li> <li>然后创建 runner 对象,调用 r.run 运行容器。这才是运行最终容器进程的地方,它会启动一个新进程,把进程放到配置的 namespaces 中,设置好 cgroups 参数以及其他内容</li> </ol> <p>我们先来看 utils_linux.go#createContainer :</p> <pre> <code class="language-go">func createContainer(context *cli.Context, id string, spec *specs.Spec) (libcontainer.Container, error) { config, err := specconv.CreateLibcontainerConfig(&specconv.CreateOpts{ CgroupName: id, UseSystemdCgroup: context.GlobalBool("systemd-cgroup"), NoPivotRoot: context.Bool("no-pivot"), NoNewKeyring: context.Bool("no-new-keyring"), Spec: spec, Rootless: isRootless(), }) .... factory, err := loadFactory(context) .... return factory.Create(id, config) }</code></pre> <p>它最终会返回一个 libcontainer.Container 对象,上面提到,这并不是一个运行的容器,而是逻辑上的容器概念,包含了 linux 上运行一个容器需要的所有配置信息。</p> <p>函数的内容分为两部分:</p> <ol> <li>创建 config 对象,这个配置对象的定义在 libcontainer/configs/config.go#Config ,包含了容器运行需要的所有参数。 specconv.CreateLibcontainerConfig 这一个函数就是把 spec 转换成 libcontainer 内部的 config 对象。这个 config 对象是平台无关的,从逻辑上定义了容器应该是什么样的配置</li> <li>通过 libcontainer 提供的 factory,创建满足 libcontainer.Container 接口的对象</li> </ol> <p>libcontainer.Container 是个接口,定义在 libcontainer/container_linux.go 文件中:</p> <pre> <code class="language-go">type Container interface { BaseContainer // 下面这些接口是平台相关的,也就是 linux 平台提供的特殊功能 // 使用 criu 把容器状态保存到磁盘 Checkpoint(criuOpts *CriuOpts) error // 利用 criu 从磁盘中重新 load 容器 Restore(process *Process, criuOpts *CriuOpts) error // 暂停容器的执行 Pause() error // 继续容器的执行 Resume() error // 返回一个 channel,可以从里面读取容器的 OOM 事件 NotifyOOM() (<-chan struct{}, error) // 返回一个 channel,可以从里面读取容器内存压力事件 NotifyMemoryPressure(level PressureLevel) (<-chan struct{}, error) }</code></pre> <p>里面包含了 Linux 平台特有的功能,基础容器接口为 BaseContainer ,定义在 libcontainer/container.go 文件中,它定义了容器通用的方法:</p> <pre> <code class="language-go">type BaseContainer interface { // 返回容器 ID ID() string // 返回容器运行状态 Status() (Status, error) // 返回容器详细状态信息 State() (*State, error) // 返回容器的配置 Config() configs.Config // 返回运行在容器里所有进程的 PID Processes() ([]int, error) // 返回容器的统计信息,主要是网络接口信息和 cgroup 中能收集的统计数据 Stats() (*Stats, error) // 设置容器的配置内容,可以动态调整容器 Set(config configs.Config) error // 在容器中启动一个进程 Start(process *Process) (err error) // 运行容器 Run(process *Process) (err error) // 销毁容器,就是删除容器 Destroy() error // 给容器的 init 进程发送信号 Signal(s os.Signal, all bool) error // 告诉容器在 init 结束后执行用户进程 Exec() error }</code></pre> <p>可以看到,上面是容器应该支持的命令,包含了查询状态和创建、销毁、运行等。</p> <p>这里使用 factory 模式是为了支持不同平台的容器,每个平台实现自己的 factory ,根据运行平台调用不同的实现就行。不过 runc 目前只支持 linux 平台,所以我们看 libcontainer/factory_linux.go 中的实现:</p> <pre> <code class="language-go">func New(root string, options ...func(*LinuxFactory) error) (Factory, error) { if root != "" { if err := os.MkdirAll(root, 0700); err != nil { return nil, newGenericError(err, SystemError) } } l := &LinuxFactory{ Root: root, InitPath: "/proc/self/exe", InitArgs: []string{os.Args[0], "init"}, Validator: validate.New(), CriuPath: "criu", } Cgroupfs(l) for _, opt := range options { if opt == nil { continue } if err := opt(l); err != nil { return nil, err } } return l, nil } func (l *LinuxFactory) Create(id string, config *configs.Config) (Container, error) { ...... containerRoot := filepath.Join(l.Root, id) if err := os.MkdirAll(containerRoot, 0711); err != nil { return nil, newGenericError(err, SystemError) } ...... c := &linuxContainer{ id: id, root: containerRoot, config: config, initPath: l.InitPath, initArgs: l.InitArgs, criuPath: l.CriuPath, newuidmapPath: l.NewuidmapPath, newgidmapPath: l.NewgidmapPath, cgroupManager: l.NewCgroupsManager(config.Cgroups, nil), } ...... c.state = &stoppedState{c: c} return c, nil }</code></pre> <p>New 创建了一个 linux 平台的 factory,从 LinuxFactory 的 fields 可以看到,它里面保存了和 linux 平台相关的信息。</p> <p>Create 返回的是 linuxContainer 对象,它是 libcontainer.Container 接口的实现。有了 libcontainer.Container 对象之后,回到 utils_linux.go#Runner 中看它是如何运行容器的:</p> <pre> <code class="language-go">func (r *runner) run(config *specs.Process) (int, error) { // 根据 OCI specs.Process 生成 libcontainer.Process 对象 // 如果出错,运行 destroy 清理产生的中间文件 process, err := newProcess(*config) if err != nil { r.destroy() return -1, err } ...... var ( detach = r.detach || (r.action == CT_ACT_CREATE) ) handler := newSignalHandler(r.enableSubreaper, r.notifySocket) // 根据是否进入到容器终端来配置 tty,标准输入、标准输出和标准错误输出 tty, err := setupIO(process, rootuid, rootgid, config.Terminal, detach, r.consoleSocket) defer tty.Close() switch r.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") } ...... status, err := handler.forward(process, tty, detach) if detach { return 0, nil } r.destroy() return status, err }</code></pre> <p>runner 是一层封装,主要工作是配置容器的 IO,根据命令去调用响应的方法。 newProcess(*config) 将 OCI spec 中的 process 对象转换成 libcontainer 中的 process,process 的定义在 libcontainer/process.go#Process ,包括进程的命令、参数、环境变量、用户、标准输入输出等。</p> <p>有了 process ,下一步就是运行这个进程 r.container.Run(process) , Run 会调用内部的 libcontainer/container_linux.go#start() 方法:</p> <pre> <code class="language-go">func (c *linuxContainer) start(process *Process, isInit bool) error { parent, err := c.newParentProcess(process, isInit) if err := parent.start(); err != nil { return newSystemErrorWithCause(err, "starting container process") } c.created = time.Now().UTC() if isInit { ...... for i, hook := range c.config.Hooks.Poststart { if err := hook.Run(s); err != nil { return newSystemErrorWithCausef(err, "running poststart hook %d", i) } } } return nil }</code></pre> <p>运行容器进程,在容器进程完全起来之前,需要利用父进程和容器进程进行通信,因此这里封装了一个 paerentProcess 的概念,</p> <pre> <code class="language-go">func (c *linuxContainer) newParentProcess(p *Process, doInit bool) (parentProcess, error) { parentPipe, childPipe, err := utils.NewSockPair("init") cmd, err := c.commandTemplate(p, childPipe) ...... return c.newInitProcess(p, cmd, parentPipe, childPipe) }</code></pre> <p>parentPipe 和 childPipe 就是父进程和创建出来的容器 init 进程通信的管道,这个管道用于在 init 容器进程启动之后做一些配置工作,非常重要,后面会看到它们的使用。</p> <p>最终创建的 parentProcess 是 libcontainer/process_linux.go#initProcess 对象,</p> <pre> <code class="language-go">type initProcess struct { cmd *exec.Cmd parentPipe *os.File childPipe *os.File config *initConfig manager cgroups.Manager intelRdtManager intelrdt.Manager container *linuxContainer fds []string process *Process bootstrapData io.Reader sharePidns bool }</code></pre> <ul> <li>cmd 是 init 程序,也就是说启动的容器子进程是 runc init ,后面我们会说明它的作用</li> <li>paerentPipe 和 childPipe 是父子进程通信的管道</li> <li>bootstrapDta 中保存了容器 init 初始化需要的数据</li> <li>process 会保存容器 init 进程,用于父进程获取容器进程信息和与之交互</li> </ul> <p>有了 parentProcess ,接下来它的 start() 方法会被调用:</p> <pre> <code class="language-go">func (p *initProcess) start() error { defer p.parentPipe.Close() err := p.cmd.Start() p.process.ops = p p.childPipe.Close() // 把容器 pid 加入到 cgroup 中 if err := p.manager.Apply(p.pid()); err != nil { return newSystemErrorWithCause(err, "applying cgroup configuration for process") } // 给容器进程发送初始化需要的数据 if _, err := io.Copy(p.parentPipe, p.bootstrapData); err != nil { return newSystemErrorWithCause(err, "copying bootstrap data to pipe") } // 等待容器进程完成 namespace 的配置 if err := p.execSetns(); err != nil { return newSystemErrorWithCause(err, "running exec setns process for init") } // 创建网络 interface if err := p.createNetworkInterfaces(); err != nil { return newSystemErrorWithCause(err, "creating network interfaces") } // 给容器进程发送进程配置信息 if err := p.sendConfig(); err != nil { return newSystemErrorWithCause(err, "sending config to init process") } // 和容器进程进行同步 // 容器 init 进程已经准备好环境,准备运行容器中的用户进程 // 所以这里会运行 prestart 的钩子函数 ierr := parseSync(p.parentPipe, func(sync *syncT) error { ...... return nil }) // Must be done after Shutdown so the child will exit and we can wait for it. if ierr != nil { p.wait() return ierr } return nil }</code></pre> <p>这里可以看到管道的用处:父进程把 bootstrapData 发送给子进程,子进程根据这些数据配置 namespace、cgroups,apparmor 等参数;等待子进程完成配置,进行同步。</p> <p>容器子进程会做哪些事情呢?用同样的方法,可以找到 runc init 程序运行的逻辑代码在 libcontainer/standard_init_linux.go#Init() ,它做的事情包括:</p> <ol> <li>配置 namespace</li> <li>配置网络和路由规则</li> <li>准备 rootfs</li> <li>配置 console</li> <li>配置 hostname</li> <li>配置 apparmor profile</li> <li>配置 sysctl 参数</li> <li>初始化 seccomp 配置</li> <li>配置 user namespace</li> </ol> <p>上面这些就是 linux 容器的大部分配置,完成这些之后,它就调用 Exec 执行用户程序:</p> <pre> <code class="language-go">if err := syscall.Exec(name, l.config.Args[0:], os.Environ()); err != nil { return newSystemErrorWithCause(err, "exec user process") }</code></pre> <p>NOTE:其实,init 在执行自身的逻辑之前,会被 libcontainer/nsenter 劫持,nsenter 是 C 语言编写的代码,目的是为容器配置 namespace,它会从 init pipe 中读取 namespace 的信息,调用setns 把当前进程加入到指定的 namespace 中。</p> <p>之后,它会调用 clone 创建一个新的进程,初始化完成之后,把子进程的进程号发送到管道中,nsenter 完成任务退出,子进程会返回,让 init 接管,对容器进行初始化。</p> <p>至此,容器的所有内容都 ok,而且容器里的用户进程也启动了。</p> <p><img src="https://simg.open-open.com/show/1d4299676a1a901181c51b950cc9c0e7.jpg"></p> <p>runc 的代码调用关系如上图所示,可以在新页面打开查看大图。主要逻辑分成三块:</p> <ul> <li>最上面的红色是命令行封装,这是根据 OCI 标准实现的接口,它能读取 OCI 标准的容器 bundle,并实现了 OCI 指定 run、start、create 等命令</li> <li>中间的紫色部分就是 libcontainer,它是 runc 的核心内容,是对 linux namespace、cgroups 技术的封装</li> <li>右下角的绿色部分是真正的创建容器子进程的部分</li> </ul> <h2>参考资料</h2> <ul> <li><a href="/misc/goto?guid=4959755319875587770" rel="nofollow,noindex">OCI FAQ page</a></li> <li><a href="/misc/goto?guid=4958970357236840690" rel="nofollow,noindex">OCI标准和runC原理解读</a></li> <li><a href="/misc/goto?guid=4958875851697412256" rel="nofollow,noindex">Docker背后的容器管理——Libcontainer深度解析</a></li> </ul> <p> </p> <p>来自:http://cizixs.com/2017/11/05/oci-and-runc</p> <p> </p>