Docker启动过程
本文最后更新于 2025年8月21日 下午
基于Unix socket的数据通信
Unix domain socket 又叫IPC socket ,用于实现同一主机上的进程间通信,socket原本是为网络通讯设计的,但后来在socket的框架上发展出一种IPC机制,就是UNIX domain socket。虽然网络socket也可用于同一台主机的进程间通信,但是UNIX domain socket用于IPC 更有效率,不需要经过网络协议栈,不需要打包拆包,计算校验和,维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程,这是因为IPC机制本质上是可靠的通讯,而网络协议是为不可靠的通讯设计的
UNIX domain socket是全双工的,API 接口语义丰富,相比其他IPC机制有明显的优越性,目前已成为使用最广泛的IPC 机制,比如X Window 服务器和GUI 程序之间就是通过UNIX domain socket 通讯的。
Docker 组件
docker 是一个Docker 客户端的完整实现,它是一个二进制文件,对用户可见的操作形式为docker命令,通过docker命令可以完成所有的Docker客户端与服务端的通信。
Docker客户端与服务端的交互过程是:docker组件向服务端发送请求后,服务端根据请求执行具体的动作,并将结果返回给docker,docker解析服务端的返回结果,并将结果通过命令行标准输出展示给客户。
docker proxy
docekr-proxy 主要用来做端口映射。当我们使用docker run 命令启动容器时,如果使用了-p 参数,docker-proxy 组件就会把容器内相应的端口映射到主机上来,底层是依赖于iptables实现的。
1 | |
docker-init
在执行docker run启动容器时可以添加—init 参数,此时Docker会使用docker-init作为1号进程,帮你管理容器内子进程,例如回收僵尸进程
1 | |
runc
runc 是一个标准的OCI 容器运行时的实现,它是一个命令行工具,可以直接用来创建和运行容器。
- 准备容器运行时文件
1 | |
- 准备config文件
使用runc spec命令根据文件系统生成对应的config.json 文件
在config.json 里指定了容器运行的args,env等等
1 | |
- 运行容器
1 | |
dockerd
dockerd 是docker服务端的后台常驻进程,用来接收客户端发送的请求,执行具体的处理任务,处理完成后将结果返回给客户端。
docker run/ps 是客户端,dockerd是服务器端。
containerd
contaienrd是用于处理客户端请求的真正进程,提供了生命周期管理等功能
- 镜像的股那里,容器运行前从镜像仓库拉取镜像到本地;
- 接收dockerd的请求,通过是藏的参数调用runc启动容器;
- 管理存储相关资源
- 管理容器相关资源
containerd 包含一个后台常驻进程,默认的socket路径为/run/containerd/containerd.sock, dockerd通过UNIX套接字向containerd发送请求,containerd接收到请求后负责执行相关的动作并把执行结果返回给dockerd。
containerd-shim
containerd-shim的主要作用是将containerd和真正的容器进程解耦,使用containerd-shim作为容器进程的父进程,从而实现重启containerd不影响已经启动的容器进程。
1 | |
- 当kubelet 通过CRI接口(gRPC)调用dockershim请求创建一个容器,CRI即容器运行时接口,kubelet 可以视为一个简单的CRI Client,而dockershim就是接收请求的Server,目前dockershim 的代码其实是内嵌在Kublet中的。所以接受调用的凑巧就是Kubelet进程
- dockershim 收到请求后,转换为Docker Daemon能听懂的请求,发送到Docker Daemon上请求创建一个容器
- Docker Daemon通知containerd创建一个容器
- containerd收到请求后,并不会直接去操作容器,而是创建一个叫做containerd-shim的进程让containerd-shim去操作容器,这是因为容器进程需要一个父进程来做状体收集,维持stdin等工作,而如果直接使用contianerd作为父进程,父进程的重启会导致所有子进程退出。
- containerd-shim 调用runc 工具来启动容器
- runc启动完容器后会直接退出,containerd-shim则会成为容器的父进程,负责收集容器进程的状态并上报给containerd,并在容器中pid为1 的进程退出后接管容器中的子进程进行处理。
- 为什么需要docker-shim
因为k8s定义了CRI,这样可以和docker,rkt等容器运行时解耦,但是dockerd没有不支持CRI,需要docker-shim进行一次转换。
- 为什么需要containers-shim
主要作用是将containerd和真正的容器进程解耦,使用containerd-shim作为容器进程的父进程,从而实现重启containerd 不影响已经启动的容器进程。
实现

