本文共 6961 字,大约阅读时间需要 23 分钟。
本节书摘来自华章社区《Docker进阶与实战》一书中的第2章,第2.4节SparkContext概述,作者华为Docker实践小组,更多章节内容可以访问云栖社区“华章社区”公众号查看
2.4 Namespace介绍
2.4.1 Namespace是什么Namespace是将内核的全局资源做封装,使得每个Namespace都有一份独立的资源,因此不同的进程在各自的Namespace内对同一种资源的使用不会互相干扰。这样的解释可能不清楚,举个例子,执行sethostname这个系统调用时,可以改变系统的主机名,这个主机名就是一个内核的全局资源。内核通过实现UTS Namespace,可以将不同的进程分隔在不同的UTS Namespace中,在某个Namespace修改主机名时,另一个Namespace的主机名还是保持不变。目前Linux内核总共实现了6种Namespace:IPC:隔离System V IPC和POSIX消息队列。Network:隔离网络资源。Mount:隔离文件系统挂载点。PID:隔离进程ID。UTS:隔离主机名和域名。User:隔离用户ID和组ID。2.4.2 Namespace的接口和使用对Namespace的操作,主要是通过clone、setns和unshare这3个系统调用来完成的。clone可以用来创建新的Namespace。它接受一个叫flags的参数,这些flag包括CLONE_NEWNS、CLONE_NEWIPC、CLONE_NEWUTS、CLONE_NEWNET、CLONE_NEWPID和CLONE_NEWUSER,我们通过传入这些CLONE_NEW*来创建新的Namespace。这些flag对应的Namespace都可以从字面上看出来,除了CLONE_NEWNS,这是用来创建Mount Namespace的。指定了这些flag后,由clone创建出来的新进程,就位于全新的Namespace里了,并且很自然地这个新进程以后创建出来的进程,也都在这个Namespace中。提示 Mount Namespace是第一个实现的Namespace,当初实现时并不是为了实现Linux容器,因此也就没有预料到会有新的Namespace出现,因此用了CLONE_NEWNS而不是CLONE_NEWMNT之类的名字。那么,能不能为已有的进程创建新的Namespace呢?答案是可以,unshare就是用来达到这个目的的。调用这个系统调用的进程,会被放进新创建的Namespace里,要创建什么Namespace由flags参数指定,可以使用的flag也就是上面提到的那些。
以上两个系统调用都是用来创建新的Namespace的,而setns则可以将进程放到已有的Namespace里,问题是如何指定已有的Namespace?答案在procfs里。每个进程在procfs下都有一个目录,在那里面就有Namespace相关的信息,如下。# ls –l /proc/$$/nstotal 0lrwxrwxrwx 1 root root 0 Jun 16 14:39 ipc -> ipc:[4026531839]lrwxrwxrwx 1 root root 0 Jun 16 14:39 mnt -> mnt:[4026531840]lrwxrwxrwx 1 root root 0 Jun 16 14:39 net -> net:[4026531957]lrwxrwxrwx 1 root root 0 Jun 16 14:39 pid -> pid:[4026531836]lrwxrwxrwx 1 root root 0 Jun 16 14:39 user -> user:[4026531837]lrwxrwxrwx 1 root root 0 Jun 16 14:39 uts -> uts:[4026531838]
这里每个虚拟文件都对应了这个进程所处的Namespace。因此,如果另一个进程要进入这个进程的Namespace,可以通过open系统调用打开这里面的虚拟文件并得到一个文件描述符,然后把文件描述符传给setns,调用返回成功的话,就进入这个进程的Namespace了。
docker exec命令的实现原理就是setns。以下是一个简单的程序,在Linux终端调用这个程序就会进入新的Namespace,同时也可以打开另一个终端,这个终端是在host的Namespace里,这样就可以对比两个Namespace的区别了。
#define _GNU_SOURCE#include#include #include #include #define STACK_SIZE (1024 * 1024)static char stack[STACK_SIZE];static char* const child_args[] = { "/bin/bash", NULL };static int child(void *arg){ execv("/bin/bash", child_args); return 0;}int main(int argc, char *argv[]){ pid_t pid; pid = clone(child, stack+STACK_SIZE, SIGCHLD|CLONE_NEWUTS, NULL); waitpid(pid, NULL, 0);}
这个程序创建了UTS Namespace,可以通过修改flag,创建其他Namespace,也可以创建几个Namespace的组合。这个程序将会用来为下面的内容做演示。
2.4.3 各个Namespace介绍那么,为什么要使用UTS Namespace做隔离?这是因为主机名可以用来代替IP地址,因此,也就可以使用主机名在网络上访问某台机器了,如果不做隔离,这个机制在容器里就会出问题。
调用之前的程序后,在Namespace终端执行以下命令:# hostname container# hostnamecontainer这里已经改变了主机名,现在通过host终端来看看host的主机名:# hostnamelinux-host
可以看到,host的主机名并没有变化,这就是Namespace所起的作用。
IPC Namespace能做到的事情是,使相同的标识符在两个Namespace中代表不同的消息队列,这样也就使得两个Namespace中的进程不能通过IPC进程通信了。
举个例子,在namespace终端创建了一个消息队列:# ipcmk -QMessage queue id: 65536# ipcs -q------ Message Queues --------key msqid owner perms used-bytes messages0x0ec037c7 65536 root 644 0 这个消息队列的标识符是65536,现在在host终端看一下:# ipcs -q------ Message Queues --------key msqid owner perms used-bytes messages
在这里看不到任何消息队列,IPC隔离的效果达到了。
当创建一个PID Namespace时,第一个进程的PID号是1,也就是init进程。init进程有一些特殊之处,例如init进程需要负责回收所有孤儿进程的资源。另外,发送给init进程的任何信号都会被屏蔽,即使发送的是SIGKILL信号,也就是说,在容器内无法“杀死”init进程。
但是当用ps命令查看系统的进程时,会发现竟然可以看到host的所有进程:# ps ax PID TTY STAT TIME COMMAND 1 ? Ss 0:24 init [5] 2 ? S 0:06 [kthreadd] 3 ? S 1:37 [ksoftirqd/0] 5 ? S< 0:00 [kworker/0:0H] 7 ? S 0:16 [kworker/u33:0]...7585 pts/0 S+ 0:00 sleep 1000这是因为ps命令是从procfs读取信息的,而procfs并没有得到隔离。虽然能看到这些进程,但由于它们其实是在另一个PID Namespace中,因此无法向这些进程发送信号:# kill -9 7585-bash: kill: (7585) - No such process
之前看到,创建PID Namespace后,由于procfs没有改变,因此通过ps命令看到的仍然是host的进程树,其实可以通过在这个PID Namespace里挂载procfs来解决这个问题,如下:
# mount –t proc none /proc# ps ax PID TTY STAT TIME COMMAND 1 pts/2 S+ 0:00 newns 3 pts/2 R+ 0:00 ps ax但此时由于文件系统挂载点没有隔离,因此host看到的procfs也会是这个新的procfs,这样在host上就会出问题:# ps axError, do this: mount -t proc none /proc
可如果同时使用Mount Namespace和PID Namespace,新的Namespace里的进程和host上的进程将会看到各自的procfs,故而也就不存在上面的问题了。
新创建的Network Namespace会有一个loopback设备,除此之外不会有任何其他网络设备,因此用户需要在这里面做自己的网络配置。IP工具已经支持Network Namespace,可以通过它来为新的Network Namespace配置网络功能。首先创建Network Namespace:
# ip netns add new_ns使用“ip netns exec”命令可以对特定的Namespace执行网络管理:# ip netns exec new_ns ip link list1: lo:mtu 65536 qdisc noop state DOWN mode DEFAULT link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00看到确实只有loopback这个网络接口,并且它还因处于DOWN状态而不可用:# ip netns exec new_ns ping 127.0.0.1connect: Network is unreachable通过以下命令可以启用loopback网络接口:# ip netns exec new-ns ip link set dev lo up# ip netns exec new-ns ping 127.0.0.1PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.053 ms...最后可以这样删除Namespace:# ip netns delete new_ns
容器的网络配置是一个很大的话题,后面有专门的章节讲解,因此这里暂不展开。
注
意 容器内的这类root用户,实际上还是有很多特权操作不能执行,基本上如果这个特权操作会影响到其他容器或者host,就不会被允许。在host上,可以看到我们是lizf用户。
$ iduid=1000(lizf) gid=100(users) groups=100(users)现在创建新的User Namespace,看看又是什么情况?$ new-userns$ iduid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)可以看到,用户名和组名都变了,变成65534,不再是原来的1000和100。接下来的问题是,怎么设定Namespace和host的UID的映射关系?方法是在创建新的Namespace后,设置这个Namespace里进程的/proc//uid_map。在Namespace终端看到的是这样的:$ iduid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)$ echo $$17074$ cat /proc/17074/uid_map$可以看到uid_map是空的,也就是还没有UID的映射。这可以在host终端上通过root用户设置,如下。# echo "0 1000 65536" > /proc/17074/uid_map上面命令表示要将[1000, 66536]的UID在Namespace里映射成[0, 65536]。再切回到Namespace终端看看:$ iduid=0(root) gid=65534(nogroup) 65534(nogroup)
可以看到,我们成功地将lizf用户映射成容器里的root用户了。对于gid,也可以做类似的操作。
至此,关于Namespace和Cgroup的知识就讲解完了,可以看到,Namespace和Cgroup的使用是很灵活的,同时这里面又有不少需要注意的地方,因此直接操作Namespace和Cgroup并不是很容易。正是因为这些原因,Docker通过Libcontainer来处理这些底层的事情。这样一来,Docker只需要简单地调用Libcontainer的API,就能将完整的容器搭建起来。而作为Docker的用户,就更不用操心这些事情了,而只需要学习Docker的使用手册,就能通过一两条简单的Docker命令启动容器。转载地址:http://etbcl.baihongyu.com/