• 从零开始写 Docker(八)---实现 mydocker run -d 支持后台运行容器


    mydocker-run-d.png

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


    完整代码见:https://github.com/lixd/mydocker
    欢迎 Star

    推荐阅读以下文章对 docker 基本实现有一个大致认识:


    开发环境如下:

    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

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

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

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

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

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

    3. 实现

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

    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要后台运行就不能再去等待,创建容器之后,父进程直接退出即可。

    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 命令:

    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 看一下 建的容器进程是否存在:

    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 命令来查看运行中的容器。


    【从零开始写 Docker 系列】持续更新中,搜索公众号【探索云原生】订阅,阅读更多文章。



    完整代码见:https://github.com/lixd/mydocker
    欢迎 Star

    相关代码见 feat-volume 分支,测试脚本如下:

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

    # 克隆代码
    git clone -b feat-run-d https://github.com/lixd/mydocker.git
    cd mydocker
    # 拉取依赖并编译
    go mod tidy
    go build .
    # 测试 
    ./mydocker run top -d
    
  • 相关阅读:
    这 20 道 Redis 经典面试题你还不会,就别去面试了!
    dlv调试kubelet
    自动控制原理7.6---离散系统的动态性能分析
    互联网摸鱼日报(2024-06-26)
    OneOS 下的 GUI 框架测试
    [ZOOKEEPER]zookeeper基础知识笔记
    Netty编程面试题
    Linux内核分析:输入输出,字符与块设备 31-35
    当下最强的 AI art 生成模型 Stable Diffusion 最全面介绍
    【Reinforcement Learning】策略学习
  • 原文地址:https://www.cnblogs.com/KubeExplorer/p/18087120