
说明:本文所使用的图片主要来自于Gao Feng@fujitsu的Lightweight virtual system mechanism。
虚幻世界- Namespace
"namespace"通常被翻译为「命名空间」,听起来好像比较抽象,其实重点是在这个"space"。它和描述进程的虚拟地址空间的address space一样,都是提供一种独占的视角(假象)。只是address space针对的是进程的地址,而namespace针对的是docker container,且维度更多,但它们的目的都是一样的:隔离,以尽可能地减少互相的干扰和影响。
【多维时空】
目前,Linux可以为docker提供的namespace种类包括文件系统mount、进程间通信、网络等,它们其实大都在内核2.6.24前就已经完成了。所以啊,docker所依赖的Linux的这些机制并不新鲜,但它通过一种新的需求,让这些沉淀多年的模块又重新焕发了生机。
文件系统 - Mount
那么多namespace,从哪个介绍起呢。按照先来后到的顺序,先来说说在内核2.4.19时代就出现的mount namespace吧(也许叫做"mount space"更好理解)。其实现原理大致是在前面介绍过的bind mount的基础上,又增加了mount propagation的特性。
在新的mount space中,进程对文件系统的mount/unmount操作都被“隐藏”了起来,对其他space不可见,也不会影响到其他的space。之后介绍了创建namespace的方法后,将通过一个小实验来印证这一点。
进程间通信 - IPC
接下来出场的是在2.6.19版本诞生的IPC namespace,用上这个space之后,进程间的通信(Inter Process Communication)就被限定在了同一个space内部,即一个container中的某个进程只能和同一container中的其他进程通信,container外部的进程对它来说好像不存在一样,因为它根本看不到。

主机域名 - UTS
由于多个container是共享OS内核的,因而像UTS里的os type和os release等信息是不可能更改的,但是每个container可以有自己独立的host name和domain name,以便于标识和区分(比如可以通过主机名来访问网络中的机器),这就是UTS namespace的作用。

进程编号 - PID
当你启动了多个container,然后在每个container内部用"ps"命令看一下,你会发现它们都有一个PID为1的进程。要知道,在一个Linux系统中,应该只有一个1号进程(以前是SysVinit,现在是systemd)。
想想Linux上物理地址也是唯一的,但不同的进程不是可以有相同的虚拟地址嘛,只要通过page table映射一下就可以了。同样地,这些container内部PID为1的进程,也会映射到host上其对应的真实的PID号上。从Linux host的视角,这都是些普通进程,但在container内部,它们却有了特殊的地位和意义。

那如何知道这种映射的对应关系呢?一个办法是在"/proc/<pid>/status"中找到"NSpid"后面有两列的item:

网络设备 - network
然后来到2.6.24时代,出现了针对网络的namespace。在每个network space内部,可以有独立的网络设备(虚拟的或者真实的)、IP地址、路由表和防火墙规则等。其中的应用所bind的端口也是per-namespace的,比如http默认使用的是80端口,使用network space后,同一host上的各个container内部就都可以运行各自的web server。

用户控制 - User
在host上全局的user id(uid)和group id(gid),到了user namespace内部就分别被映射成了各自的kuid和kgid。这样做有什么用呢?比如在container内部是root用户(kuid为0),可以「为所欲为」,但是在container外部,就是一个普通的unprivileged user(uid非0)。

看完以上这六种给container带来虚幻错觉的花招,不难发现,其实它们要达到的效果都是差不多的,就是在不同的space里面,可以有相同的name,不会重名,只能看见和使用与该namespace相关的资源。这种isolation,是一种软件层面的隔离,而不是物理上「泾渭分明」的隔离。
随着cgroup namespace(4.6版本引入)和time namespace的新鲜出炉,截止内核5.6版本,Linux支持的namespace已经多达8种。也许以后namespace的种类还会继续增加,以提供更多的隔离性选择(比如正在proposal阶段的syslog namespace)。在使用container的时候,也并不是所有namepsace都必须要有,可以根据需要选择其中的几个。
【自立门户】
同挂在sysfs中,依靠mkdir/rmdir/mv等基础的文件操作命令来新建/销毁/移动的cgroup不同,创建一个namespace需要使用clone()系统调用,namespace的种类由标志位"CLONE_NEWxxx"来决定。

clone()不是用于创建新的进程/线程的函数吗?没错,与fork()相比,clone()可以更细粒度地控制与父进程共享哪些资源,因此也就可以用于选择是否与父进程共用namespace。
就像fork()可以搭配"unshare"来实现和clone同样的效果,对于一个既有的进程(创建阶段没有脱离父进程的namespace),也还是有第二次“分家的”机会,就是通过"unshare"来创建新的namespace(借助这个小实验,可以更好地体会"unshare"和mount namespace的用法及其意义)。
【改弦更张】
那如果一个进程并不想开宗立派,只是想加入一个既有的namespace呢?在Linux中,进程的相关信息一般是记录在procfs里,namspace也不例外,在"/proc/<pid>/ns"中,通过symbol link的形式,给出了该进程所属的各个namespace的编号。

namespace也可被抽象为一个广义上的文件,因此这个编号其实就是文件的inode号。如果两个进程的某一ns指向的inode号相同,说明它们在这个ns对应的属性上,是属于同一namespace的。
当进程通过open()操作打开ns对应的文件,就获得了一个"fd",接下来再使用setns(),就可以加入这个namespace:
intsetns(intfd,intnstype);
参考:
LWN - Namespace file descriptors