1. UTS namespace
UTS(UNIX Time-sharing System)namespace提供了主机名和域名的隔离,这样每个Docker容器就可以拥有独立的主机名和域名了,在网络上可以被视为一个独立的节点,而非宿主机上的一个进程。Docker中,每个镜像基本都以只身所提供的服务名称来命名镜像的hostname,且不会对宿主机产生任何影响,其原理就是利用了UTS namespace。
2. IPC namespace
进程间通信(Inter-Process Communication,IPC)涉及的IPC资源包括常见的信号量、消息队列和共享内存。申请IPC资源就申请了一个全局唯一的32位ID,所以IPC namespace中实际上包含了系统IPC标识符以及实现POSIX消息队列的文件系统。在同一个IPC namespace下的进程彼此可见,不同IPC namespace下的进程则互相不可见。
3. PID namespace
PID namespace隔离非常实用,对进程PID重新标号,即两个不同 namespace的进程可以有相同的PID。每个 PID namespace都有自己的计数程序。内核为所有的 PID namespace维护了一个树状结构,最顶层的是系统初始时创建的,被称为 root namespace它创建的新 PID namespace被称为 child namespace(树的子节点),而原先的 PID namespace就是新创建的 namespace的parent namespace(树的父节点)通过这种方式,不同的 PID namespaces会成一个层级体系。所属的父节点可以看到子节点中的进程,并可以通过信号等方式对子节点中的进程产生影响。反过来,子节点却不能看到父节点 PID namespace中的任何内容,由此产生如下结论
- 每个 PID namespace中的第一个进程“PID1”,都会像传统 Linux中的init进程一样拥有特权,起特殊作用。
- 一个 namespace中的进程,不可能通过kill或 ptrace影响父节点或者兄弟节点中的进程,因为其他节点的PID在这 namespace个中没有任何意义。
- 如果你在新的 PID namespace中重新挂载/proc文件系统,会发现其下只显示同属一个pid namespace中的其他进程。
- 在 root namespace中可以看到所有的进程,并且递归包含所有子节点中的进程。
到这里,读者可能已经联想到一种在外部监控Docker中运行程序的方法了,就是监控Docker daemon所在的PID namespace下的所有进程及其子进程,再进行筛选即可。
4. mount namespace
mount namespace通过隔离文件系统挂载点对隔离文件系统提供支持,它是历史上第一个 Linux namespace,所以标识位比较特殊,就是 CLONENEWNS。隔离后,不同 mount namespace中的文件结构发生变化也互不影响。可以通过/proc/[pid]/mounts看到所有挂载在当前中的文件系统,还可以通过proc/[pid]/mountstats到 mount namespace中文件设备的统计信息,包括挂载文件的名字、文件系统类型、挂载位置等。
进程在创建 mount namespace时,会把当前的文件结构复制给新的 namespace。新 namespace中的所有 mount操作都只影响自身的文件系统,对外界不会产生任何影响。这种做法非常严格地实现了隔离,但对某些情况可能并不适用,比如父节点 namespace 中的进程挂载了一张CD-ROM, 这时子节点 namespace复制的目录结构是无法动挂载上这张CD-ROM的,因为这种操作会影响到父节点的文件系统
2006年引入的挂载传播( mount propagation)解决了这个问题,挂载传播定义了挂载对象( mount object)之间的关系,这样的关系包括共享关系和从属关系,系统用这些关系决定任何挂 载对象中的挂载事件如何传播到其他挂载对象。
- 共享关系( share relationship)。如果两个载对象具有共享关系,那么一个挂载对象中 的挂载事件会传播到另一个挂载对象,反之亦然。
- 从属关系( slave relationship)。如果两个挂载对象形成从属关系,那么一个挂载对象中的接收者。 的挂载事件会传播到另一个挂载对象,但是反之不行;在这种关系中,从属对象是事件的接收者。
一个挂载状态可能为以下一种:
- 共享挂载(share)
- 从属挂载(slave)
- 共享从属挂载(shared and slave)
- 私有挂载(private)
- 不可绑定挂载(unbendable)
传播事件的挂载对象称为共享挂载;接收传播事件的挂载对象称为从属挂载;同时兼有前述两者特征的挂载对象称为共享从属挂載;既不传播也不接收传播事件的挂载对象称为私有挂载;另一种特殊的挂载对象称为不可绑定的挂载,它们与私有挂载相似,但是不允许执行绑定挂载,即创建 mount namespace时这块文件对象不可被复制。通过下图可以更好地了解它们的状态变化。
mount各类挂载状态示意图:
5. network namespace
当我们了解完各类 namespace,兴致勃勃地构建出一个容器,并在容器中启动一个 Apache进程时,却出现了“80端口已被占用”的错误,原来主机上已经运行了一个 Apache进程,这时就要借助 Network namespace技术进行网络隔离。
network namespace主要提供了关于网络资源的隔离,包括网络设备、IPv4和IPv6协议栈、IP路由表、防火墙、/proc/net目录、/sys/ Class/net目录、套接字( socket)等。一个物理的网络设备最多存在于一个 network namespace中,可以通过创建 veth pair(虚拟网络设备对:有两端类似管道,如果数据从一端传入另一端也能接收到,反之亦然)在不同的 network namespace间创建通道,以达到通信目的。
一般情况下,物理网络设备都分配在最初的 root namespace(表示系统默认的 namespace)中。但是如果有多块物理网卡,也可以把其中一块或多块分配给新创建的 network namespace。需要注意的是,当新创建的 network namespace被释放时(所有内部的进程都终止并且 namespace文件没 有被挂载或打开),在这个 namespace中的物理网卡会返回到 root namespace,而非创建该进程的父 进程所在的 network namespace 。
当说到 network namespace时,指的未必是真正的网络隔离,而是把网络独立出来,给外部用户一种透明的感觉,仿佛在与一个独立网络实体进行通信。为了达到该目的,容器的经典做法就是创建一个 veth pair,一端放置在新的 namespace中,通常命名为eth0,一端放在原先的 namespace 中连接物理网络设备,再通过把多个设备接入网桥或者进行路由转发,来实现通信的目的。
也许读者会好奇,在建立起 veth pair之前,新旧 namespace该如何通信呢?答案是pipe(管道)。以 Docker daemon,启动容器的过程为例,假设容器内初始化的进程称为 init docker daemon在宿主机上负责创建这个 veth pair,把一端绑定到 docker网桥上,另一端接入新建的 network namespace进程中。这个过程执行期间,Docker daemon和init就通过pipe进行通信。具体来说,就是在 Docker daemon完成 veth pair的创建之前,init在管道的另一端循环等待,直到管道另一端传来 Dockerdaemon关于veth设备的信息,并关闭管道。init才结束等待的过程,并把它的“eth0”启动起来。
6. user namespaces
user namespace主要隔离了安全相关的标识符( identifier)和属性( attribute),包括用户ID用户组D、root目录、key(指密钥)以及特殊权限。通俗地讲,一个普通用户的进程通过 clone()创建的新进程在新 user namespace中可以拥有不同的用户和用户组。这意味着一个进程在容器外属于一个没有特权的普通用户,但是它创建的容器进程却属于拥有所有权限的超级用户,这个技术为容器提供了极大的自由。