深度探秘:Docker 容器创建与启动的完整生命周期
容器的启动看似只有一瞬间,但在底层,它经历了一场从用户空间命令行、Docker 守护进程、高级/低级容器运行时,再到 Linux 内核系统调用的精密协作。
以下是容器创建与启动的完整生命周期时序图:
[User] [Client] [dockerd] [containerd] [containerd-shim] [runc] [Kernel]
│ │ │ │ │ │ │
│──run─────>│ │ │ │ │ │
│ │──REST API──>│ │ │ │ │
│ │ │──gRPC(Pull)─>│ │ │ │
│ │ │<──Unpacked───│ │ │ │
│ │ │ │ │ │ │
│ │ │──gRPC(Task)─>│ │ │ │
│ │ │ │──fork/exec─────>│ │ │
│ │ │ │ │──run create─>│ │
│ │ │ │ │ │──Syscalls─>│ (Namespace/Cgroup)
│ │ │ │ │ │<─Created───│
│ │ │<──Alloc Net──│ │ │ │
│ │ │──(iptables)───────────────────────────────────┼───────────>│ (DNAT/veth)
│ │ │ │ │──run start──>│ │
│ │ │ │ │ │──execve───>│ (Launch App PID 1)
│ │ │ │ │ │──Exit─────>│
│ │ │ │ │<─Supervise───┼────────────│ (Shim monitors PID 1)
第一阶段:命令解析与接入 (Client & dockerd)
步骤 1:客户端命令行解析与验证 (Docker Client)
- 参数解析:Docker CLI 接收到用户输入的命令,将其拆解。例如:
-d(后台运行)、-p 8080:80(端口映射)、--name my-web(容器名)、nginx(镜像名)。 - API 版本协商:Client 检查本地配置,确定与 Daemon 通信的 API 版本。
- 构建请求:Client 将这些参数序列化为一个标准的 JSON 载荷,构建一个 HTTP POST 请求发送给 Docker Daemon。
- 请求地址示例:
POST /v1.41/containers/create?name=my-web - 通信媒介:默认通过本地 Unix 域套接字
/var/run/docker.sock。
- 请求地址示例:
步骤 2:守护进程受理与鉴权 (dockerd)
- API 路由接收:
dockerd监听套接字,接收到创建容器的 POST 请求。 - 配置校验:
- 检查容器名称
my-web是否冲突。 - 验证宿主机上是否存在
-p 8080:80所需的物理端口。 - 检查安全配置(如 Seccomp 过滤规则、AppArmor 配置文件、Linux Capabilities)。
- 检查容器名称
- 本地镜像检索:
dockerd检查本地 Graph Database(镜像元数据库)中是否存在nginx:latest的元数据和分层文件。
第二阶段:镜像准备与规格书生成 (dockerd & containerd)
步骤 3:镜像拉取与解压 (containerd)
如果本地不存在该镜像,将触发以下拉取流程:
- 下发拉取任务:
dockerd通过 gRPC 接口向containerd发起PullImage请求。 - Registry 交互:
containerd连接到指定的镜像仓库(如 Docker Hub),获取镜像的 Manifest(清单文件),解析出各个层(Layers)的 SHA256 哈希值。 - 并行下载与校验:
containerd并行下载各个只读层(tarball),下载完成后校验哈希值,确保镜像未被篡改。 - 解压存储:
containerd调用底层存储驱动(如overlay2),将下载的只读层解压并堆叠,建立起镜像的分层视图。
步骤 4:生成 OCI 规范规格书(Bundle)
- 元数据持久化:
dockerd在其本地目录(如/var/lib/docker/containers/<container-id>/)下创建该容器的元数据目录,写入配置信息。 - 转换 OCI 规格书:
dockerd将 Docker 特有的高级配置,翻译为符合 OCI(开放容器计划)标准的结构化配置文件:config.json。config.json包含的核心内容:- 要创建的 Namespaces 类型列表。
- 绑定的 Cgroups 路径和资源限制值(CPU shares, Memory limits)。
- 需要挂载的目录(Mounts)。
- 容器启动时执行的程序(
nginx -g "daemon off;")以及环境变量。
- 准备读写层(Container Layer):
dockerd指示存储驱动在只读镜像层之上,创建一个专属于该容器的可写层(Upperdir)和工作目录(Workdir),并将它们联合挂载,形成容器的rootfs(根文件系统)。
第三阶段:容器沙箱构建 (containerd-shim & runc)
步骤 5:启动容器垫片进程 (containerd-shim)
- 触发创建任务:
dockerd向containerd发起 gRPC 请求:CreateTask(创建一个容器运行任务)。 - Fork 垫片进程:
containerd为该容器 fork 并 exec 出一个containerd-shim进程。 - 传入 Bundle 路径:启动 shim 时,会把容器的
id、OCIconfig.json的路径以及rootfs路径作为参数传给它。
步骤 6:沙箱环境初始化 (runc create)
- 调用低级运行时:
containerd-shim立即调用命令行工具runc create <container-id>。 - 内核调用隔离(Namespaces):
runc读取config.json,调用 Linux 内核系统调用clone()(或unshare()),并传入隔离标志位:CLONE_NEWPID(进程隔离)CLONE_NEWNET(网络隔离)CLONE_NEWNS(文件系统挂载点隔离)CLONE_NEWIPC(进程间通信隔离)CLONE_NEWUTS(主机名隔离)
- 此时,一个处于暂停(Suspended)/ 挂起状态的初始化进程(通常称为
runc init)在这些新创建的命名空间中诞生。
- 资源限额配置(Cgroups):
runc在/sys/fs/cgroup/目录下创建该容器对应的子目录(例如/sys/fs/cgroup/memory/docker/<container-id>/)。- 将限制参数(如
memory.limit_in_bytes写入 512MB)写入相应的 Cgroup 文件。 - 将刚才创建的初始化进程的 PID 写入该 Cgroup 目录下的
cgroup.procs中,完成资源绑定。
- 根路径切换(Pivot Root):
- 在容器的 MNT Namespace 内,
runc执行系统调用pivot_root,将容器的当前根目录切换为准备好的rootfs目录,并将旧的宿主机根目录卸载(umount)。此时,容器进程再也无法看到宿主机的文件系统。
- 在容器的 MNT Namespace 内,
第四阶段:网络配置与程序激活
步骤 7:网络设备准备与绑定 (libnetwork)
当容器处于“Created”状态(沙箱已建好,但业务程序尚未运行)时,开始配置网络:
- IP 分配:
dockerd的网络引擎libnetwork从默认网桥docker0的网段中,自动分配一个未使用的 IP 地址。 - 创建虚拟网线(veth pair):在宿主机内核中创建一对虚拟网卡设备,一端留在宿主机,另一端塞入容器的 NET Namespace。
- 网卡重命名与配置:
- 进入容器 NET Namespace 的那一端网卡被重命名为
eth0,并配置上分配的 IP 地址和默认网关。 - 留在宿主机的那一端网卡被连接到
docker0网桥上。
- 进入容器 NET Namespace 的那一端网卡被重命名为
- 端口映射(DNAT):
dockerd向宿主机的iptables规则链中追加一条 DNAT 规则,将宿主机0.0.0.0:8080的流量,重定向到容器内eth0的172.17.0.x:80端口。
[ 宿主机外部流量 ] ──> [ 宿主机 8080 端口 ]
│ (iptables DNAT 转换)
▼
[ docker0 网桥 ]
│
(veth 虚拟网线)
│
▼
[ 容器内 eth0 网卡 (80端口) ]
步骤 8:业务进程激活 (runc start)
- 发出启动指令:
containerd-shim向runc发出runc start <container-id>指令。 - 进程替换(execve):
runc向处于挂起状态的容器初始化进程发送信号,将其唤醒。- 该进程执行系统调用
execve(),用 Nginx 的二进制程序(/usr/sbin/nginx)替换掉runc自身的初始化程序。 - 此时,Nginx 进程正式运行,并在容器的 PID Namespace 中成为 PID 1 进程。
- runc 退出:由于任务已完成,
runc工具进程立即退出,释放系统资源。
第五阶段:持续运行与状态监控 (Post-Start)
步骤 9:状态托管与监控 (containerd-shim)
- 接管孤儿进程:
runc退出后,Nginx 进程的父进程指针指向了containerd-shim。 - I/O 重定向:
containerd-shim继续保持对容器中 Nginx 进程的stdout和stderr管道的读取,将容器日志源源不断地写入到宿主机的日志文件中(如/var/lib/docker/containers/<id>/<id>-json.log)。
步骤 10:客户端响应
- 状态更新:
containerd检测到任务状态变更为Running,通过 gRPC 向上汇报给dockerd。 - 控制台输出:
dockerd更新本地数据库,并向 Docker Client 返回容器的唯一 ID(64位哈希值)。 - 终端呈现:用户的终端上打印出缩短后的 12 位容器 ID,命令执行完毕,控制权交还给用户。
核心系统调用速查表
| 系统调用 (Syscall) | 执行组件 | 作用与目的 |
|---|---|---|
clone() |
runc |
创建新进程,并通过传入 CLONE_NEW* 标志位创建各种隔离的 Namespace。 |
pivot_root() |
runc |
安全地改变调用进程的根文件系统目录,将其限制在容器镜像的 rootfs 中(比 chroot 更安全)。 |
mount() |
runc |
挂载容器内部的虚拟文件系统(如 /proc、/sys)和用户指定的数据卷(Volumes)。 |
execve() |
runc / 容器 Init |
用容器内的实际业务程序(如 Nginx 二进制程序)替换当前的 stub 引导程序,使其成为 PID 1。 |
setns() |
docker exec |
用于让新加入的进程(如执行 docker exec 时)进入已经存在的某个容器的 Namespace。 |
浙公网安备 33010602011771号