文件结构

run 命令主要是创建了一个用 namespace 隔离的容器进程,并初始化容器内容,挂载 proc 文件系统,运行用户指定的程序。
实现 run 命令主要有这几个文件:main.go、main_command.go、container_process.go、init.go、run.go。

main.go

main 文件是程序的入口,使用 github.com/urfave/cli 提供的命令行工具,定义了 mydocker 的几个基本命令,包括 runCommand 和 initCommand。
0aab715cb3c330197ed38a7cea14ff5.png

main_command.go

main_command.go 定义了 runCommand 和 initCommand 的具体实现。

对于 runCommand,定义了 runCommand 的 Flags,其作用类似于运行命令时使用 — 来指定参数。
然后解析参数:

  1. 判断参数是否包含 command
  2. 获取用户指定的 command
  3. 调用 Run function 去准备启动容器

780027c3644389cbbf8cb259eea11a7.png

对于 initCommand,主要是做了两件事情:

  1. 获取传递过来的 command 参数
  2. 执行容器初始化操作

其实是去调用 init.go 中的 RunContainerinitProcess 方法。

接着看一下 Run 函数做了什么

run.go

run.go 首先通过 container_process.go文件中的 NewParentProcess 方法获得一个配置了 namespace隔离参数的 command 对象,然后调用该对象的 Start() 方法。这里的 Start 方法是真正开始前面创建好的command 的调用,它首先会 clone 出来 namespace 隔离的进程,然后在子进程中,调用proc/self/exe ,也就是调用自己,发送 init 参数,调用我们写的工nit 方法, 去初始化容器的一些资源。
0cca0e266d7b02d81fdc79287d12ed1.png

container_process.go

container_process.go 文件中的 NewParentProcess 主要是配置 command 对象的参数。
facb576bf4f96c568c6e8547ee7bc62.png

  1. 这里的 /proc/self/exe 调用中,/proc/self/ 指的是当前运行进程自己的环境,exec 其实就是自己调用了自己,使用这种方式对创建出来的进程进行初始化
  2. 后面的 args 是参数,其中 init 是传递给本进程的第一个参数,在本例中,其实就是会去调用 initCornmand 去初始化进程的一些环境和资源
  3. 下面的 clone 参数就是去 fork 出来一个新进程,并且使用了 namespace 隔离新创建的进程和外部环境

init.go

这里的 init 函数是在容器内部执行的,也就是说,代码执行到这里后,容器所在的进程其实就已经创建出来了, 这是本容器执行的第一个进程。
使用 mount 先去挂载 proc 文件系统,以便后面通过 ps 等系统命令去查看当前进程资源的情况。
f6cba5cd4f0de84aaa2724d17f56b38.png
syscall.Exec 这个方法,其实最终调用了 Kernel 的 execve 系统函数。它的作用是执行当前 filename 对应 的程序。它会覆盖当前进程的镜像、数据和堆栈等信息,包括 PID,这些都会被将要运行的进程覆盖掉。
也就是说,调用这个方法,将用户指定的进程运行起来,把最初的 init 进程给替换掉,这样当进入到容器内部的时候,就会发现容器内的第一个程序就是我们指定的进程了。

流程图

84dacf7afa1321288745db48d4f6e2e.png