Namespace是什么
Namespace技术则是用来修改进程视图的主要方法。先通过一个实例来看是讲解namespace。
以下运行一个busybox的容器,使用ps可以看到,我们在Docker里最开始执行的/bin/sh,就是这个容器内部的第1号进程(PID=1),而这个容器里一共只有两个进程在运行。这就意味着,前面执行的/bin/sh,以及我们刚刚执行的ps,已经被Docker隔离在了一个跟宿主机完全不同的世界当中。
1 | [root@linjx ~]# docker run --name=busybox -it busybox:latest /bin/sh |
但是在宿主机里面,是可以看到这个/bin/sh的进程的,其PID为27220。
1 | [root@linjx ~]# ps aux |grep busy |
这种机制,其实就是对被隔离应用的进程空间做了手脚,使得这些进程只能看到重新计算过的进程编号,比如PID=1。可实际上,他们在宿主机的操作系统里,还是原来的进程。这种技术,就是Linux里面的Namespace机制。
而Namespace的使用方式也非常有意思:它其实只是Linux创建新进程的一个可选参数。我们知道,在Linux系统中创建线程的系统调用是clone(),当我们用clone()系统调用创建一个新进程时,就可以在参数中指定CLONE_NEWPID参数int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL);
;这时,新创建的这个进程将会“看到”一个全新的进程空间,在这个进程空间里,它的PID是1。之所以说“看到”,是因为这只是一个“障眼法”,在宿主机真实的进程空间里,这个进程的PID还是真实的数值。
除了我们刚刚用到的PID Namespace,Linux操作系统还提供了Mount、UTS、IPC、Network和User这些Namespace,用来对各种不同的进程上下文进行“障眼法”操作。
比如,Mount Namespace,用于让被隔离进程只看到当前Namespace里的挂载点信息;Network Namespace,用于让被隔离进程看到当前Namespace里的网络设备和配置。
namespaces简介
分类
目前 linux 内核主要实现了一下几种不同的资源 namespace:
名称 | 宏定义 | 隔离的内容 |
---|---|---|
IPC | CLONE_NEWIPC | 实现容器与宿主机、容器与容器之间的IPC隔离。IPC资源包括信号量、消息队列和共享内存。(since Linux 2.6.19) |
Network | CLONE_NEWNET | 提供了关于网络资源的隔离,包括网络设备、IPv4和IPv6协议栈、IP路由表、防火墙、/proc/net目录、/sys/class/net目录、套接字(socket)等。(since Linux 2.6.24) |
Mount | CLONE_NEWNS | 通过隔离文件系统挂载点隔离文件系统,是第一个实现的Linux namespace(since Linux 2.4.19) |
PID | CLONE_NEWPID | 两个不同namespace下的进程没有关系,因此PID也可以相同。内核为所有的PID namespace维护了一个树状结构。(since Linux 2.6.24) |
User | CLONE_NEWUSER | 隔离了安全相关的标识符和属性(用户ID、用户组ID、root目录、key(密钥)、特殊权限)。(started in Linux 2.6.23 and completed in Linux 3.8) |
UTS | CLONE_NEWUTS | 提供了主机名和域名的隔离,使每个Docker容器可以拥有独立的主机名和域名,在网络上可以视为独立的节点。 (since Linux 2.6.19) |
Cgroup | CLONE_NEWCGROUP | Cgroup root directory (since Linux 4.6) |
查看进程所属的namespace
从版本号为 3.8 的内核开始,/proc/[pid]/ns 目录下会包含进程所属的 namespace 信息,使用下面的命令可以查看当前进程所属的 namespace 信息:
1 | [root@localhost ~]# ll /proc/self/ns/ |
- 这些 namespace 文件都是链接文件。链接文件的内容的格式为
xxx:[inode number]
。其中的 xxx 为 namespace 的类型,inode number 则用来标识一个 namespace,我们也可以把它理解为 namespace 的 ID。如果两个进程的某个 namespace 文件指向同一个链接文件,说明其相关资源在同一个 namespace 中。 - 在 /proc/[pid]/ns 里放置这些链接文件的另外一个作用是,一旦这些链接文件被打开,只要打开的文件描述符(fd)存在,那么就算该 namespace 下的所有进程都结束了,但这个 namespace 也会一直存在,后续的进程还可以再加入进来。
当一个namespace中的所有进程都退出时,该namespace将会被销毁。当然还有其他方法让namespace一直存在,假设我们有一个进程号为1000的进程,以ipc namespace为例:
- 通过mount —bind命令。例如
mount --bind /proc/1000/ns/ipc /other/file
,就算属于这个ipc namespace的所有进程都退出了,只要/other/file还在,这个ipc namespace就一直存在,其他进程就可以利用/other/file,通过setns函数加入到这个namespace - 在其他namespace的进程中打开
/proc/1000/ns/ipc
文件,并一直持有这个文件描述符不关闭,以后就可以用setns函数加入这个namespace。
跟namespace相关的API
clone
clone:创建一个新的进程并把他放到新的namespace中
1 | int clone(int (*child_func)(void *), void *child_stack |
setns
setns将当前进程加入到已有的namespace中
1 | int setns(int fd, int nstype); |
unshare
unshare: 使当前进程退出指定类型的namespace,并加入到新创建的namespace(相当于创建并加入新的namespace)
1 | int unshare(int flags); |
clone和unshare区别
clone和unshare的功能都是创建并加入新的namespace, 他们的区别是:
- unshare是使当前进程加入新的namespace
- clone是创建一个新的子进程,然后让子进程加入新的namespace,而当前进程保持不变
相关命令
在linux上,有 nsenter unshare
这2个命令可以直接使用来实现不同的功能,具体如下:
- nsenter:加入指定进程的指定类型的namespace,然后执行参数中指定的命令。详情请参考帮助文档和代码。
- unshare:离开当前指定类型的namespace,创建且加入新的namespace,然后执行参数中指定的命令。详情请参考帮助文档和代码。
IPC & UTS namespace
IPC namespace用来隔离System V IPC objects和POSIX message queues。其中System V IPC objects包含消息列表Message queues、信号量Semaphore sets和共享内存Shared memory segments.
在实例之前,先介绍2个跟ipc相关的命令:
- ipcmk:创建shared memory segments, message queues, 和semaphore arrays
- ipcs:查看shared memory segments, message queues, 和semaphore arrays的相关信息
- 参数-a:显示全部可显示的信息
- 参数-q:显示活动的消息队列信息
- 参数-m:显示活动的共享内存信息
- 参数-s:显示活动的信号量信息
这里将以消息队列为例,演示一下隔离效果,为了使演示更直观,我们在创建新的ipc namespace的时候,同时也创建新的uts namespace,然后为新的utsnamespace设置新hostname,这样就能通过shell提示符一眼看出这是属于新的namespace的bash,后面的文章中也采取这种方式启动新的bash。
在这个示例中,我们将用到两个shell窗口
1 | #--------------------------第一个shell窗口---------------------- |
这样就实现了 hostname 以及 ipc 的隔离了。
此内容全部来自:Linux Namespace系列(03):IPC namespace (CLONE_NEWIPC)
Mount namespace
Mount namespaces是第一个被加入Linux的namespace,由于当时没想到还会引入其它的namespace,所以取名为CLONE_NEWNS
,而没有叫CLONE_NEWMOUNT
。
Mount namespace用来隔离文件系统的挂载点, 使得不同的mount namespace拥有自己独立的挂载点信息,不同的namespace之间不会相互影响,这对于构建用户或者容器自己的文件系统目录非常有用。
当前进程所在mount namespace里的所有挂载信息可以在/proc/[pid]/mounts、/proc/[pid]/mountinfo和/proc/[pid]/mountstats
里面找到。
每个mount namespace都拥有一份自己的挂载点列表,当用clone或者unshare函数创建新的mount namespace时,新创建的namespace将拷贝一份老namespace里的挂载点列表,但从这之后,他们就没有关系了,通过mount和umount增加和删除各自namespace里面的挂载点都不会相互影响。
以下例子就实现了mount的隔离了。
1 | #--------------------------第一个shell窗口---------------------- |
在这篇文章中, Linux Namespace系列(04):mount namespaces (CLONE_NEWNS) 还有一个实例,是关于Shared subtrees,有兴趣的同学可以看再下。
PID namespace
PID namespaces用来隔离进程的ID空间,使得不同pid namespace里的进程ID可以重复且相互之间不影响。
PID namespace可以嵌套,也就是说有父子关系,在当前namespace里面创建的所有新的namespace都是当前namespace的子namespace。父namespace里面可以看到所有子孙后代namespace里的进程信息,而子namespace里看不到祖先或者兄弟namespace里的进程信息。目前PID namespace最多可以嵌套32层,由内核中的宏MAX_PID_NS_LEVEL来定义。
Linux下的每个进程都有一个对应的/proc/PID目录,该目录包含了大量的有关当前进程的信息。 对一个PID namespace而言,/proc目录只包含当前namespace和它所有子孙后代namespace里的进程的信息。
在Linux系统中,进程ID从1开始往后不断增加,并且不能重复(当然进程退出后,ID会被回收再利用),进程ID为1的进程是内核启动的第一个应用层进程,一般是init进程(现在采用systemd的系统第一个进程是systemd),具有特殊意义,当系统中一个进程的父进程退出时,内核会指定init进程成为这个进程的新父进程,而当init进程退出时,系统也将退出。
除了在init进程里指定了handler的信号外,内核会帮init进程屏蔽掉其他任何信号,这样可以防止其他进程不小心kill掉init进程导致系统挂掉。不过有了PID namespace后,可以通过在父namespace中发送SIGKILL或者SIGSTOP信号来终止子namespace中的ID为1的进程。
由于ID为1的进程的特殊性,所以每个PID namespace的第一个进程的ID都是1。当这个进程运行停止后,内核将会给这个namespace里的所有其他进程发送SIGKILL信号,致使其他所有进程都停止,于是namespace被销毁掉。
简单示例
我们经常在docker下面看到pid为1的进程,他是怎么样实现的呢?如下:
1 | #查看当前pid namespace的ID |
嵌套示例
- 调用unshare或者setns函数后,当前进程的namespace不会发生变化,不会加入到新的namespace,而它的子进程会加入到新的namespace。也就是说进程属于哪个namespace是在进程创建的时候决定的,并且以后再也无法更改。
- 在一个PID namespace里的进程,它的父进程可能不在当前namespace中,而是在外面的namespace里面(这里外面的namespace指当前namespace的祖先namespace),这类进程的ppid都是0。比如新namespace里面的第一个进程,他的父进程就在外面的namespace里。通过setns的方式加入到新namespace中的进程的父进程也在外面的namespace中。
- 可以在祖先namespace中看到子namespace的所有进程信息,且可以发信号给子namespace的进程,但进程在不同namespace中的PID是不一样的。
- 以下示例是在ubuntu测试的,如果在centos上,大体是一样的,但/proc/[pid]/status没有pid信息。
1 | #--------------------------第一个shell窗口---------------------- |
注意,kill的动作,类似进程的概念,父进程被kill之后,所有的子进程都会自动消亡。
本小节来源于:Linux Namespace系列(05):pid namespace (CLONE_NEWPID)
Network namespace
network namespace用来隔离网络设备, IP地址, 端口等. 每个namespace将会有自己独立的网络栈,路由表,防火墙规则,socket等。
每个新的network namespace默认有一个本地环回接口,除了lo接口外,所有的其他网络设备(物理/虚拟网络接口,网桥等)只能属于一个network namespace。每个socket也只能属于一个network namespace。
当新的network namespace被创建时,lo接口默认是关闭的,需要自己手动启动起。
标记为”local devices”的设备不能从一个namespace移动到另一个namespace,比如loopback, bridge, ppp等,我们可以通过ethtool -k命令来查看设备的netns-local属性。
1 | #这里“on”表示该设备不能被移动到其他network namespace |
实例演示
本示例将演示如何创建新的network namespace并同外面的namespace进行通信。
1 | #--------------------------第一个shell窗口---------------------- |
到目前为止,两个namespace之间可以网络通信了,但在container001里还是不能访问外网。下面将通过NAT的方式让container001能够上外网。这部分内容完全是网络相关的知识,跟namespace已经没什么关系了。
1 | #--------------------------第二个shell窗口---------------------- |
network namespace的概念比较简单,但如何做好网络的隔离和连通却比较难,包括性能和安全相关的考虑,需要很好的Linux网络知识。
ip netns
在单独操作network namespace时,ip netns是一个很方便的工具,并且它可以给namespace取一个名字,然后根据名字来操作namespace。那么给namespace取名字并且根据名字来管理namespace里面的进程是怎么实现的呢?请看下面的脚本演示。
1 | #开始之前,获取一下默认network namespace的ID |
从上面可以看出,给namespace取名字其实就是创建一个文件,然后通过mount --bind
将新创建的namespace文件和该文件绑定,就算该namespace里的所有进程都退出了,内核还是会保留该namespace,以后我们还可以通过这个绑定的文件来加入该namespace。
本小节内容来源:Linux Namespace系列(06):network namespace (CLONE_NEWNET)
User namespace
Linux Namespace系列(07):user namespace (CLONE_NEWUSER) (第一部分)
Linux Namespace系列(08):user namespace (CLONE_NEWUSER) (第二部分)
总结
Docker容器这个听起来玄而又玄的概念,实际上是在创建容器进程时,指定了这个进程所需要启用的一组Namespace参数。这样,容器就只能“看”到当前Namespace所限定的资源、文件、设备、状态,或者配置。而对于宿主机以及其他不相关的程序,它就完全看不到了。
Namespace技术实际上修改了应用进程看待整个计算机“视图”,即它的“视线”被操作系统做了限制,只能“看到”某些指定的内容。但对于宿主机来说,这些被“隔离”了的进程跟其他进程并没有太大区别。