在描述中,我提到了容器是一种沙盒技术。那什么是沙盒呢?

沙盒就像一个装着小猫的纸箱,把小猫“放”进去的技术。不同的小猫之间,因为有了纸箱的边界,而不至于互相干扰,纸箱 A 中吃饭的小猫并不会打扰到纸箱 B 中睡觉的小猫;而被装进纸箱的小猫,也可以方便地搬来搬去,你不用再去找它躲在哪里了!

我用了小猫在纸箱中的例子,其实想表达沙盒技术的两个能力,“隔离”和“迁移”。这两个能力,要如何用技术手段去实现呢?

我们先思考这样一个问题,一个加法程序是如何在计算机上运行的?

我们假设它需要输入值,处理值,输出值,才能完成了一次闭环。在输入值时,输入值会被加载到内存中待命。随后在处理逻辑中,操作系统又读到了加法的指令,操作系统告诉 CPU 进行加法操作。CPU 与内存协作,完成加法计算,又会使用寄存器存放输出值、内存堆栈保存执行的命令和变量。最后,再把计算结果,输出值,保存到指定位置。

这样的加法程序,它就是磁盘上的二进制文件,一旦被执行,它就需要内存中的数据、寄存器的值、堆栈中的指令等,这些条件的执行环境汇总起来,就是进程。对于进程来说,它的静态表现就是程序,平时就老老实实的待在磁盘上;一旦运行起来,它便有了动态表现,变成了计算机里的数据和状态的总和。

而容器的核心功能,“隔离”,就是通过约束和修改进程的动态表现,创造出一个“纸箱”,也就是“边界”。

在 Linux 系统中,Namespace、Cgroups 机制是用来制造约束的主要手段。

Namespace 是 Linux 在创建新进程的一个可选参数,Linux 通过系统调用函数 clone() 来创建进程:

int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL); 

在可选参数中添加 CLONE_NEWPID 后,我们就使用了一把 Namespace 机制,clone() 会返回新创建进程的 PID,此时 PID 对应的进程空间中,再运行新的程序,这个新进程的 PID 会是 1。而在宿主机真实的进程空间里,这个新创建的进程 PID 还是真实的数值,它的前面有着之前已经运行的进程。

除了刚刚用到的 CLONE_NEWPID -> PID Namespace,Linux 还提供了 Mount、Network 等 Namespace,用于约束和修改进程。比如 Mount Namespace,用于让被隔离的进程只看到当前 Namespace 里的挂载点信息;Network Namespace,用于让被隔离进程看到当前 Namespace 里的网络设备和配置。

以上就是 Linux 容器最基本的实现原理了。Docker 容器,实际上是在创建容器进程时,指定了进程所需要启用的一组 Namespace 参数。这样容器就只能“看”到当前 Namespace 所限定的资源、设备等。

我们理解 Namespace 的工作方式后,就可以知道,容器就是一种特殊的进程而已。