实现容器后台运行功能
本文介绍如何通过实现一个简易的容器管理工具,支持容器在后台运行的功能。我们将模拟 Docker 的 run -d 命令,使容器能够脱离前台运行。
开发环境
以下是测试所用的开发环境:
root@mydocker:~# lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 20.04.2 LTS
Release: 20.04
Codename: focal
root@mydocker:~# uname -r
5.4.0-74-generic
注意:需要使用 root 用户。
背景与需求
在之前的实现中,我们已经完成了一个基础的容器管理系统,但目前还缺少以下功能:
- 支持容器后台运行(detach 模式)。
- 查看当前运行中的容器状态。
- 获取容器的日志输出。
- 进入已运行的容器进行交互。
本文将专注于实现容器后台运行功能。
原理分析
早期版本的 Docker 中,所有容器的初始化进程都是从 Docker 守护进程 fork 出来的。这种方式虽然简单,但也存在一个问题:如果守护进程崩溃,所有容器会变成孤儿进程并被 init 进程接管。
为了解决这一问题,现代 Docker 使用了 containerd 来管理容器生命周期,并为每个容器启动一个独立的 containerd-shim 进程,确保即使 Docker 守护进程崩溃,容器仍能继续运行。
在我们的实现中,采用更简单的方案:
- 主程序(如 mydocker)作为父进程。
- 容器作为子进程,由父进程 fork 创建。
- 当父进程退出时,容器会被 init 进程接管,从而实现后台运行。
代码实现
首先,在命令解析部分添加对 -d 参数的支持:
var runCommand = cli.Command{
Name: "run",
Usage: "创建一个带命名空间和资源限制的容器",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "it",
Usage: "启用 tty 模式",
},
cli.BoolFlag{
Name: "d",
Usage: "以分离模式运行容器",
},
},
Action: func(context *cli.Context) error {
if len(context.Args()) < 1 {
return fmt.Errorf("缺少容器命令")
}
cmdArgs := context.Args()
isTTY := context.Bool("it")
isDetach := context.Bool("d")
if isTTY && isDetach {
return fmt.Errorf("无法同时指定 it 和 d 参数")
}
resConfig := &subsystems.ResourceConfig{
MemoryLimit: context.String("mem"),
CpuSet: context.String("cpuset"),
CpuCfsQuota: context.Int("cpu"),
}
volume := context.String("v")
Run(isTTY, isDetach, cmdArgs, resConfig, volume)
return nil
},
}
接下来调整容器启动逻辑,确保在分离模式下父进程不会等待子进程结束:
func Run(isTTY bool, isDetach bool, cmdArgs []string, resConfig *subsystems.ResourceConfig, volume string) {
parentProc, writePipe := container.NewParentProcess(isTTY, volume)
if parentProc == nil {
log.Errorf("创建父进程失败")
return
}
if err := parentProc.Start(); err != nil {
log.Errorf("启动父进程错误:%v", err)
return
}
cgroupMgr := cgroups.NewCgroupManager("custom-cgroup")
defer cgroupMgr.Destroy()
_ = cgroupMgr.Set(resConfig)
_ = cgroupMgr.Apply(parentProc.Process.Pid)
sendInitCommand(cmdArgs, writePipe)
if !isDetach { // 如果不是分离模式,则等待子进程结束
_ = parentProc.Wait()
container.DeleteWorkSpace("/root/", volume)
}
}
测试验证
编译并运行以下命令:
root@mydocker:~/mydocker# go build .
root@mydocker:~/mydocker# ./mydocker run -d top
可以看到,mydocker 命令直接返回,没有阻塞。
检查宿主机上的进程列表,确认容器是否正常运行:
root@mydocker:~/mydocker# ps -ef | grep -e PPID -e top
UID PID PPID C STIME TTY TIME CMD
root 166637 1 0 16:5 pts/8 00:00:00 top
可以看到,top 命令对应的进程正在运行,且其父进程为 1(即 init 进程),说明容器成功进入了后台运行模式。
总结
本文通过让父进程在启动子进程后立即退出的方式,实现了容器的后台运行功能。然而,当前实现还存在一个问题:后台运行的容器无法被追踪。因此,下一步将实现类似 ps 的功能,用于查看运行中的容器。