Docker CLI
docker cli 即docker命令行工具,通过docker命令来完成docker容器的创建,它的主要工作
通过main函数入口 ,接收传入的参数,创建cmd对象,进行参数校验。最终返回cmd 进行执行
1
2
3
4
5
6
7
8func runDocker(dockerCli *command.DockerCli) error {
tcmd := newDockerCommand(dockerCli)
// 初始化一个dockerCommand对象
cmd,args,err := tcmd.HandleGlobalFlags()
...
return cmd.Execute()
}
// 初始化 Command 对象和 初始化dockerCli 客户端- 创建DockerCommand,调用HandleGlobalFlags方法 返回cmd,args,err 3个对象
- err用于校验 传入的命令是否符合语法树规范
- args 分析args,解析其中的别名,转换为实际需要执行的命令,设置到cmd中
- cmd最终需要执行的命令行对象
使用cobra.Command构建命令解析树,定义IMAGE 命令,最终执行由runRun方法执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19func NewRunCommand(dockerCli command.cli) *cobra.Command{
var opts runOptions
var copts *containerOptions
cmd:= &cobra.Command{
Use: "run [OPTIONS] IMAGE [COMMAND] [ARG...]",
Short: "Run a command in a new container"
Args: cli.RequiresMinArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
copts.Image = args[0]
if len(args) > 1{
copts.Args = args[1:]
}
return runRun(dockerCli, cmd.Flags(), &opts, copts)
},
}
...
return cmd
}在runRun方法中,解析本地的Deamon配置文件,加载配置信息,校验API版本,执行runContainer进入配置流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24func runRun(dockerCli command.Cli, flags *pflag.FlagSet , ropts *runOptions , copts *containerOptions) error{
proxyConfig := dockerCli.ConfigFile().ParseProxyConfig(dockerCli.Client().DeamonHost()
opts.ConvertKVStringsToMapWithNil(copts.env.GetAll()))
newEnv := []string{}
for k, v := range proxyConfig {
if v ==nil{
newEnv = append(newEnv, k)
}else {
newEnv = append(newEnv, fmt.Sprintf("%s=%s",k,*v))
}
}
copts.env = *opts.NewListOptsRef(&newEnv, nil)
contaienrConfig , err := parse(flags, copts, dockerCli.ServerInfo().OSType)
if err != nil {
reportError(dockerCli.Err(), "run", err.Error(), true)
return cli.StatusError{StatusCode: 125}
}
if err = validateAPIVersion(containerConfig, dockerCli.Client().ClientVersion()); err != nil{
reportError(dockerCli.Err(), "run", err.Error(), true)
return cli.StatusError{StatusCode : 125}
}
return runContainer(dockerCli, ropts, copts, containerConfig)
}
run container 分为两个过程 createContainer和 startContainer 。
这里就是通过配置信息向docker deamon 发起 配置Container和启动Container 的请求
1 | |
Docker Deamon(Dockerd)
dockerd是持续运行在服务器上的后台服务,它通过unix socket接收 客户端发送的命令请求
docker会以linux服务的形式被配置在服务器上
1 | |
daemon.json 则作为dockerd的配置目录
1 | |
dockerd 创建根目录命令,加载默认配置。
1 | |
之后执行 runDeamon
1 | |
daemonCli.start 执行
- 设置默认配置,检测配置
- 设置相关的目录与文件
- 创建监听server
- 初始化 插件,中间件,http服务
1 | |
initContainerD 初始化容器,启动一个containerd进程,最终返回对方的监听地址
1 | |
之后进行文件配置,并将来自客户端的rest请求路径 转换为对containerd的调用
1 | |
由dockerd 完成对containerd的调用,创建容器
1 | |
ContainerCreate 创建容器
1 | |
containerCreate 进行计时,配置校验信息,之后调用daemon.create 创建容器
1 | |
create 主要为容器创建提供最终的准备
- 组织 镜像信息
- 校验信息
- 创建进程
- 设置权限,可读层,以及命名空间,网络设置。
1 | |
postContainerExecStart
1 | |
ContainerStart
- 根据容器name,判断容器状态,
- 判断hostconfig信息
- 调用containerStart 进行容器的启动
1 | |
containerStart
- 判断容器状态
- 在容器启动前,为其准备挂载目录,网络配置,之后使用daemon.containerd.start调用Contianerd启动容器。
1 | |
Containerd-Shim
当dockerd 对containerd 发起创建的调用后,Containerd 会调用Shim的二进制程序,创建一个新的进程,该进程与Containerd脱离,Containerd不会等待Shim创建进程,之后再由Contaienr-Shim会设置新的进程组,重定向标准输入输出,更换工作目录。完成启动过程(调用runc创建实际的容器进程),并通过 Unix socket 连接回 Containerd,注册自己的 gRPC 服务端点,准备接收来自 Containerd 的任务指令。
runc
runc 是一个容器运行时实现,Container 最终通过调用这个程序完成的容器进程创建和配置过程
主要涉及到3个命令
runc create
/proc/self/exe init
runc start
create
1 | |
startContainer
- 生成一个libcontainer.Container 状态处于stopped/destroyed
- 然后把libcontainer.Container封装到type runner struct对象中
- 通过runner.run 来把容器中进程给跑起来
1 | |
createContainer
- 生成一个libcontainer.Factory, 用于配置容器
- 调用factory.Create()方法生成libcontainer.Container
1 | |
工厂结构
1 | |
runner struct
- 根据config.json 来设置将要在容器中执行的process
- runc create 调用container.Start(process) =⇒ func(c *linuxContainer) Start(process * Process)
- runc start 调用container.Run(process) =⇒ func(c *linuxContainer) Run(process *Process)
1 | |
Start
- linuxContainer.newParentProcess 组装将要执行的命令
- parent.start()会根据parent的类型来选择对应的start(), 自此之后,将进入/proc/self/exe init
- 容器的状态是created
1 | |
newParentProcess
通过匿名管道 parentPipe, childPipe , err := newPipe() 来在runc create和 runc init间进行通信
1 | |
init
在runc create 中会clone 出一个子进程,在子进程中调用/proc/self/exe init
initCommand
1 | |
factory.StartInitialization
- Init进程通过管道pipe来读取父进程传过来的信息
- 调用func newContainerInit(), 生成一个type linuxStandardInit struct对象
- 执行linuxStandardInit.Init()
1 | |
linuxStandardInit.Init
- 参数设置,状态检查
- exec.LookPath(l.config.Args[0])在当前系统的PATH中寻址cmd的绝对路径
- 以“只写” 方式打开fifo管道,形成阻塞,等待另一端有进程以“读“的方式打开管道
- 如果单独执行runc create命令 ,到这里就会发送阻塞,后面将是等待runc start以只读的方式打开FIFO管道,阻塞才会消除,本进程才会继续后面的流程
- 阻塞清除后,Init进程会根据config配置初始化seccomp ,并调用syscall.Exe执行cmd。系统调用syscall.Exec() ,执行用户真正希望执行的命令,用来覆盖掉PID为1 的Init进程。至此,在容器内部PID为1的进程才是用户希望一致在前台执行的进程
1 | |
start
从context中 获取id, 再获取指定的容器
启动处于Created 中的容器
1 | |
获取指定container
1 | |
container.Exec()
1 | |