大家好,我是张锦涛。至此我们所提到的容器技术和虚拟化技术(无论任何抽象层次的虚拟化技术)都可以实现资源层面的隔离和限制。对于容器技术,它实现了资源级别的限制和隔离,依赖于Linux内核提供的cgroup和namespace技术。我们先总结一下这两种技术的功能:cgroup的主要功能:管理资源的分配和限制;命名空间的主要作用:封装抽象、限制、隔离,让命名空间中的进程好像拥有自己的全局资源;这是一个系列文章,对这个系列感兴趣的朋友可以查看:深入理解容器技术基石:cgroup深入理解容器技术基石:命名空间(上)我们将在本篇继续讲命名空间.命名空间类型让我们首先看一下命名空间的类型。在上一篇文章中,我们介绍了四种命名空间:Cgroup、IPC、Network和Mount。让我们继续剩下的。命名空间名称使用的标识-Flag控制内容CgroupCLONE_NEWCGROUPCgroup根目录cgroup根目录IPCCLONE_NEWIPCSystemVIPC,POSIX消息队列信号量,消息队列NetworkCLONE_NEWNET网络设备、协议栈、端口等网络设备、协议栈、端口等MountCLONE_NEWNS挂载点PIDCLONE_NEWPIDProcessIDsprocessIDTimeCLONE_NEWTIMEBootandmonotonicclocksstartupandmonotonicclockUserCLONE_NEWUSERUserandgroupIDsuserandusergroupUTSCLONE_NEWUTSHostnameandNISdomainnamehostnameandNISdomainnamePIDnamespaces我们知道在Linux系统中,每个进程都会有自己独立的PID,PID命名空间主要用来隔离进程号。也就是说,相同的进程ID可以包含在不同的PID名称空间中。每个PID命名空间中的进程号都是从1开始的。在这个PID命名空间中,可以通过调用fork(2)、vfork(2)、clone(2)等系统调用创建其他具有独立PID的进程。使用PID命名空间需要内核支持CONFIG_PID_NS选项。如下:(MoeLove)?grepCONFIG_PID_NS/boot/config-$(uname-r)CONFIG_PID_NS=yinit进程我们都知道Linux系统中有一个特殊的进程,所谓的init进程,也就是进程withPID1。我们已经说过每个PID命名空间中的进程号都是从1开始的,那么它有什么特点呢?首先,PID命名空间中的1号进程是所有孤立进程的父进程。其次,如果进程终止,内核将调用SIGKILL来发出终止该命名空间中所有进程的信号。这部分内容涉及Kubernetes中应用的优雅关闭/平滑升级。(对这部分感兴趣的小伙伴可以留言交流,如果对这些内容感兴趣,我可以专门写一篇文章展开讨论。)最后,从Linuxv3.4内核版本开始,如果重启发生在PID命名空间()系统调用中,PID命名空间中的init进程会立即退出。这是一个比较特殊的技巧,可以用来处理高负载机器上的容器出口。PID命名空间的层级结构PID命名空间支持嵌套,除初始PID命名空间外,其余PID命名空间都有其父节点的PID命名空间。也就是说,PID命名空间也是一个树形结构,我们可以将这个结构中的所有PID命名空间追溯到祖先PID命名空间。当然,这个深度不是无限的。从Linuxv3.7内核版本开始,树的最大深度被限制为32。如果达到这个最大深度,将抛出Nospaceleftondevice错误。(我之前尝试嵌套容器时遇到过这个问题)进程在同一个(和同级)PID命名空间中彼此可见。但是如果一个进程位于子PID命名空间中,那么该进程就看不到上层(即父PID命名空间)中的进程。进程是否可见决定了进程之间是否存在一定的关联和调用关系。小伙伴们应该对此不陌生,这里就不赘述了。那么,是否可以将进程调度到不同级别的PID命名空间中呢?先说结论,PID命名空间下进程的调度只能是单向调度(从high->low)。即:进程只能从父PID命名空间调度到子PID命名空间;无法将进程从子PID命名空间调度到父PID命名空间;图1,通过setns(2)调度进程可以看出,PID命名空间的层级关系实际上是由ioctl_ns(2)发现和维护的系统调用(NS_GET_PARENT)决定的,这里不再展开。那么,上面内容中的调度是如何实现的呢?要回答这个问题,首先要认识到在PID命名空间创建之初,哪些进程拥有该命名空间的权限就已经确定了。至于调度,我们可以简单理解为关系图或者符号链接。线程必须在同一个PID命名空间中,以确保进程中的线程可以相互传递信号。这导致CLONE_NEWPID不能与CLONE_THREAD同时使用。但是如果分布在不同PID命名空间的多个进程需要相互传输信号怎么办?可以通过使用共享信号队列来解决。另外,我们经常接触到的/proc目录下还有很多/proc/${PID}目录,在其中可以看到PID命名空间中的进程状态。同时这个目录也可以直接挂载操作。例如:(MoeLove)?mount|grepprocprocon/proctypeproc(rw,nosuid,nodev,noexec,relatime)有没有办法知道当前PID的最大数量?这也是可以的,因为Linuxv3.3内核增加了一个/proc/sys/kernel/ns_last_pid文件来记录最后一个进程的ID。当下一个进程ID需要分配时,内核会寻找最大未使用的ID进行分配,然后更新这个文件中的PID信息。时间命名空间在谈论时间命名空间之前,我们需要先谈谈单调时间。首先,我们通常所说的系统时间是指clockrealtime,即机器对当前时间的显示。它可以向前或向后调整(结合NTP服务来理解)。时钟单调表示某一时刻之后的时间记录,是单向后退的绝对时间,不受系统时间变化的影响。使用时间命名空间需要内核支持CONFIG_TIME_NS选项。例如:(MoeLove)?grepCONFIG_TIME_NS/boot/config-$(uname-r)CONFIG_TIME_NS=ytime命名空间不虚拟化CLOCK_REALTIME时钟。你可能想知道,为什么内核支持时间命名空间?主要针对一些特殊场景。时间命名空间中的所有进程共享时间命名空间提供的以下两个参数:CLOCK_MONOTONIC-单调时间,不可设置的时钟;CLOCK_BOOTTIME(参见CLOCK_BOOTTIME_ALARM内核参数)——一个不可设置的时钟,包括系统挂起时的时间。时间命名空间目前仅可用于CLONE_NEWTIME标志,并通过调用unshare(2)系统调用创建。创建时间命名空间的进程独立于新创建的时间命名空间,该进程后续的子进程将放置在新创建的时间命名空间中。同一时间命名空间中的进程共享CLOCK_MONOTONIC和CLOCK_BOOTTIME。当父进程创建子进程时,子进程的时间命名空间所有权将显示在文件/proc/[pid]/ns/time_for_children中。(MoeLove)?ls-al/proc/self/ns/time_for_childrenlrwxrwxrwx。1taotao0December1402:06/proc/self/ns/time_for_children->'time:[4026531834]'文件/proc/PID/timens_offsets定义初始时间命名空间的单调时钟和起始时钟,并记录偏移量.(如果还没有入驻新的时间命名空间,可以修改,这里就不展开了,有兴趣的朋友可以在讨论区留言讨论。)需要注意的是,在初始时间命名空间中,/proc/self/timens_offsets显示的偏移量都是0。(萌萌哒)?cat/proc/self/timens_offsetsmonotonic00boottime00第二列和第三列的含义如下:可以为负数,unit:second(s)是一个无符号值,unit:nanosecond(ns)以下时钟接口与此命名空间相关联:clock_gettime(2)clock_nanosleep(2)nanosleep(2)timer_settime(2)timerfd_settime(2)总体来说,时间命名空间在一些特殊场景下还是有用的。用户命名空间用户命名空间,顾名思义,隔离用户id、组id等。使用用户命名空间需要内核支持CONFIG_USER_NS选项。如:?local_timegrepCONFIG_USER_NS/boot/config-$(uname-r)CONFIG_USER_NS=y一个进程的用户id和组id在用户命名空间内外可能不同。例如,进程的用户和组在用户命名空间中可能是特权用户(root),但在该用户命名空间之外,它可能只是一个普通的非特权用户。这里涉及到用户和组映射(uid_map、gid_map)等相关内容。从Linuxv3.5内核开始,在/proc/[pid]/uid_map和/proc/[pid]/gid_map文件中,我们可以查看映射内容。(MoeLove)?cat/proc/self/uid_map004294967295(MoeLove)?cat/proc/self/gid_map004294967295用户命名空间也支持嵌套,使用CLONE_NEWUSER标志,使用unshare(2)或clone(2)等systems调用create,最大嵌套层数深度为32。如果fork(2)或clone(2)创建的子进程没有CLONE_NEWUSER标志也是一样的,子进程与父进程。树状关系也通过ioctl(2)系统调用接口维护。单线程进程可以通过setns(2)系统调用调整自己的用户命名空间。另外,用户命名空间还有一个很重要的规则,就是Linux能力的继承关系。我不会扩展Linux功能。这里简单记录一下:当进程所在的用户命名空间拥有有效能力集中的能力时,该进程拥有该能力。当一个进程在这个用户命名空间中持有一个能力时,这个进程在这个用户命名空间的所有子用户命名空间中持有这个能力。创建用户命名空间的用户将被内核记录为所有者,即拥有用户命名空间中的所有能力。对于Docker来说,它可以原生支持这种能力,从而实现对容器环境的一种保护。UTS命名空间UTS命名空间隔离主机名和NIS域名。使用UTS命名空间需要内核支持CONFIG_UTS_NS选项。如:(MoeLove)?grepCONFIG_UTS_NS/boot/config-$(uname-r)CONFIG_UTS_NS=y在同一个UTS命名空间下,sethostname(2)和setdomainname(2)系统调用所做的设置和修改都是进程共享查看,但彼此隔离并且对不同的UTS名称空间不可见。前面Namespaces主要API的内容中提到了很多系统调用。在这里我们将介绍几个重要的。clone(2)系统调用clone(2)创建一个新进程,该进程会根据参数中的CLONE_NEW*设置一一实现相应的配置功能。当然,这个系统调用也实现了一些与命名空间无关的功能。对于内核早于Linux3.8的系统,大多数情况下需要CAP_SYS_ADMIN的能力。unshare(2)系统调用unshare(2)将进程分配给新的命名空间。同样,它也会根据参数中的CLONE_NEW*设置,调整并实现相应的配置功能。对于Linux3.8以下的系统,大多数情况下需要CAP_SYS_ADMIN的能力。setns(2)系统调用setns(2)将进程移动到现有命名空间,这会导致与/proc/[pid]/ns对应的目录的内容发生变化。进程创建的子进程可以通过调用unshare(2)和setns(2)来调整自己所属的命名空间。这通常需要具有CAP_SYS_ADMIN的能力。一些关键目录说明/proc/[pid]/ns/目录每个进程都有一个/proc/[pid]/ns/子目录,其内容受setns(2)系统调用的影响。只要目录下的文件被打开,对应的命名空间就不会被破坏。系统可以通过调用setns(2)来更改这些文件的内容。Linux3.7及更早版本——文件以硬链接形式存在;Linux3.8以上——文件以软链接形式存在;(MoeLove)?ls-l--time-style='+'/proc/$$/ns总使用量0lrwxrwxrwx。1taotao0cgroup->'cgroup:[4026531835]'lrwxrwxrwx.1taotao0ipc->'ipc:[4026531839]'lrwxrwxrwx.1taotao0mnt3[180'm]'lrwxrwxrwx。1淘淘0网->'网:[4026532008]'lrwxrwxrwx。1taotao0pid->'pid:[4026531836]'lrwxrwxrwx。1淘淘0pid_for_children->'pid]3':[4lrwxrwxrwx。1淘淘0时间->'time:[4026531834]'lrwxrwxrwx.1taotao0time_for_children->'时间:[4026531834]'lrwxrwxrwx。1taotao0uts->'uts:[4026531838]'如果两个进程的命名空间相同,那么它们目录下的内容应该是相同的。以下是该目录下文件的详细说明:文件名初始版本描述/proc/[pid]/ns/cgroupLinux4.6进程cgroup命名空间/proc/[pid]/ns/ipcLinux3.0进程IPC命名空间/proc/[pid]/ns/mntLinux3.8进程挂载命名空间/proc/[pid]/ns/netLinux3.0进程网络命名空间/proc/[pid]/ns/pidLinux3.8进程PID命名空间不在进程的整个生命周期中changed/proc/[pid]/ns/pid_for_childrenLinux4.12进程创建子进程的PID命名空间。这个文件不一定和/proc/[pid]/ns/pid一致。/proc/[pid]/ns/timeLinux5.6进程时间命名空间/proc/[pid]/ns/time_for_childrenLinux5.6进程创建子进程时间命名空间/proc/[pid]/ns/userLinux3.8进程用户命名空间/proc/[pid]/ns/utsLinux3.0进程UTS命名空间/proc/sys/user目录/proc/sys/user目录下的文件记录了各个命名空间的相关限制。当达到限制时,相关调用会报错ENOSPC。文件名限制内容说明max_cgroup_namespaces每个用户在用户命名空间中可以创建的最大cgroup命名空间数max_ipc_namespaces每个用户在用户命名空间中可以创建的最大ipc命名空间数max_mnt_namespaces每个用户可以在用户命名空间中创建的最大数目挂载命名空间个数max_net_namespaces用户命名空间中每个用户最多可以创建的网络命名空间个数max_pid_namespaces每个用户在用户命名空间中最多可以创建的PID命名空间个数max_time_namespacesLinux5.7每个用户在网络中最多可以创建的时间usernamespace命名空间的个数max_user_namespaces用户命名空间中每个用户可以创建的用户命名空间的最大个数max_uts_namespaces用户命名空间中每个用户可以创建的uts命名空间的最大个数。命名空间生命周期正常的命名空间生命周期与最后一个进程的终止离开有关。但在某些情况下,即使最后一个进程已经退出,命名空间仍然无法销毁。下面说说这些特殊情况:/proc/[pid]/ns/*中的文件被打开或挂载,即使最后一个进程退出,也无法销毁;命名空间是分层的,子命名空间仍然存在,即使最后一个进程退出,也无法销毁;用户命名空间有一些非用户命名空间(如PID命名空间等命名空间),即使最后一个进程退出,也无法销毁;对于PID命名空间,如果与/时proc/[pid]/ns/pid_for_children有关联关系,即使最后一个进程退出,也无法销毁;当然还有一些其他的情况,有空再补充。在总结了上一篇和本文之后,主要介绍了Linux命名空间的发展过程、基本类型、主要API,以及一些使用场景和用途。命名空间是容器技术中非常核心的部分。后续系列中,我们将继续分享容器、Kubernetes等技术内容,敬请期待。欢迎订阅我的文章公众号【MoeLove】