从 docker 到 runC

从 docker 到 runC

从 Docker 到 runC:揭开容器运行时神秘面纱

大家好,我是你们的资深技术博主。今天我们来聊一个有趣又硬核的话题——从 Docker 到 runC。很多同学可能用过 Docker,知道它能“打包应用、随处运行”。但你有没有想过,Docker 是怎么在底层实现容器隔离的?它背后到底调用了什么?今天,我们就从 Docker 这个“指挥官”出发,深入到它手下的“执行者”——runC。## 什么是 Docker?——一个漂亮的“管家”我们先简单回顾一下 Docker。Docker 是一个容器化平台,它提供了从镜像构建、存储、拉取到运行的一整套工具链。简单说,Docker 像一个“管家”,你告诉它:“我要跑一个 Nginx”,它就会帮你拉镜像、创建容器、跑起来。但 Docker 本身并不直接创建容器。它依赖一个更底层的工具——runC。## 什么是 runC?——真正的“执行者”runC 是一个轻量级的命令行工具,它按照 OCI(Open Container Initiative)标准来创建和运行容器。OCI 是一套容器格式和运行时的行业标准,而 runC 是其参考实现。换句话说,Docker 是一个高层工具,它负责用户交互、镜像管理、网络等复杂功能;而 runC 是底层工具,它只做一件事:让容器跑起来。我们可以把 Docker 想象成一个高级饭店的经理,runC 则是后厨里专门负责炒菜的厨师。经理接单、协调、上菜,但真正把食材变成菜的,是厨师。## Docker 和 runC 的关系Docker 从 1.11 版本开始,就采用了 runC 作为其默认的容器运行时。具体流程如下:1. 用户输入docker run命令2. Docker 守护进程(dockerd)接收命令3. dockerd 调用 containerd(容器管理守护进程)4. containerd 启动 runC 实例5. runC 根据 OCI 规范创建并运行容器所以,最终容器进程是由 runC 创建的,Docker 只是“指挥官”。## 动手实践:安装并运行 runC理论说太多,不如动手跑一下。下面我们来看看如何安装 runC 并直接用它运行一个容器。### 安装 runC在 Ubuntu/Debian 上,可以直接用包管理器安装:bashsudo apt-get updatesudo apt-get install runc或者从 GitHub 下载预编译的二进制文件:bashwget https://github.com/opencontainers/runc/releases/download/v1.1.12/runc.amd64chmod +x runc.amd64sudo mv runc.amd64 /usr/local/bin/runc验证安装:bashrunc --version### 用 runC 运行一个容器安装好后,我们准备一个 OCI 标准的容器配置。OCI 标准规定,容器的配置以 JSON 格式存放在config.json文件中,根文件系统放在rootfs目录下。首先,创建一个工作目录:bashmkdir my-runc-containercd my-runc-container接下来,我们需要一个根文件系统。最简单的办法是从 Docker 镜像导出:bash# 拉取一个最小的 Alpine 镜像docker pull alpine:latest# 导出为 tar 包docker export $(docker create alpine:latest) -o alpine-rootfs.tar# 解压到 rootfs 目录mkdir rootfstar -xf alpine-rootfs.tar -C rootfs然后,使用 runC 自带的工具生成配置:bashrunc spec这会生成一个config.json文件,里面包含了容器的默认配置。现在我们把它修改一下,让容器启动后打印一句话。打开config.json,找到"process"部分,修改"args"为:json{ "process": { "args": ["/bin/sh", "-c", "echo Hello from runC! && sleep 10"], ... }}现在,我们运行容器:bashrunc run my-first-container你会看到输出:Hello from runC!容器运行 10 秒后自动退出。恭喜你,你已经直接使用 runC 运行了一个容器!### 代码示例:用 Go 调用 runC如果你是一个 Go 语言开发者,还可以直接通过编程方式调用 runC。下面是一个简单的示例:gopackage mainimport ( "fmt" "os/exec" "log")func main() { // 准备 runC 命令,指定容器 ID 和操作 cmd := exec.Command("runc", "run", "my-container") // 设置工作目录,该目录下必须包含 config.json 和 rootfs cmd.Dir = "/path/to/container/config" // 执行命令 err := cmd.Start() if err != nil { log.Fatalf("启动容器失败: %v", err) } fmt.Println("容器 my-container 已启动") // 等待容器退出 err = cmd.Wait() if err != nil { log.Fatalf("容器运行出错: %v", err) } fmt.Println("容器已退出")}这个示例展示了如何用 Go 代码调用 runC。在实际的 containerd 源码中,就是通过类似的机制来启动容器的。## runC 的底层原理:Linux 内核特性runC 之所以能“隔离”进程,是因为它利用了 Linux 内核的几大特性:-命名空间(Namespaces):让容器有独立的进程、网络、挂载等视图。-控制组(Cgroups):限制容器的 CPU、内存等资源使用。-联合文件系统(UnionFS):实现分层镜像,runC 本身不处理镜像,但依赖挂载点。-Seccomp、Capabilities:增强安全性。当我们运行runc run时,它会:1. 解析config.json2. 设置命名空间3. 配置 cgroups4. 挂载 rootfs5. 使用clone()系统调用创建新进程6. 最后通过exec()执行用户指定的命令## Docker 如何调用 runC?为了让大家更清楚,我们写一个简单的 Python 脚本,模拟 Docker 调用 runC 的过程:pythonimport subprocessimport osimport json# 模拟 Docker 创建容器配置def create_config(): config = { "ociVersion": "1.0.2-dev", "process": { "args": ["/bin/sh", "-c", "echo 'Hello from simulated Docker!'"], "env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"], "cwd": "/" }, "root": { "path": "rootfs" # 根文件系统路径 }, "linux": { "namespaces": [ {"type": "pid"}, {"type": "network"}, {"type": "ipc"}, {"type": "uts"}, {"type": "mount"} ] } } with open("config.json", "w") as f: json.dump(config, f, indent=2)# 模拟 Docker 调用 runCdef run_container(): container_id = "simulated-container" # 设置工作目录 work_dir = "/tmp/my-container" os.makedirs(work_dir, exist_ok=True) os.chdir(work_dir) # 创建配置 create_config() # 假设 rootfs 已经存在 if not os.path.exists("rootfs"): print("错误: rootfs 目录不存在!") return # 调用 runC print(f"模拟 Docker 调用 runC 运行容器: {container_id}") result = subprocess.run( ["runc", "run", container_id], capture_output=True, text=True ) print("标准输出:", result.stdout) print("标准错误:", result.stderr) if result.returncode == 0: print("容器运行成功!") else: print(f"容器运行失败,返回码: {result.returncode}")if __name__ == "__main__": run_container()这个脚本展示了 Docker 底层的工作流程:生成 OCI 配置,然后调用 runC 执行。当然,Docker 的实际实现要复杂得多,但核心思想是一样的。## 总结今天我们走了一遍从 Docker 到 runC 的探索之旅。总结一下:1.Docker 是高层工具,提供了完整的容器生态链,包括镜像管理、网络、存储等。2.runC 是底层工具,专注于按照 OCI 标准创建和运行容器,是容器真正的执行者。3.Docker 调用 containerd,containerd 再调用 runC,形成“Docker → containerd → runC”的三层架构。4. runC 利用 Linux 内核的命名空间、cgroups等特性实现隔离。5. 通过直接使用 runC,我们可以更深入地理解容器本质:容器本质上就是加了隔离和限制的普通进程。理解了 runC,你就能明白 Docker 并不是什么黑魔法。它只是一个漂亮的壳,里面包裹着 Linux 内核提供的能力。下次你用docker run时,可以想想背后那个默默工作的 runC——它才是那个真正让容器“跑起来”的英雄。希望这篇文章对你有帮助!如果你有任何问题,欢迎在评论区留言。我们下期再见!