自己动手写Docker系列 -- 5.1实现容器的后台运行

991 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第7天,点击查看活动详情

简介

在前几篇中,我们已经构建了一个基础的镜像,本篇开始做一些进阶的功能,下面就是实现docker中的-d命令,让容器能够后台运行

源码说明

同时放到了Gitee和Github上,都可进行获取

本章节对应的版本标签是:5.1,防止后面代码过多,不好查看,可切换到标签版本进行查看

代码实现

在Docker早期版本,所有的容器init进程都是从dockerdaemon这个进程fork出来的。

这也就会导致一个众所周知的问题,如果dockerdaemon挂掉,那么所有的容器都会宕掉,这给升级docker daemon带来很大的风险。

后来,Docker使用了containerd,也就是现在的runC,便可以实现即使daemon挂掉,容器依然健在的功能了

我们并不想去实现一个daemon,因为这和容器的关联不是特别大,而且,查看Docker的运行引擎runC可以发现,runC也提供一种detach功能,可以保证在runC退出的情况下容器依然可以运行。

因此,我们将会使用detach功能去实现创建完成容器后,mydocker就会退出,但是容器依然继续运行的功能。

容器,在操作系统看来,其实就是一个进程。当前运行命令的mydocker 是主进程,容器是被当前mydocker进程fork出来的子进程。

子进程的结束和父进程的运行是一个异步的过程,即父进程永远不知道子进程到底什么时候结束。

如果创建子进程的父进程退出,那么这个子进程就成了没人管的孩子,俗称孤儿进程。

为了避免孤儿进程退出时无法释放所占用的资源而僵死,进程号为1的进程init就会接受这些孤儿进程。

这就是父进程退出而容器进程依然运行的原理。

虽然容器刚开始是由当前运行的mydocker进程创建的,但是当mydocker进程退出后,容器进程就会被进程号为1的init进程接管,这时容器进程还是运行着的,这样就实现了mydocker退出、容器不宕掉的功能。

原理大致就如上,对应的实现代码如下:

1.首先增加-d的命令选项,并传入run中

var RunCommand = cli.Command{
	Name:  "run",
	Usage: `Create a container with namespace and cgroups limit mydocker run -ti [command]`,
	Flags: []cli.Flag{
		......
		// 添加-d标签
		cli.BoolFlag{
			Name:  "d",
			Usage: "detach container",
		},
	},
	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 := context.Bool("ti")
		detach := context.Bool("d")
		resConfig := &subsystem.ResourceConfig{
			MemoryLimit: context.String("mem"),
			CpuShare:    context.String("cpuShare"),
			CpuSet:      context.String("cpuSet"),
		}
		volume := context.String("v")
		run.Run(tty, detach, cmdArray, resConfig, volume)
		return nil
	},
}

2.在run中,如果detach不为true则一直等待父进程退出,不反之,则父进程退出,docker进行成为孤儿进程,让init进程进行接管

func Run(tty, detach bool, cmdArray []string, config *subsystem.ResourceConfig, volume string) {
	.....

	log.Infof("parent process run")
	// 和书中稍微有点不一样,我们这里直接判断detach即可
	// 在我们使用docker的时候,-dit是可以共存的,所以我们优先判断detach
	if !detach {
		_ = parent.Wait()
		deleteWorkSpace(rootUrl, mntUrl, volume)
	}
	os.Exit(-1)
}

3.对文件空间初始化的改造

在完成上面的代码后,是可以运行了,但如果我们强杀进程后,其挂载点和可写层并没有进行清理

查看了书中提供的整个源码,应该是后面的rm命令实现的

而我们在实际使用docker的过程中,好像不删除容器,直接使用容器ID重启后,上次的文件是有进行保存的,还存在

所有我们对原来的文件空间初始化进行改造,让我们强杀容器进程后,能够再次启动进程,而不是报文件夹已存在的错误

如下所示,当挂载点和读写层不存在时,我们才进行创建

func createWriteLayer(rootUrl string) error {
	writeUrl := rootUrl + "writeLayer/"
	exist, err := pathExist(writeUrl)
	if err != nil && !os.IsNotExist(err) {
		return err
	}
	if !exist {
		if err := os.Mkdir(writeUrl, 0777); err != nil {
			return fmt.Errorf("create write layer failed: %v", err)
		}
	}
	return nil
}

func createMountPoint(rootUrl string, mntUrl string) error {
	// 创建mnt文件夹作为挂载点
	exist, err := pathExist(mntUrl)
	if err != nil && !os.IsNotExist(err) {
		return err
	}
	if !exist {
		if err := os.Mkdir(mntUrl, 0777); err != nil {
			return fmt.Errorf("mkdir faild: %v", err)
		}
	}
	// 把writeLayer和busybox目录mount到mnt目录下
	dirs := "dirs=" + rootUrl + "writeLayer:" + rootUrl + "busybox"
	cmd := exec.Command("mount", "-t", "aufs", "-o", dirs, "none", mntUrl)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		return fmt.Errorf("mmt dir err: %v", err)
	}
	return nil
}

运行测试

我们如书中的例子,运行一个top命令来进行测试

➜  dockerDemo git:(main) ✗ go build mydocker/main.go
➜  dockerDemo git:(main) ✗ ./main run -d top
{"level":"info","msg":"memory cgroup path: /sys/fs/cgroup/memory/mydocker-cgroup","time":"2022-03-21T05:58:44+08:00"}
{"level":"info","msg":"memory cgroup path: /sys/fs/cgroup/memory/mydocker-cgroup","time":"2022-03-21T05:58:44+08:00"}
{"level":"info","msg":"all command is : top","time":"2022-03-21T05:58:44+08:00"}
{"level":"info","msg":"parent process run","time":"2022-03-21T05:58:44+08:00"}
➜  dockerDemo git:(main) ✗ ps -ef |grep top
root       69016       1  0 05:58 pts/0    00:00:00 top
root       69044    5508  0 05:58 pts/0    00:00:00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn --exclude-dir=.idea --exclude-dir=.tox top
➜  dockerDemo git:(main) ✗

可以看到启动起来后就退出了,没有进入交互命令,查看top进程时,其父进程id是1