深度探秘: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)

  1. 参数解析:Docker CLI 接收到用户输入的命令,将其拆解。例如:-d(后台运行)、-p 8080:80(端口映射)、--name my-web(容器名)、nginx(镜像名)。
  2. API 版本协商:Client 检查本地配置,确定与 Daemon 通信的 API 版本。
  3. 构建请求:Client 将这些参数序列化为一个标准的 JSON 载荷,构建一个 HTTP POST 请求发送给 Docker Daemon。
    • 请求地址示例POST /v1.41/containers/create?name=my-web
    • 通信媒介:默认通过本地 Unix 域套接字 /var/run/docker.sock

步骤 2:守护进程受理与鉴权 (dockerd)

  1. API 路由接收dockerd 监听套接字,接收到创建容器的 POST 请求。
  2. 配置校验
    • 检查容器名称 my-web 是否冲突。
    • 验证宿主机上是否存在 -p 8080:80 所需的物理端口。
    • 检查安全配置(如 Seccomp 过滤规则、AppArmor 配置文件、Linux Capabilities)。
  3. 本地镜像检索dockerd 检查本地 Graph Database(镜像元数据库)中是否存在 nginx:latest 的元数据和分层文件。

第二阶段:镜像准备与规格书生成 (dockerd & containerd)

步骤 3:镜像拉取与解压 (containerd)

如果本地不存在该镜像,将触发以下拉取流程:

  1. 下发拉取任务dockerd 通过 gRPC 接口向 containerd 发起 PullImage 请求。
  2. Registry 交互containerd 连接到指定的镜像仓库(如 Docker Hub),获取镜像的 Manifest(清单文件),解析出各个层(Layers)的 SHA256 哈希值。
  3. 并行下载与校验containerd 并行下载各个只读层(tarball),下载完成后校验哈希值,确保镜像未被篡改。
  4. 解压存储containerd 调用底层存储驱动(如 overlay2),将下载的只读层解压并堆叠,建立起镜像的分层视图。

步骤 4:生成 OCI 规范规格书(Bundle)

  1. 元数据持久化dockerd 在其本地目录(如 /var/lib/docker/containers/<container-id>/)下创建该容器的元数据目录,写入配置信息。
  2. 转换 OCI 规格书dockerd 将 Docker 特有的高级配置,翻译为符合 OCI(开放容器计划)标准的结构化配置文件:config.json
    • config.json 包含的核心内容
      • 要创建的 Namespaces 类型列表。
      • 绑定的 Cgroups 路径和资源限制值(CPU shares, Memory limits)。
      • 需要挂载的目录(Mounts)。
      • 容器启动时执行的程序(nginx -g "daemon off;")以及环境变量。
  3. 准备读写层(Container Layer)dockerd 指示存储驱动在只读镜像层之上,创建一个专属于该容器的可写层(Upperdir)工作目录(Workdir),并将它们联合挂载,形成容器的 rootfs(根文件系统)。

第三阶段:容器沙箱构建 (containerd-shim & runc)

步骤 5:启动容器垫片进程 (containerd-shim)

  1. 触发创建任务dockerdcontainerd 发起 gRPC 请求:CreateTask(创建一个容器运行任务)。
  2. Fork 垫片进程containerd 为该容器 fork 并 exec 出一个 containerd-shim 进程。
  3. 传入 Bundle 路径:启动 shim 时,会把容器的 id、OCI config.json 的路径以及 rootfs 路径作为参数传给它。

