从 docker 到 runC 从 Docker 到 runC揭开容器运行时神秘面纱大家好我是你们的资深技术博主。今天我们来聊一个有趣又硬核的话题——从 Docker 到 runC。很多同学可能用过 Docker知道它能“打包应用、随处运行”。但你有没有想过Docker 是怎么在底层实现容器隔离的它背后到底调用了什么今天我们就从 Docker 这个“指挥官”出发深入到它手下的“执行者”——runC。## 什么是 Docker——一个漂亮的“管家”我们先简单回顾一下 Docker。Docker 是一个容器化平台它提供了从镜像构建、存储、拉取到运行的一整套工具链。简单说Docker 像一个“管家”你告诉它“我要跑一个 Nginx”它就会帮你拉镜像、创建容器、跑起来。但 Docker 本身并不直接创建容器。它依赖一个更底层的工具——runC。## 什么是 runC——真正的“执行者”runC 是一个轻量级的命令行工具它按照 OCIOpen 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, indent2)# 模拟 Docker 调用 runCdef run_container(): container_id simulated-container # 设置工作目录 work_dir /tmp/my-container os.makedirs(work_dir, exist_okTrue) 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_outputTrue, textTrue ) 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 调用 containerdcontainerd 再调用 runC形成“Docker → containerd → runC”的三层架构。4. runC 利用 Linux 内核的命名空间、cgroups等特性实现隔离。5. 通过直接使用 runC我们可以更深入地理解容器本质容器本质上就是加了隔离和限制的普通进程。理解了 runC你就能明白 Docker 并不是什么黑魔法。它只是一个漂亮的壳里面包裹着 Linux 内核提供的能力。下次你用docker run时可以想想背后那个默默工作的 runC——它才是那个真正让容器“跑起来”的英雄。希望这篇文章对你有帮助如果你有任何问题欢迎在评论区留言。我们下期再见