Docker基本原理:Docker通过namespace实现资源隔离,通过cgroups实现资源限制,通过写时复制机制(copy-on-write)实现了高效的文件操作。
1.namespace API
共有以下四种操作方式:
(1)clone():使用clone(child_fun,child_stack,flags,args)函数创建一个独立的namespace。
(2)setns():加入一个已经存在的namespace。int setns(int fd,int nstype)函数执行后使用clone()创建子进程继续执行命令,让原先进程结束运行。
(3)unshare():在原先进程上进行namespace隔离。int unshare(int flags)函数与clone()很像,不同的是,unshare运行在原先的进程上,不需要启动一个新的进程。
(4)/proc/目录操作:3.8版本的Linux内核开始,用户可在/proc/${PID}/ns/目录下查看指向不同namespace号的文件。
2.UTS namespace
clone函数调用参数加入CLONE_NEWUTS标志时,就实现了主机名和域名的隔离。子进程中调用sethostname函数可设置主机名。
3.IPC namespace
涉及的IPC资源包括常见的信号量、消息队列和共享内存。标志为CLONE_NEWIPC
4.PID namespace
PID namespace隔离非常实用,对进程PID重新编号,即两个不同namespace下的进程可以有相同的PID。
内核为所有的PID namespace维护了一个树状结构,最顶层的是系统初始时创建的,成为root namespace。它创建的新PID namespace被称为child namespace(树的子节点),而原先的PID namespace就是新创建的PID namespace的parent namespace(树的父节点)。通过这种方式,不同的PID namespace会形成一个层级体系。所属的父节点可以看到子节点中的进程,并通过信号等方式对子节点中的进程产生影响。而子节点看不到父节点PID namespace中的任何内容。
ps/top之类的命令调用的是真实系统下的/proc目录下的文件内容,看到的是所有的进程。
传统的Unix系统中,PID为1的进程是init,地位特殊,是所有进程的父进程,维护一张表,不断检查进程状态,一旦某个子进程因为父进程错误成为了孤儿进程,init就会负责收养这个子进程并最终收回资源,结束进程。所以在要实现的容器中,启动的第一个进程也需要实现类似init的功能,维护所有后续启动进程的运行状态。
一旦init进程被销毁,同一PID namespace中的其他进程也随之收到SIGKILL信号而被销毁。
5.MOUNT namespace
通过隔离文件系统挂载点对隔离文件系统提供支持,是历史上第一个Linux namespace,标识位为CLONE_NEWNS。
进程在创建mount namespace时,会把当前的文件结构复制给新的namespace。新namespace中的所有mount操作都只影响自身的文件系统,对外界不会产生任何影响。
挂载传播(mount propagation)
6.NETWORK namespace
提供关于网络资源的隔离,包括网络设备、IPv4和IPv6协议栈、IP路由表、防火墙、/proc/net/目录、/sys/class/net/目录、套接字(socket)等。
一个网络设备最多存在于一个network namespace,可以通过创建veth pair(虚拟网络设备对)在不同的network namespace间创建通道,达到通信的目的。一般情况下,网络设备都分配在最初的root namespace中。
建立veth pair之前,新旧namespace通过pipe进行通信。
7.USER namespace
隔离了安全相关的标识符(identifier)和属性(attribute),包括用户ID、用户组ID、root目录、key(指密钥)以及特殊权限。