mount 内核源码_runc源码分析

本文深入探讨runc在docker生态中的关键作用,通过分析runc的源码,特别是其namespace切换的过程,揭示了容器化的实现细节。文章首先介绍了runc在docker组件中的位置和功能,然后详细解析了runc create、runc init和runc start的执行流程,特别关注了namespace的创建和切换。此外,还提到了调试工具的选择和使用,以及runc如何遵循OCI规范来构建和运行容器。
摘要由CSDN通过智能技术生成
前言 2013年,一个叫dotCloud的公司将内部名为docker的容 器管理工具开源。它利用Linux namespace,cgroups技术,以docker image形式将资源打包和分发,运行在任一主流的Linux上(内核版本有一定要求)。docker的出现为Paas化解了许多痛点:敏捷性的部署启动;环境的隔离;资源的精细化控制。对开发来说:无需再纠结线上线下环境的差异,端口的分配,头疼的库依赖……对运维来说:借助于docker的之上编排系统(k8s),实现资源的合理调度,服务注册发现,弹性的扩容缩容…… docker催生出CaaS(Container as a Service),衍生出庞大的生态,丰富的想象,深刻影响了互联网技术的发展。也许连docker的作者都没有想到,docker真正做到了“让容器走向世界”。

笔者日常维护和开发的服务都是将docker image部署到kubernetes集群,时常因为缺少对背后的细节和原理认知,踩过坑也走过弯路,让笔者深刻认识到实现的细节有多么重要。后端程序员接触到docker,也许是通过docker这个CLI命令,或许是通过kubernetes,也或者是想尝试一个开源工具,而这个工具提供了用docker启动的简单入门方式……而所有这些,多数开发者并不是直接接触到runc。但笔者觉得,runc在docker及整个生态中像一把关键钥匙,通过了解runc的背后实现,有助于我们更好的理解其中许多的奥秘。

笔者能力有限,无法站在一个更高的维度来更好的抽象runc的架构设计和面面俱到的阐述runc的实现细节。本文从runc实现namespace过程来抛砖引玉,从自己的理解出发,希望能给读者一些帮助。

阅读本文之前,推荐读者读一下这篇文章:

https://mkdev.me/en/posts/the-tool-that-really-runs-your-containers-deep-dive-into-runc-and-oci-specifications

runc简介

c8a0991a3b74e90080b7f237b8b30cbd.png

(照片来自www.pexels.com David Dibert 摄影师)

docker生态常见用航海,集装箱,航舵等等logo。马尔科姆·麦克莱恩被称为集装箱之父,自从有了这个标准尺寸的箱子,世界货运的历史从此被改变。而容器也常常被称为集装箱,巴别塔的故事告诉我们:一旦有了通用的标准,连上帝都会感觉恐怖。

runc是docker组件中的low-level runtime(与之相对应的high-level是containerd),它本身是一个创建并运行容器的命令行。它是真正实现将我们的应用运行在"沙箱"中的关键。

docker组件及进程间关系如下图所示:

a9ea8896107b8280f307425624c302bf.png

如上图: 在一系列docker组件中,runc处于最后的处理部分,也是距离用户应用最近的一环。

runc经过多次自身调用,历经新旧namespace创建和切换,最后执行entrypoint替换掉自身进程,可谓是实现容器化的幕后英雄。

本文重点放在runc的多次自身调用实现切换namespaces上面。

本文分析使用的版本:

github repo: https://github.com/opencontainers/runc/

commit: 636f23dd21bda3a7d54c134f7612e9a25522e5f5

准备工作

bundle

docker处理流程到runc阶段,所需的容器名称(container id),根文件系统(rootfs),以及配置(config.json)已经准备就绪了。其中,rootfs和config.json称为bundle。(这一点跟macos的安装包的包内容很相似: mac的软件安装包内容放在一个Contents目录中,目录下有个info.plist的描述文件) docker的bundle有一个专门为此开放的规范称之为OCI: (Open Container Initiative Runtime Specification),后期之秀像gVisor等其他容器引擎都遵循OCI规范。简单来说,docker的bundle包含了完整的根目录及docker运行时的运行时库,文件,程序等等。其中的config.json配置描述docker运行起来要执行的应用程序路径,执行的终端信息,还包括了各个操作系统平台下的一些操作细节,比如: 在Linux环境下需要开启哪些命名空间,文件的挂载点和cgoups资源限制等等。

创建方法

$ mkdir /mycontainer; cd /mycontainer

# 创建一个根目录
$ mkdir rootfs

# 以busybox镜像为例,将busybox镜像中的文件导出到rootfs目录下
$ docker export $(docker create busybox) tar -C rootfs -xvf -

# 生成 config.json 的配置模板
$ runc spec

rootfs是一个可以chroot的完整文件根目录:

