理论:https://blog.csdn.net/qq_37133717/article/details/86359947
当谈论docker时,常常会聊到docker的实现方式。很多开发者都知道,docker容器本质上是宿主机的进程,Docker通过namespace实现了资源隔离,通过cgroups实现了资源限制,通过写时复制机制(copy-on-write)实现了高效的文件操作。当进一步深入namespace和cgroups等技术细节时,大部分开发者都会感到茫然无措。尤其是接下来解释libcontainer的工作原理时,我们会接触大量容器核心知识。所以在这里,希望先带领大家走进linux内核,了解namespa和cgroups的技术细节。
namespace资源隔离
linux内核提拱了6种namespace隔离的系统调用,如下图所示,但是真正的容器还需要处理许多其他工作。
namespace | 系统调用参数 | 隔离内容 |
---|---|---|
UTS | CLONE_NEWUTS | 主机名或域名 |
IPC | CLONE_NEWIPC | 信号量、消息队列和共享内存 |
PID | CLONE_NEWPID | 进程编号 |
Network | CLONE_NEWNET | 网络设备、网络战、端口等 |
Mount | CLONE_NEWNS | 挂载点(文件系统) |
User | CLONE_NEWUSER | 用户组和用户组 |
实际上,linux内核实现namespace的主要目的,就是为了实现轻量级虚拟化技术服务。在同一个namespace下的进程合一感知彼此的变化,而对外界的进程一无所知。这样就可以让容器中的进程产生错觉,仿佛自己置身一个独立的系统环境中,以达到隔离的目的。
需要注意的是,本文所讨论的namespace实现针对的是linux内核3.8及以后版本。
PID namespace
PID namespace隔离非常实用,它对进程PID重新标号,即两个不同namespace下的进程可以有相同的PID。每个PID namespace都有自己的计数程序。内核为所有的PID namespace维护了一个树状结构,最顶层的是系统初始时创建的,被称为root namespace,它创建的新PID namespace被称为child namespace(树的子节点),而原来的PID namespace就是新创建的PID namespace的parent namespace(树的父节点)。通过这种方式,不同的PID namespace会形成一个层级体系。所属的父节点可以看到子节点中的进程,并可以通过信号等方式对子节点中的进程产生影响。反过来,子节点却不能看到父节点PID namespace中的任何内容,由此产生如下结论。
- 每个PID namespace中的第一个进程“PID 1”,都会像全通Linux中的init进程一样拥有特权,其特殊作用。
- 一个namespace中的进程,不可能通过kill或ptrace影响父节点或者兄弟节点中的进程,因为其他几点的PID在这个namespace没有任何意义。
- 如果你在新的PID namespace中重新挂载/proc文件系统,会发现其下只显示同属一个PID namespace中的其他进程。
- 在root namespace中看到所有的进程,并且递归包含所有子节点中的进程。到这里,读者可能已经联想到了一种在Docker外部监控运行程序的方法了,就是监控Docker daemon所在的PID namespace下的所有进程及子进程,在进行筛选即可。
-
PID namespace中的init进程
在传统的Unix系统中,PID为1的进程时init,地位非常特殊。它作为所有进程的父进程,维护一张进程表,不断检查进程状态,一旦某个子进程因为父进程错误成为了“孤儿”进程,init就会负责收养这个子进程并最终回收资源,结束进程。所以在要实现的容器中,启动的第一个进程也需要实现类似init的功能,维护所有后续启动进程的状态。
当系统中存在的树状嵌套结构的PID namespace时,若某个子进程成为了孤儿进程,收养孩子进程的责任就交给了孩子进程所属的PID namespace中的init进程。
至此,读者可以明白内核设计的良苦用心。PID namespace维护这样一个树状结构,有利于系统的资源的控制与回收。因此,如果确实需要在一个Docker容器中运行多个进程,最先启动的命令进程应该是具有资源监控与回收等管理能力的,如bash。 -
信号与init进程
内核还为PID namespace中的init进程赋予了其他特权—信号屏蔽。如果init中没有编写处理某个信号的代码逻辑,那么与init在同一个PID namespace下的进程(即使有超级权限)发送非他的信号都会屏蔽。这个功能主要作用就是防止init进程被误杀。
那么,父节点PID namespace中的进程发送同样的信号给子节点中的init的进程,这会被忽略吗?父节点中的进程发送的信号,如果不是SIGKILL(销毁进程)或SIGSTOPO(暂停进程)也会诶忽略。但如果发送SIGKILL或SIGSTOP,子节点的init会强制执行(无法通过代码捕捉进行特殊处理),也就是说父节点中的进程有权终止子进程。
一旦init进程被销毁,同一PID namespace中的其他进程也所致接收到SIGKIKLL信号而被销毁。理论上,该PID namespace也不复存在了。但如果/proc/[pid]/ns/pid处于被挂载或打开的状态,namespace就会被保留下来。然而,被保留下来的namespace无法通过setns()或者fork()创建进程,所以实际上并没有什么作用。
当一个容器内存在多个进程时,容器内的init进程可以对信号进行捕获,当SIGTERM或SIGINT等信号到来时,对其子进程做信息保存、资源回收等处理工作。在Docker daemon的源码中也可以看到类似的处理方式,当结束信号来临时,结束容器进程并回收相应资源。 -
挂载proc文件系统
前文提到,如果在新的PID namespace中使用使用ps命令查看,看到的还是所有的进程,因为与PID直接相关的/proc文件系统(procfs)没有挂载到一个与原/proc不同的位置。如果只想看到PID namespace本身应该看到的进程,需要重新挂载/proc,命令如下。
$ mount -t proc proc /proc
$ ps a
- 1
- 2
-
unshare()和setns()
本文开头就谈到了unshare()和setns()这两个API,在PID namespace中使用,也有一些特别之处需要注意。
unshare()允许用户在原有进程中建立命名空间进行隔离。但创建了PID namespace后,原先unshare()调用者进程并不进入新的PID namespace,接下来创建的子进程才会进入新的namespace,这个子进程也就随之成为新的namespace中的init进程。
类似地,调用setns()创建新PID namespace时,调用者进程也不进入新的PID namespace,而是随后创建的子进程进入。
为什么创建其他namespace时unshare()和setns()会直接进入新的namespace,二唯独PID namespace例外呢?因为调用getpid()函数得到的PID是根据调用者所在的PID namespace而决定返回哪个PID,进入新的PID namespace会导致PID产生变化。而对用户态的程序和库函数来说,他们都认为进程的PID是一个常量,PID的变化会引起这些进程崩溃。
换句话说,一旦程序进程创建以后,那么它的PID namespace的关系就确定下来了,进程不会变更它们对应的PID namespace。在Docker中,docker exec会使用setns()函数加入已经存在的命名空间,但是最终还是会调用clone()函数,原因就在于此。 -
mount namespace
mount namespace通过隔离文件系统挂载点对隔离文件系统提供支持,它是历史上第一个Linux namespace,所以标示位比较特殊,就是CLONE_NEWNS。隔离后,不同的mount namespace中的文件结构发生变化也互不影响。也可以通过/proc/[pid]/mounts查看到所有挂载在当前namespace中的文件系统,还可以通过/proc/[pid]/mountstats看到mount namespace中文件设备的统计信息,包括挂载文件的名字、文件系统的类型、挂载位置等。
进程在创建mount namespace时,会把当前的文件结构复制给新的namespace。新namespace中的所有mount操作都只影响自身的文件系统,对外界不会产生任何影响。这种做法非常严格的实现了隔离,但对某些状况可能并不适用。比如父节点namespace中的进程挂载了一张CD-ROM,这时子节点namespace复制的目录结构是无法自动挂载上这张CD-ROM的,因为这种操作会影响到父节点的文件系统。
一个挂载状态可能为以下一种:
- 共享挂载
- 从属挂载
- 共享/从属挂载
- 私有挂载
- 不可绑定挂载
传播事件的挂载对象称为共享挂载;接收传播事件的挂载对象称为从属挂载;同时兼有前述两者特征的挂载对象为共享/从属挂载;既不传播也不接受事件的挂载对象称为私有挂载;另一种特殊的挂载对象称为不可绑定挂载,它们与私有挂载相似,但不允许执行绑定挂载,即创建mount namespace时这块文件对象不可被复制。
1.docker run
[root@localhost ~]# docker run -it --name my-busybox2 docker.io/busybox /bin/sh
/ #
另启一个窗口在宿主机上
[root@localhost proc]# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES cb465fa71b06 docker.io/busybox "/bin/sh" 37 seconds ago Up 36 seconds my-busybox2 9b8c4b4cce0d docker.io/busybox "/bin/sh" 28 hours ago Up 6 minutes my-busybox1 [root@localhost proc]# docker top my-busybox2 UID PID PPID C STIME TTY TIME CMD root 2117 2101 0 06:09 pts/4 00:00:00 /bin/sh [root@localhost proc]# ##进入2117容器进程,pid的namespace的id为4026532746
2.docker exec
[root@localhost ~]# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES cb465fa71b06 docker.io/busybox "/bin/sh" 10 minutes ago Up 10 minutes my-busybox2 9b8c4b4cce0d docker.io/busybox "/bin/sh" 28 hours ago Up 16 minutes my-busybox1 [root@localhost ~]# docker exec -it my-busybox2 /bin/sh / #
主机上查看一下,docker-run 的进程2117和docker exec进程2354 在同一个namespace (pid对应的namespace的id 是一样)
所以在两个进程中看到的内容是一样的
在两个窗口中执行top
在docker exec中运行ping命令
在宿主机上查看namespace,发现ping和docker run 及docker exec及top都在同一个namespace中
[root@localhost ns]# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES cb465fa71b06 docker.io/busybox "/bin/sh" 27 minutes ago Up 27 minutes my-busybox2 9b8c4b4cce0d docker.io/busybox "/bin/sh" 29 hours ago Up 33 minutes my-busybox1 [root@localhost ns]# docker top my-busybox2 UID PID PPID C STIME TTY TIME CMD root 2117 (docker exec) 2101 0 06:09 pts/4 00:00:00 /bin/sh root 2354(docker run) 2340 0 06:20 pts/5 00:00:00 /bin/sh root 2519(docker run 中运行的top) 2354 0 06:33 pts/5 00:00:00 top root 2536 (docker exec中运行的ping命令) 2117 0 06:35 pts/4 00:00:00 ping www.baidu.com [root@localhost ns]# cd ../../2536/ns [root@localhost ns]# ll total 0 lrwxrwxrwx 1 root root 0 Aug 16 06:37 cgroup -> cgroup:[4026531835] lrwxrwxrwx 1 root root 0 Aug 16 06:37 ipc -> ipc:[4026532745] lrwxrwxrwx 1 root root 0 Aug 16 06:37 mnt -> mnt:[4026532743] lrwxrwxrwx 1 root root 0 Aug 16 06:37 net -> net:[4026532748] lrwxrwxrwx 1 root root 0 Aug 16 06:37 pid -> pid:[4026532746] lrwxrwxrwx 1 root root 0 Aug 16 06:37 pid_for_children -> pid:[4026532746] lrwxrwxrwx 1 root root 0 Aug 16 06:37 user -> user:[4026531837] lrwxrwxrwx 1 root root 0 Aug 16 06:37 uts -> uts:[4026532744] [root@localhost ns]# pwd /proc/2536/ns [root@localhost ns]#
3.单进程模式控制
https://blog.csdn.net/M2l0ZgSsVc7r69eFdTj/article/details/102028724