# 从零开始写 Docker(八)---实现 Mydocker Run -D 支持后台运行容器


![mydocker-run-d.png](https://img.lixueduan.com/docker/mydocker/cover/mydocker-run-d.png)

本文为从零开始写 Docker 系列第八篇，实现类似 docker run -d 的功能，使得容器能够后台运行。

<!--more-->

---

> 完整代码见：[https://github.com/lixd/mydocker](https://github.com/lixd/mydocker) 
> 欢迎 Star



推荐阅读以下文章对 docker 基本实现有一个大致认识：
* **核心原理**：[深入理解 Docker 核心原理：Namespace、Cgroups 和 Rootfs](https://www.lixueduan.com/posts/docker/03-container-core/)
* **基于 namespace 的视图隔离**：[探索 Linux Namespace：Docker 隔离的神奇背后](https://www.lixueduan.com/posts/docker/05-namespace/)
* **基于 cgroups 的资源限制**
  * [初探 Linux Cgroups：资源控制的奇妙世界](https://www.lixueduan.com/posts/docker/06-cgroups-1/)
  * [深入剖析 Linux Cgroups 子系统：资源精细管理](https://www.lixueduan.com/posts/docker/07-cgroups-2/)
  * [Docker 与 Linux Cgroups：资源隔离的魔法之旅](https://www.lixueduan.com/posts/docker/08-cgroups-3/)
* **基于 overlayfs 的文件系统**：[Docker 魔法解密：探索 UnionFS 与 OverlayFS](https://www.lixueduan.com/posts/docker/09-ufs-overlayfs/)
* **基于 veth pair、bridge、iptables 等等技术的 Docker 网络**：[揭秘 Docker 网络：手动实现 Docker 桥接网络](https://www.lixueduan.com/posts/docker/10-bridge-network/)

---

开发环境如下：
```bash
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 用户



## 1. 概述

经过前面的 7 篇文章，我们已经基本实现了一个简单的 docker 了。

不过与 Docker 创建的容器相比，我们还缺少以下功能

* 1）指定后台运行容器，也就是 detach 功能
* 2）通过 docker ps 查看目前处于运行中的容器
* 3）通过docker logs 查看容器的输出
* 4）通过 docker exec 进入到一个已经创建好了的容器中

后续几篇文章主要就是一一实现这些功能，本文首先实现 `mydocker run -d` 让容器后台运行。



## 2. 原理分析

在 Docker 早期版本，所有的容器 init 进程都是从 docker daemon 这个进程 fork 出来的，这也就会导致一个众所周知的问题，如果 docker daemon 挂掉，那么所有的容器都会宕掉，这给升级 docker daemon 带来很大的风险。

子进程的结束和父进程的运行是一个异步的过程，即父进程永远不知道子进程到底什么时候结束。如果创建子进程的父进程退出，那么这个子进程就成了没人管的孩子，俗称孤儿进程。为了避免孤儿进程退出时无法释放所占用的资源而僵死，进程号为 1 的 init 进程就会接受这些孤儿进程。

即：Docker 早期架构中，docker daemon挂掉后，所有容器作为子进程都会被 init 进程托管，实际上还是可以运行的，但是 docker daemon 挂了会导致他维护的一些资源也没了，所以容器实际上是不能正常运行的。



为了解决该问题后来，Docker 使用了 containerd， 负责管理容器的生命周期，包括创建、运行、停止等。同时 containerd 为每个进程都启动了一个 init 进程(图中的 `containerd-shim`)，`containerd-shim` 进程负责接收来自 `containerd` 的命令，启动容器中的进程，并监控它们的生命周期。

便可以实现即使 daemon 挂掉，容器依然健在的功能了，其结构如下图所示。

![docker-engine-arch.png](https://img.lixueduan.com/docker/mydocker/docker-engine-arch.png)



为了简单起见，我们就按照 Docker 早期架构实现吧。在我们的实现中：

* 当前运行命令的 mydocker 是主进程
* 容器是被当前 mydocker 进程 fork 出来的子进程。

> 这样看来，mydocker 可以看做是图中的 containerd，mydocker 中具体实现 Namespace 隔离，cgroups 资源限制的部分代码则可以看做是 runC或者 libcontainer。



具体实现就是，fork 出子进程后，mydocker 进程直接退出掉。而当 mydocker 进程退出后，容器进程就会被 init 进程接管，这时容器进程还是运行着的。

> 也算是实现了一个简易版本的后台运行。



## 3. 实现

首先，需要在 main-command.go 里面添加 -d flag，表示这个容器启动的时候后台在运行：

```go
var runCommand = cli.Command{
    Name: "run",
    Usage: `Create a container with namespace and cgroups limit
          mydocker run -it [command]`,
    Flags: []cli.Flag{
       cli.BoolFlag{
          Name:  "it", // 简单起见，这里把 -i 和 -t 参数合并成一个
          Usage: "enable tty",
       },
       cli.BoolFlag{
          Name:  "d",
          Usage: "detach container",
       },
        // 省略其他代码
    },
    /*
       这里是run命令执行的真正函数。
       1.判断参数是否包含command
       2.获取用户指定的command
       3.调用Run function去准备启动容器:
    */
    Action: func(context *cli.Context) error {
       if len(context.Args()) < 1 {
          return fmt.Errorf("missing container command")
       }

       var cmdArray []string
       for _, arg := range context.Args() {
          cmdArray = append(cmdArray, arg)
       }
       // tty和detach只能同时生效一个
       tty := context.Bool("it")
       detach := context.Bool("d")

       if tty && detach {
          return fmt.Errorf("it and d paramter can not both provided")
       }
       resConf := &subsystems.ResourceConfig{
          MemoryLimit: context.String("mem"),
          CpuSet:      context.String("cpuset"),
          CpuCfsQuota: context.Int("cpu"),
       }
       volume := context.String("v")
       Run(tty, cmdArray, resConf, volume)
       return nil
    },
}
```

然后调整 Run 方法,只有指定 tty 的时候才执行 parent.Wait。

parent.Wait() 主要是用于父进程等待子进程结束，这在交互式创建容器的步骤里面是没问题的，但是指定了 `-d`要后台运行就不能再去等待，创建容器之后，父进程直接退出即可。

```go
func Run(tty bool, comArray []string, res *subsystems.ResourceConfig, volume string) {
	parent, writePipe := container.NewParentProcess(tty, volume)
	if parent == nil {
		log.Errorf("New parent process error")
		return
	}
	if err := parent.Start(); err != nil {
		log.Errorf("Run parent.Start err:%v", err)
		return
	}
	// 创建cgroup manager, 并通过调用set和apply设置资源限制并使限制在容器上生效
	cgroupManager := cgroups.NewCgroupManager("mydocker-cgroup")
	defer cgroupManager.Destroy()
	_ = cgroupManager.Set(res)
	_ = cgroupManager.Apply(parent.Process.Pid, res)

	// 在子进程创建后才能通过pipe来发送参数
	sendInitCommand(comArray, writePipe)
	if tty { // 如果是tty，那么父进程等待，就是前台运行，否则就是跳过，实现后台运行
		_ = parent.Wait()
		container.DeleteWorkSpace("/root/", volume)
	}
}
```



## 4. 测试

运行一个 top 命令：

```shell
root@mydocker:~/feat-run-d/mydocker# go build .
root@mydocker:~/feat-run-d/mydocker# ./mydocker run -d top
{"level":"info","msg":"createTty false","time":"2024-01-24T16:58:16+08:00"}
{"level":"info","msg":"resConf:\u0026{ 0  }","time":"2024-01-24T16:58:16+08:00"}
{"level":"info","msg":"busybox:/root/busybox busybox.tar:/root/busybox.tar","time":"2024-01-24T16:58:16+08:00"}
{"level":"info","msg":"mount overlayfs: [/usr/bin/mount -t overlay overlay -o lowerdir=/root/busybox,upperdir=/root/upper,workdir=/root/work /root/merged]","time":"2024-01-24T16:58:16+08:00"}
{"level":"info","msg":"command all is top","time":"2024-01-24T16:58:16+08:00"}
```

可以看到，mydocker 命令直接退出了。

使用 top 作为容器内前台进程。然后在宿主机上执行 ps -ef 看一下 建的容器进程是否存在：

```shell
root@mydocker:~/feat-run-d/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。

这说因为`mydocker 主进程`退出了，但是 fork 出来的`容器子进程`依然存在，由于父进程消失，它就被 PID为 1 的 init 进程给托管了，由此就实现了 mydocker run -d 命令，即容器的后台运行。



## 4. 总结

本篇实现的 mydocker run -d 比较简单，就是启动完子进程（容器）后，直接退出父进程，让 init 进程去接管子进程。



不过现在比较大的问题是，虽然容器在后台运行了，但是已经找不到了，因此下一篇需要实现 `mydocker ps` 命令来查看运行中的容器。



---

> 完整代码见：[https://github.com/lixd/mydocker](https://github.com/lixd/mydocker)
> 欢迎 Star



相关代码见 `feat-run-d` 分支,测试脚本如下：

> 需要提前在 /root 目录准备好 busybox.tar 文件，具体见第四篇第二节。

```bash 
# 克隆代码
git clone -b feat-run-d https://github.com/lixd/mydocker.git
cd mydocker
# 拉取依赖并编译
go mod tidy
go build .
# 测试 
./mydocker run top -d
```



---

> 作者: [意琦行](https://github.com/lixd)  
> URL: https://www.lixueduan.com/posts/docker/mydocker/08-mydocker-run-d/  