$ tree -L 1 rootfs
rootfs
├── bin
├── dev
├── etc
├── home
├── proc
├── root
├── sys
├── tmp
├── usr
└── var

如果将容器比作集装箱,那么config.json可以是这个"集装箱"的规格说明书。config.json长成这个样子:

{
    
  "process": {
    "terminal": false,
    "user": { "uid": 0, "gid": 0},
    "args": ["sh"],
    "cwd": "/",
    "capabilities": {},
    "rlimits": [],
    "noNewPrivileges": true
  },
  "hostname": "runc",
  "mounts": [],
  "linux": {
    "resources": {},
    "namespaces": [
     { "type": "pid"},
     { "type": "network"},
     { "type": "ipc"},
     { "type": "uts"},
     { "type": "mount"}
     ],
     "maskedPaths": [],
     "readonlyPaths": []
   }
}

这个配置中的process.args就是用户在Dockfile中指定的entrypoint,linux.namespaces来指示runc生成的容器要创建的新的namespace。

调试工具

在分析开源代码时,一款调试工具有助于我们更清楚地追踪整个执行流程,掌握主体脉络。笔者这里选用delve和gdb这两款调试工具,这两个调试工具在使用上比较类似。这两款调试工具各有优劣。

  • dlv专职调试go程序,比如像gr/grs等可以查看goroutine信息,但dlv有一定局限性,无法对子进程进行调试。

  • gdb是老牌的调试器,它对c/c++等系统native程序支持完善。在调试go程序时,对go变量打印不太友好。这里选择gdb是因为gdb具有强大的follow-fork-mode特性,可以追踪runc的多个子进程。

需要说明的是,笔者的机器上安装的是gdb 8.1.1,这个版本对clone(2)的子进程进行调试时有bug(gdb follow-fork-mode bug),笔者最终下载最新的10.1版本gdb,调试过程非常流畅。

编译及调试

delve

# 进入到源码目录,debug后可跟package。这里指main包。如果想要给程序传递参数,则可跟在“--”之后。
dlv debug . -- create mycontainer -b /home/noodles/work/mydocker/
# b/break 这里在process_linux.go 315行设置断点
(dlv) b /home/noodles/codes/opensrc/runc/libcontainer/process_linux.go:315

4567409bd64e24d5e29e5977cdec7e0b.png

gdb

go build -gcflags=all=
(gdb)

runc源码分析

接下来的分析将runc的执行过程分为三部分:

  • runc create

  • runc init

  • runc start

其中,runc createrunc init做准备工作,runc init 很少被直接使用,而是被 runc create 隐式地调用,runc init是本文要分析的重点。

另外,runc run其实是将三部走变成一步走。

runc create

runc create 调用路径

e04e950ff01d181ef4f12f9938c96ba2.png

main.go:117 行是runc的所有子命令。

从createCommand开始,经过第一轮处理,runc create 会以runc init回到initCommand。过程比较清晰:

  • 第1步中: setupSepc(context)加载bundle目录下的config.json配置。

  • 在第5,6步,构造一个工厂函数,这个LinuxFactory的可执行文件设置为/proc/self/exe,其实就是runc自己,它的参数是init,也即,在后续过程中的某个地方执行runc init,于是整个流程又从上图中标注的第8步: initCommand子命令开始,进入到runc的init阶段。

  • 在第7步之前,构造runner对象。在 create 阶段,runner.init为true。然后调用run()。

首先,从run()开始:

272 func(r *runner)run(config *specs.Process) (int, error) {
    
282   process, err := newProcess(*config, r.init)
319   switch r.action {
320    case CT_ACT_CREATE:
321      err = r.container.Start(process)
322    case CT_ACT_RESTORE:
323      err = r.container.Restore(process, r.criuOpts)
324    case CT_ACT_RUN:
325      err = r.container.Run(process)
326    default:
327      panic("Unknown action")
328    }
357 }
// newProcess: 将读取到的config.json的process配置构造成libcontainer.Process。
// 例如,这里的Args就是`sh`, Env是`PATH`环境变量。
106 func newProcess(p specs.Process, init bool) (*libcontainer.Process) {
107  lp := &libcontainer.Process{
108    Args: p.Args,
109    Env: p.Env,
111    User: fmt.Sprintf("%d:%d", p.User.UID, p.User.GID),
112    Cwd: p.Cwd,
113    Label: p.SelinuxLabel,
114    NoNewPrivileges: &p.NoNewPrivileges,
115    AppArmorProfile: p.ApparmorProfile,
116    Init: init, // true
118  }
143  return lp
144 }

执行libcontainer/container_linux.go中linuxContainer.Start():

252 func(c *linuxContainer) Start(process *Process) error {
    
258   if process.Init {
259     if err := c.createExecFifo(); err != nil {
260       re
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值