步骤 6:沙箱环境初始化 (runc create)

  1. 调用低级运行时containerd-shim 立即调用命令行工具 runc create <container-id>
  2. 内核调用隔离(Namespaces)
    • runc 读取 config.json,调用 Linux 内核系统调用 clone()(或 unshare()),并传入隔离标志位:
      • CLONE_NEWPID(进程隔离)
      • CLONE_NEWNET(网络隔离)
      • CLONE_NEWNS(文件系统挂载点隔离)
      • CLONE_NEWIPC(进程间通信隔离)
      • CLONE_NEWUTS(主机名隔离)
    • 此时,一个处于暂停(Suspended)/ 挂起状态的初始化进程(通常称为 runc init)在这些新创建的命名空间中诞生。
  3. 资源限额配置(Cgroups)
    • runc/sys/fs/cgroup/ 目录下创建该容器对应的子目录(例如 /sys/fs/cgroup/memory/docker/<container-id>/)。
    • 将限制参数(如 memory.limit_in_bytes 写入 512MB)写入相应的 Cgroup 文件。
    • 将刚才创建的初始化进程的 PID 写入该 Cgroup 目录下的 cgroup.procs 中,完成资源绑定。
  4. 根路径切换(Pivot Root)
    • 在容器的 MNT Namespace 内,runc 执行系统调用 pivot_root,将容器的当前根目录切换为准备好的 rootfs 目录,并将旧的宿主机根目录卸载(umount)。此时,容器进程再也无法看到宿主机的文件系统。

第四阶段:网络配置与程序激活

步骤 7:网络设备准备与绑定 (libnetwork)

当容器处于“Created”状态(沙箱已建好,但业务程序尚未运行)时,开始配置网络:

  1. IP 分配dockerd 的网络引擎 libnetwork 从默认网桥 docker0 的网段中,自动分配一个未使用的 IP 地址。
  2. 创建虚拟网线(veth pair):在宿主机内核中创建一对虚拟网卡设备,一端留在宿主机,另一端塞入容器的 NET Namespace。
  3. 网卡重命名与配置
    • 进入容器 NET Namespace 的那一端网卡被重命名为 eth0,并配置上分配的 IP 地址和默认网关。
    • 留在宿主机的那一端网卡被连接到 docker0 网桥上。
  4. 端口映射(DNAT)dockerd 向宿主机的 iptables 规则链中追加一条 DNAT 规则,将宿主机 0.0.0.0:8080 的流量,重定向到容器内 eth0172.17.0.x:80 端口。
[ 宿主机外部流量 ] ──> [ 宿主机 8080 端口 ] 
                             │  (iptables DNAT 转换)
                             ▼
                    [ docker0 网桥 ]
                             │
                      (veth 虚拟网线)
                             │
                             ▼
                  [ 容器内 eth0 网卡 (80端口) ]

步骤 8:业务进程激活 (runc start)

  1. 发出启动指令containerd-shimrunc 发出 runc start <container-id> 指令。
  2. 进程替换(execve)
    • runc 向处于挂起状态的容器初始化进程发送信号,将其唤醒。
    • 该进程执行系统调用 execve(),用 Nginx 的二进制程序(/usr/sbin/nginx)替换掉 runc 自身的初始化程序。
    • 此时,Nginx 进程正式运行,并在容器的 PID Namespace 中成为 PID 1 进程
  3. runc 退出:由于任务已完成,runc 工具进程立即退出,释放系统资源。

第五阶段:持续运行与状态监控 (Post-Start)

步骤 9:状态托管与监控 (containerd-shim)

  1. 接管孤儿进程runc 退出后,Nginx 进程的父进程指针指向了 containerd-shim
  2. I/O 重定向containerd-shim 继续保持对容器中 Nginx 进程的 stdoutstderr 管道的读取,将容器日志源源不断地写入到宿主机的日志文件中(如 /var/lib/docker/containers/<id>/<id>-json.log)。

步骤 10:客户端响应

  1. 状态更新containerd 检测到任务状态变更为 Running,通过 gRPC 向上汇报给 dockerd
  2. 控制台输出dockerd 更新本地数据库,并向 Docker Client 返回容器的唯一 ID(64位哈希值)。
  3. 终端呈现:用户的终端上打印出缩短后的 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。
posted on 2026-05-25 10:31  LeeHang  阅读(3)  评论(0)    收藏  举报