第3章 构造容器
3.1 构造实现run命令版本的容器
本节代码获取方式:
本章即将开始真正踏上构造自己的容器的道路。我们会基于当前的操作系统创建一个与宿主机隔离的容器环境,下面就开始吧。
3.1.1 Linux proc文件系统介绍
在开始之前,还是需要稍微补充一些基本知识。但如果你对这些基本知识已经很熟悉了,请直接略过。Linux 下的/proc 文件系统是由内核提供的,它其实不是一个真正的文件系统,只包含了系统运行时的信息(比如系统内存、mount设备信息、一些硬件配置等),它只存在于内存中,而不占用外存空间。它以文件系统的形式,为访问内核数据的操作提供接口。实际上,很多系统工具都是简单地去读取这个文件系统的某个文件内容,比如lsmod,其实就是cat/proc/modules。
当遍历这个目录的时候,会发现很多数字,这些都是为每个进程创建的空间,数字就是它们的PID。
下面介绍几个比较重要的部分。
/proc/N PID为N的进程信息
/proc/N/cmdline 进程启动命令
/proc/N/cwd 链接到进程当前工作目录
/proc/N/environ 进程环境变量列表
/proc/N/exe 链接到进程的执行命令文件
/proc/N/fd 包含进程相关的所有文件描述符
/proc/N/maps 与进程相关的内存映射信息
/proc/N/mem 指代进程持有的内存,不可读
/proc/N/root 链接到进程的根目录
/proc/N/stat 进程的状态
/proc/N/statm 进程使用的内存状态
/proc/N/status 进程状态信息,比stat/statm更具可读性
/proc/self/ 链接到当前正在运行的进程
3.1.2 实现run命令
首先实现一个简单版本的run命令,类似于docker run-ti [command]。然后在3.2节到3.4节中逐步添加network、mount filesystem等功能。为了方便了解Docker启动容器的原理,该简单版本的实现参考runC的实现。
目前的代码文件结构如下。
首先,来看一下入口main文件。
使用github.com/urfave/cli 提供的命令行工具,定义了mydocker 的几个基本命令,包括runCommand和initCommand,然后在app.Before内初始化一下logrus的日志配置。下面来看一下子命令的具体定义。
先来看一下run函数做了哪些事情。
那么,init函数里面做了些什么呢?
这里的MountFlag的意思如下。
MS_NOEXEC在本文件系统中不允许运行其他程序。
MS_NOSUID在本系统中运行程序的时候,不允许set-user-ID或set-group-ID。
MS_NODEV这个参数是自从Linux 2.4以来,所有mount的系统都会默认设定的参数。
本函数最后的syscall.Exec,是最为重要的一句黑魔法,正是这个系统调用实现了完成初始化动作并将用户进程运行起来的操作。你可能会说,这有什么神奇的,不就是运行一下程序嘛。下面来解释一下这句话的神奇之处。
首先,使用Docker创建起来一个容器之后,会发现容器内的第一个程序,也就是PID为1的那个进程,是指定的前台进程。那么,根据3.1.1小节所讲的过程发现,容器创建之后,执行的第一个进程并不是用户的进程,而是init初始化的进程。这时候,如果通过ps命令查看就会发现,容器内第一个进程变成了自己的init,这和预想的是不一样的。你可能会想,大不了把第一个进程给kill掉。但这里又有一个令人头疼的问题,PID 为1的进程是不能被kill掉的,如果该进程被kill掉,我们的容器也就退出了。那么,有什么办法呢?这里的execve系统调用就可以大显神威了。
syscall.Exec这个方法,其实最终调用了Kernel的int execve(const char*filename,char*const argv[],char*const envp[]);这个系统函数。它的作用是执行当前filename对应的程序。它会覆盖当前进程的镜像、数据和堆栈等信息,包括PID,这些都会被将要运行的进程覆盖掉。也就是说,调用这个方法,将用户指定的进程运行起来,把最初的init进程给替换掉,这样当进入到容器内部的时候,就会发现容器内的第一个程序就是我们指定的进程了。这其实也是目前Docker使用的容器引擎runC的实现方式之一。
流程图如图3.1所示。
图3.1
到这里,代码就差不多讲完了。下面来将其编译运行一下。
在容器运行ps-ef时,可以发现/bin/sh进程是容器内的第一个进程,PID 为1,而ps-ef是PID为1的父进程创建出来的。来对比一下Docker运行的容器的效果,如下。
是不是有些类似呢?这里的/bin/sh是一个会在前台一直运行的进程,那么可以试一下如果指定一个运行完就会退出的进程会是什么效果。
由于没有chroot,所以目前的系统文件系统是继承自父进程的。运行了一下ls命令,发现容器启动起来以后,打印出了当前目录的内容,然后便退出了,这个结果和Docker要求容器必须有一个一直在前台运行的进程的要求一致。