Container研究 - 从头实现系列之三(容器运行时)

创建容器

前言

容器的核心原理,在 Container研究 - 从头实现系列之一(核心原理) 中已经有了介绍, 核心就是:

  • Namespace: 进行资源隔离; 让容器内外看到不一样的文件系统,进程ID空间等等
  • CGroup: 进行资源控制; 限制容器可以使用的CPU/MEM等
  • Cap: 进行权限管控; 限制容器可以调用的系统命令

容器的启动过程

在代码 run_linux.go 中,我们列出了一个典型的容器启动过程; 核心主要是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
ContainerRunCommand
    image.GetImage          # 根据需要运行的容器镜像,找到其镜像文件
    buildOverlaySandbox     # 创建容器运行所需要的`overlay`文件系统
                            # 核心主要是创建3层目录: 镜像解压后的目录,容器的工作目录,最终容器看到的`/`目录
    buildProcessCmd         # 根据镜像的Entrypoint/CMD等,以及容器启动命令,构建容器的启动命令行
                            # 这里我们就简化了,只考虑了`Entrypoint`
    reexec.Command          # 启动命令,代码中,我们通过syscall.SysProcAttr创建了新的`Namespace`, `IPC`空间, `PID`空间
                            # 这个函数比较重要,他的工作类似于`C`中的`fork`函数,他的作用就是创建一个新进程调用`container.Run`函数
                            # 所以新进程并不是直接使用`buildProcessCmd`中生成的`Cmd`来启动的
        [container.Run]         # 容器首进程的执行函数

            buildNetworkEnv     # 第一步: 创建一些网络相关的配置;这里我们没有创建新的网卡,分配IP,只是为新容器分配了`HostName`, 设置一些默认的地址, 设置`DNS`信息
            buildFileSystem     # 第二步: 创建一些挂载点,比如: /dev, /proc 等等;因为系统会用到这些挂载点。 因为当前子进程已经在新的`NS`中,所以这些挂载点,并不会影响原来的系统(即:外步看不到这些新的挂载点)
                                #      : 通过`chroot`将当前目录切换为文件系统的 `/`, 至此后续看到的文件系统根目录就不再是原来的目录
            buildUser           # 第三步: 如果容器指定了运行用户,则切换到对应的用户
            syscall.Chdir       # 第四步: 切换进程的工作目录到容器的`Workdir`
            syscall.Exec        # 第五步: 加载实际`Cmd`到内存,并跳转执行
    SetContainerCgroup      # 设置进程的`CGroup`,即CPU/MEM限制
                            # 根据`CGroup`的原理,这里其实就是创建一些文件即可。具体需要理解`CGroup`的具体原理了
    childcmd.Wait           # 等待子进程(容器主进程)结束

结合上面的工作流程来看代码,实际上还是比较简单的,我们在启动子进程时设置新的NS,然后在新启动的子进程中,进行一些"容器"的初始化操作,然后加载实际的容器命令行来执行即可。

其他需要注意的:

  • 如果要考虑网络,还是比较复杂的,这里只是简单处理,直接使用了Host网络
  • 代码中没有考虑权限Caf的一些设置,这个也是类似CGroup,只需要在指定位置写入一些文件即可

此外,代码中还有一项上文没有介绍的,即ContainerMeta,这个是用来保存容器的一些信息,父/子进程可以基于它来传递一些信息,此外一些其他管理命令也可以使用这个文件来对容器进行一些操作。

命令包装

基于 cobra.Command 可以很方便包装出来一个 container run 命令,所以这里不赘述了,可以查看代码 container.go

附录