Linux名称空间 —— Docker背后的故事

原创 宋宝华 Linux阅码场 2017-08-28

Linuxer征稿以及给作者赠送任意在售技术图书(作者喜欢哪本就赠送哪本)的说明:
Linuxer-"Linux开发者自己的媒体"第二月稿件录取和赠书名单
Linuxer-"Linux开发者自己的媒体"首月稿件录取和赠书名单

Linuxer 《打通Linux脉络》系列之进程管理和调度,4节连续CSDN直播课的说明:
《Linux的进程、线程以及调度》4节系列课报名方法说明


名称空间是在OS之上实现容器与主机隔离,以及容器之间互相隔离的Linux内核核心技术。根据《宋宝华:Docker 最初的2小时(Docker从入门到入门)》一文,名称空间本质上就是在不同的工作组里面封官许愿。本文接下来从细节做一些讨论。
由于本文敲的命令既有可能位于主机,又有可能位于新的名称空间(模拟容器),为了避免搞乱你的脑子,下面主机命令一概采用本颜色,而模拟容器类的命令一概采用本颜色。色盲读者,敬请谅解。

名称空间是什么?
名称空间(Namespace),它表示着一个标识符(identifier)的可见范围。一个标识符可在多个名称空间中定义,它在不同命名空间中的含义是互不相干的。这样,在一个新的名称空间中可定义任何标识符,它们不会与任何已有的标识符发生冲突,只要已有的定义都处于其他命名空间中。再次回忆一下这个封官许愿图,大家都是官:
Linux名称空间 —— Docker背后的故事
名称空间是C++、Java里面常见的概念。比如下面最简单的程序,在2个独立的名称空间里面各自的函数都是叫func(),func就是一个标识符(identifier),可以并存于多个名称空间。

#include<iostream>
usingnamespace std;

// 第一个命名空间
namespace first_space{
   void func(){
     cout <<"Inside first_space"<< endl;
   }
}
// 第二个命名空间
namespace second_space{
   void func(){
     cout <<"Inside second_space"<< endl;
   }
}
int main ()
{

   // 调用第一个命名空间中的函数
  first_space::func();

   // 调用第二个命名空间中的函数
  second_space::func();

   return0;
}

Docker要营造OS级别的虚拟化,需要实现一点,让每个容器都感觉自己拥有整个的独立OS,但是实际上,在Docker下,多个容器实际上是运行于相同的OS内核上面:
Linux名称空间 —— Docker背后的故事
所以,内核需要提供某种意义上的抽象,让各个容器感觉自己拥有独立的OS,让它们自己运行的时候觉得不是在一个整体的OS里面运行,而是各个容器感觉自己独有一个OS,这个OS最好和底层实际的主机资源隔离,才能实现容器运行的平台无关性。这个抽象可以从这几个角度展开:

  • 进程的ID(PID):现在每个容器内部的进程应该拥有独立的PID,不能在同一个OS的一个大池子里面(尽管实际上是,但是在容器内部要意识不到)。典型的,在Linux里面,init进程的PID是1,容器化后,应该每个容器都有一个1以及由1衍生的子进程和子进程的子进程(子子孙孙无穷匮)。但是这个容器内部的1进程,在容器内部它是1,但是最终它肯定是属于底下那个同一个OS大池子里面的某一个PID。
    类似你在上海呼叫电话号码88888888,和在武汉呼叫电话号码88888888,在各自的城市都觉得是88888888,但是在全国(底下唯一的kernel)范围内则分别是021-88888888和027-88888888。
    这种映射关系类似于:
    Linux名称空间 —— Docker背后的故事
  • 进程间通信(IPC):与PID类似,在容器内部的进程间通信应该被从全局的Linux的进程间通信隔离开来。在没有名称空间的情况下,Linux System VIPC都会有各自的ID。譬如:
    
    $ ipcs

------ Shared Memory Segments--------
key shmid owner perms bytes nattch status
0x00000000 524288 baohua 600 524288 2 dest
0x00000000 327681 baohua 600 1048576 2 dest
0x00000000 425986 baohua 600 524288 2 dest

------ Semaphore Arrays --------
key semid owner perms nsems
0x002fa327 0 root 666 2
------ Message Queues --------
key msqid owner perms used-bytes messages

* 主机名称(UTS):要让容器各自感觉独立,那么从底层的主机名独立也是很重要的。比如,我的主机名现在是:

baohua@baohua-VirtualBox:~/develop/linux$hostname
baohua-VirtualBox

而运行的Docker内部的主机名则可以用docker run的-h参数指定,现在我们指定为container:

baohua@baohua-VirtualBox:~/develop/linux$docker run -h container -it --rm ubuntu:14.04 bash
root@container:/# hostname
container

这样容器内部的进程,就不觉得自己在“baohua-VirtualBox”这个机器上面跑。
* 用户(User ):比如我用我的电脑,我是用baohua这个用户名。但是在Docker的容器里面,为了体现虚拟化的概念,容器肯定要和实际的主机分离,这个时候,容器里面应该有自己的用户名。
*

baohua@baohua-VirtualBox:~/develop/linux$docker run -h container -it --rm ubuntu:14.04 bash
root@container:/#

登陆到容器后,我们得到的用户名是root。
看到这个root,我们会疑惑?它是否会拥有类似主机的root权限,比如甚至都可以跑到sysfs里面卸掉一个CPU?这个显然是不可能的:

root@container:/sys/devices/system/cpu/cpu1#sh -c ‘echo 0 > online‘
sh: 1: cannot create online:Read-only file system

因为在容器里面,sysfs都是只读的。实际上,我们并不太希望容器里面控制真实的主机。这个root权限发挥的作用,更多的是在容器内部,它针对虚拟化后的资源,拥有的root权限。譬如,容器启动后,我们在根目录下创建文件1,并且写入hello,之后在容器内创建用户名baohua,以baohua这个用户,再在1里面写入hello就不会有权限。

$ docker run -h container -it --rmubuntu:14.04 bash
root@container:/# touch 1
root@container:/# echo hello >1
root@container:/# useradd baohua
root@container:/# su baohua
baohua@container:/$ echo hello> 1
bash: 1: Permission denied

* 挂载(mount):既然我们强调容器与主机的剥离,我们显然不应该把主机的文件系统暴露给容器内部。众所周知,Linux应用的运行不能没有根文件系统以及proc,sys,dev等特殊的文件系统。所以容器内部也不能不拥有自己的这些文件。但是另外一方面,容器内部看到的东西和主机看到的应该不一样,否则主机就直接暴露给了容器。
Linux的mount名称空间可以实现不同mount 命名空间的进程看到的文件系统层次不一样。也就是说,不同的容器,以及容器与主机之间,可以出现不同目录结构;当然也可以出现相同的目录结构,但是他们在磁盘的位置可以不一样。
![](https://s4.51cto.com/images/blog/202011/30/9fca076b669262689a3917025033ebe2.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
另外一个方面,mount()和umount()系统调用的影响不再是全局的而只影响其调用进程指向的名称空间。
* 网络(network):网络名称空间可以被认为是隔离的拥有单独网络协议资源(网卡、路由转发表、iptables)的环境。以网络端口为例,一个容器里面占据掉的某端口,在另外一个容器里面可以占据同样的端口。
当然新的网络名称空间内,也是需要网卡的,我们一般可以增加一对虚拟网卡veth peer。veth 设备是成对的,把veth中的一个放入名称空间A,另外一个放入名称空间B,则这2个名称空间就可以通过这对veth来通信。当然,这对veth中的一个也可以位于主机。veth的工作原理是:发给veth一端的包可以被另外一个一端收到,这点有些类似具备双向功能的pipe(但是实际pipe是单向的)。
![](https://s4.51cto.com/images/blog/202011/30/e0ba1794cce86ee5aa847ad9143d36a3.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
### Linux如何支持名称空间
地球人都知道,Linux创建一个新的进程可以用fork()、vfork()和clone(),而它们在Linux内核的最底层都九九归一到do_fork()这个函数。
![](https://s4.51cto.com/images/blog/202011/30/6d757dde70dcad86b3ace352382df779.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
其中clone()是最特殊的,因为它可以带上一系列的flags从而实现子进程资源的不同编排。比如pthread_create()创建线程的时候,底层就是调用clone()并通过一系列CLONE flags来表明子进程要继续父进程资源,从而以这种轻量级进程的方式来实现所谓线程的。
clone(child_stack=0xb6cf1424,flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID,…)
为了支持名称空间,Linux有几个flags与之密切相关,它们是CLONE_NEWNS(针对mount)、CLONE_NEWUTS(针对主机名)、CLONE_NEWIPC、CLONE_NEWPID、CLONE_NEWNET、CLONE_NEWUSER。

说明:上面这幅图稍微有点老,目前的Linux,fork应该都是调clone了,只不过flags参数比较简单。

下面我们在一个简单的C程序中逐步增加各种名称空间的支持,这个C程序中,我们用clone()创建一个子进程,并在子进程中调用exec()执行bash shell。在透过clone()创建子进程时,我们通过不同的clone FLAGS来使得子进程拥有独特名称空间。
**第一步,没有新的名称空间**
我们编写一个最简单的程序,通过clone()创建一个子进程,子进程再通过exec()函数族执行bash shell(目的是为了方便在子进程中,运行Linux的命令)

#define _GNU_SOURCE
#include <sched.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdio.h>
#define STACK_SIZE (1024 * 1024)
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
} while (0)

static char child_stack[STACK_SIZE];

int child_main(void *arg)
{
printf("child\n");
execlp("/bin/bash","bash",NULL,NULL);
return1;
}

int main()
{
pid_t child_pid;
child_pid= clone(child_main,child_stack+STACK_SIZE,SIGCHLD,NULL);
if(child_pid == -1)
errExit("clone");
wait(NULL);
return0;
}

我们编译运行它,在子进程的bash shell里面查看自身PID,结果是18374,看起来很正常:

$ gcc main.cbr/>baohua@baohua-VirtualBox:~/develop/training/namespace$./a.out
child
baohua@baohua-VirtualBox:~/develop/training/namespace$echo $$
18374

此时,clone()的时候一个CLONE_NEWxxx都未带,所以父子进程共享一样的名称空间。
![](https://s4.51cto.com/images/blog/202011/30/e1c71bec50c045c1047b9362e333877b.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
**第二步,添加PID名称空间:**
只改一行代码:

@@ -23,7 +23,7 @@ int child_main(void *arg)
int main()
{
pid_t child_pid;

  • child_pid = clone(child_main,child_stack+STACK_SIZE,SIGCHLD,NULL);
  • child_pid = clone(child_main,child_stack+STACK_SIZE,SIGCHLD |CLONE_NEWPID,NULL);

    if (child_pid == -1)
    errExit("clone");

    
    编译后运行:
$ sudo setcap all+eip./a.out
[sudo] password forbaohua:
baohua@baohua-VirtualBox:~/develop/training/namespace$./a.out
child
baohua@baohua-VirtualBox:~/develop/training/namespace$echo $$
1

前面一步setcap的目的是为了给程序执行CLONE_NEWPID的能力。后面echo $$显示的结果是1,子进程的bash shell是新的PID名称空间的init进程。但是在主机环节中,bash的PID是多少呢?运行命令ps --ppid:


$ ps --ppid `pidof a.out`
  PID  TTY         TIME CMD
19094 pts/8    00:00:00 bash

如果这个时候,我们在主机环境进入/proc/19094/ns,会发现其中的pid与主机的其他进程的ns内容不一样。
第三步,mount名称空间:
我们现在增加CLONE_NEWNS,然后mount proc等。修改2行代码:


@@ -16,6 +16,7 @@ static charchild_stack[STACK_SIZE];

 int child_main(void *arg)
 {
       printf("child\n");
+      system("mount -t proc proc /proc");
       execlp("/bin/bash","bash",NULL,NULL);
       return 1;
 }
@@ -23,7 +24,7 @@ int child_main(void *arg)
 int main()
 {
       pid_t child_pid;
-      child_pid = clone(child_main,child_stack+STACK_SIZE,SIGCHLD |CLONE_NEWPID,NULL);
+      child_pid = clone(child_main,child_stack+STACK_SIZE,SIGCHLD |CLONE_NEWPID | CLONE_NEWNS,NULL);

       if (child_pid == -1)
                errExit("clone");
$ sudo ./a.out

然后进入/proc目录查看,里面只有1和20这2个进程。
相反的,主机下的/proc却含有大量的进程:
Linux名称空间 —— Docker背后的故事
下面我们在主机的家目录创建~/test-dir,在里面创建文件1,内容写为hello。并挂载/home/baohua/test-dir到新名称空间的/mnt目录。

$ mkdir ~/test-dir
$ touch ~/test-dir/1
$ echo hello >~/test-file

只需要在child_main()函数里面增加一行代码:

mount("/home/baohua/test-dir","/mnt", "none", MS_BIND, NULL);

执行结果如下,我们现在发现在新的进程的名称空间内/mnt目录下面有文件1,而且内容是hello。

sudo ./a.out
child
root@baohua-VirtualBox:~/develop/training/namespace#cd /mnt/
root@baohua-VirtualBox:/mnt#ls
1
root@baohua-VirtualBox:/mnt#cat 1
hello

而此时转过头来看主机的/mnt的内容完全不同:

baohua@baohua-VirtualBox:/$cd /mnt/
baohua@baohua-VirtualBox:/mnt$ls
hgfs

第四步:网络名称空间
现在在前面程序中clone()的flags增加CLONE_NEWNET,修改1行代码:
@@ -27,7 +26,8 @@ int child_main(void *arg)

 int main()
 {
       pid_t child_pid;
-      child_pid = clone(child_main,child_stack+STACK_SIZE,SIGCHLD |CLONE_NEWPID | CLONE_NEWNS,NULL);
+      child_pid = clone(child_main,child_stack+STACK_SIZE,SIGCHLD |CLONE_NEWPID |
+               CLONE_NEWNS |CLONE_NEWNET,NULL);

在新的进程的名称空间内,运行ifconfig和ip link list,可以说网络环境是十分的单纯:

root@baohua-VirtualBox:~/develop/training/namespace#ifconfig
root@baohua-VirtualBox:~/develop/training/namespace#ip link list
1: lo:<LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default
    link/loopback 00:00:00:00:00:00 brd00:00:00:00:00:00

而主机的确是十分丰富:

baohua@baohua-VirtualBox:~/develop/training/namespace$ifconfig
docker0   Link encap:Ethernet  HWaddr 00:00:00:00:00:00 
          inet addr:172.17.42.1  Bcast:0.0.0.0 Mask:255.255.0.0
          …
eth0      Link encap:Ethernet  HWaddr 00:0c:29:ef:11:2f 
          inet addr:192.168.47.128  Bcast:192.168.47.255  Mask:255.255.255.0
          inet6 addr:fe80::20c:29ff:feef:112f/64 Scope:Link
          …
lo        Link encap:Local Loopback 
          inet addr:127.0.0.1  Mask:255.0.0.0
          inet6 addr: ::1/128 Scope:Host
          …
baohua@baohua-VirtualBox:~/develop/training/namespace$ip link list
1: lo:<LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULTgroup default
    link/loopback 00:00:00:00:00:00 brd00:00:00:00:00:00
2: eth0:<BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP modeDEFAULT group default qlen 1000
    link/ether 00:0c:29:ef:11:2f brdff:ff:ff:ff:ff:ff
3: docker0:<NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWNmode DEFAULT group default
    link/ether 00:00:00:00:00:00 brdff:ff:ff:ff:ff:ff

在新的名称空间中,只有一个loopback设备,这个时候还ping不通127.0.0.1因为它还没有up:

root@baohua-VirtualBox:~/develop/training/namespace#ping 127.0.0.1
connect: Network isunreachable

把它up一下再ping:

root@baohua-VirtualBox:~/develop/training/namespace#ip link set dev lo up
root@baohua-VirtualBox:~/develop/training/namespace#ping 127.0.0.1
PING 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.035 ms
64 bytes from127.0.0.1: icmp_seq=2 ttl=64 time=0.022 ms
^C
--- 127.0.0.1 pingstatistics ---
2 packetstransmitted, 2 received, 0% packet loss, time 999ms
rtt min/avg/max/mdev= 0.022/0.028/0.035/0.008 ms

但是,我们在新的名称空间的loopback设备,和主机里面的loopback其实不是一个loopback。
下面查看a.out子进程bash的PID是21405(接下来添加虚拟网卡的时候需要这个数值):

$ ps --ppid `pidofa.out`
  PID TTY     STAT   TIME COMMAND
19093 pts/8    S     0:00 ./a.out
21405 pts/8    S+    0:00 bash

添加一对虚拟网卡,让新的名称空间可以和主机互联。在主机中敲入如下命令:

$ sudo ip link addname veth0 type veth peer name veth1 netns 21405

上述命令设置了连接的一对虚拟网络设备,它是这么工作的:发送给veth0的数据包将会被veth1收到,发送给veth1数据包将会被veth0收到。
我们进入新的名称空间的bash,敲如下命令,发现新的名称空间里面真的多出来veth1虚拟网卡!

root@baohua-VirtualBox:~/develop/training/namespace#ip link list
1: lo:<LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default
    link/loopback 00:00:00:00:00:00 brd00:00:00:00:00:00
2: veth1: <BROADCAST,MULTICAST> mtu 1500 qdisc noopstate DOWN mode DEFAULT group default qlen 1000
link/ether 3e:7a:86:a3:8b:9d brdff:ff:ff:ff:ff:ff

而主机上面则涌现出了新的veth0网卡:

$ ip link list
1: lo:<LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULTgroup default
    link/loopback 00:00:00:00:00:00 brd00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP>mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
    link/ether 00:0c:29:ef:11:2f brdff:ff:ff:ff:ff:ff
3: docker0:<NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWNmode DEFAULT group default
    link/ether 00:00:00:00:00:00 brdff:ff:ff:ff:ff:ff
24: veth0: <BROADCAST,MULTICAST> mtu 1500 qdiscnoop state DOWN mode DEFAULT group default qlen 1000
    link/etherb2:80:d7:36:b5:84 brd ff:ff:ff:ff:ff:ff

在新名称空间内执行如下命令:

root@baohua-VirtualBox:~/develop/training/namespace#ifconfig veth1 10.1.1.1/24 up

主机上执行如下命令:


$ sudo ifconfig veth010.1.1.2/24 up

而后我们会发现在新名称空间可以ping通10.1.1.2,而主机可以ping通10.1.1.1,这样就实现了双向通信。

第五步:UTS名称空间

下面我们继续安置CLONE_NEWUTS 标记,来实现主机名的分裂。修改2行代码
@@ -19,6 +19,7 @@ int child_main(void *arg)

       printf("child\n");
       system("mount -t proc none /proc");
       mount("/home/baohua/test-dir", "/mnt","none", MS_BIND, NULL);
+      sethostname("container",10);
       execlp("/bin/bash","bash",NULL,NULL);
       return 1;
 }
@@ -27,7 +28,7 @@ int main()
 {
       pid_t child_pid;
       child_pid = clone(child_main,child_stack+STACK_SIZE,SIGCHLD |CLONE_NEWPID |
-               CLONE_NEWNS |CLONE_NEWNET,NULL);
+               CLONE_NEWNS | CLONE_NEWNET |CLONE_NEWUTS, NULL);

       if (child_pid == -1)
                errExit("clone");

编译运行后,在bash中敲hostname命令,获取主机名,变为“container”。

baohua@baohua-VirtualBox:~/develop/training/namespace$sudo ./a.out
[sudo] password forbaohua:
child
root@container:~/develop/training/namespace#hostname
container

第六步:USER名称空间

先看如下最简单的程序,只在clone()时候使用CLONE_NEWUSER:

#define _GNU_SOURCE
#include <sched.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdio.h>

#define STACK_SIZE (1024 * 1024)

#define errExit(msg)    do { perror(msg); exit(EXIT_FAILURE);                         } while (0)

static char child_stack[STACK_SIZE];

static int child_main(void *arg)
{
       printf("child\n");
       execlp("/bin/bash","bash",NULL,NULL);
       return1;
}

int main()
{
       pid_tchild_pid;

      child_pid= clone(child_main,child_stack+STACK_SIZE,SIGCHLD | CLONE_NEWUSER, NULL);
       if(child_pid == -1)
              errExit("clone");
       wait(NULL);
       return0;
}

它的运行结果如下,看起来在子进程里面(新的名称空间里面),我们得到的用户是nobody:

baohua@baohua-VirtualBox:~/develop/training/namespace$gcc user.c
baohua@baohua-VirtualBox:~/develop/training/namespace$./a.out
child
nobody@baohua-VirtualBox:~/develop/training/namespace$

在子进程对应的shell里面,敲id命令,看一下自身的ID,发现都是

nobody@baohua-VirtualBox:~/develop/training/namespace$id
uid=65534(nobody)gid=65534(nogroup) groups=65534(nogroup)

clone()用了CLONE_NEWUSER的参数后,子进程运行于新的USER名称空间,内部看到的UID和GID已经与外部不同了,在默认情况下以ID 65534运行。
其实我们可以把主机的ID,与新USER名称空间的ID进行一个映射,比如我们启动子进程的时候,实际上是以baohua这个用户启动的,则说明bash子进程,在主机对应的用户是baohua。但是,在新的名称空间内部,它究竟映射到哪个用户呢?这个我们可以通过修改进程的/proc/pid/uid_map和/proc/pid/gid_map这2个文件来进行ID的内外映射。
主机里面baohua的ID是1000:

$ id baohua
uid=1000(baohua)gid=1000(baohua) groups=1000(baohua),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),108(lpadmin),124(sambashare),131(docker)

我们现在把uid 1000对应的baohua映射到新名称空间内部的root用户(uid为0),在主机中运行如下命令:

$ ps --ppid `pidofa.out`
  PID TTY          TIME CMD
27321 pts/6    00:00:00 bash
我们手动进行映射:
$ sudo sh -c ‘echo 01000 1 > /proc/27321/uid_map‘
$ sudo sh -c ‘echo 01000 1 > /proc/27321/gid_map‘

之后在子进程再次敲id命令,发现重大不同。

nobody@baohua-VirtualBox:~/develop/training/namespace$id
uid=0(root)gid=0(root) groups=0(root),65534(nogroup)

发现自身的uid、gid变为了0。接下来,只用su -就可以让shell显示root@。
下面我们用程序实现这个过程:

#define _GNU_SOURCE
#include <sched.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/mount.h>
#include <signal.h>
#include <stdio.h>

#define STACK_SIZE (1024 * 1024)

#define errExit(msg)    do {perror(msg); exit(EXIT_FAILURE);                         } while (0)

static char child_stack[STACK_SIZE];

static void set_map(char* file, intinside_id, int outside_id)
{
       FILE*mapfd = fopen(file, "w");
       if(NULL == mapfd) {
              perror("openfile error");
              return;
       }
       fprintf(mapfd,"%d %d %d", inside_id, outside_id, 1);
       fclose(mapfd);
}

static void set_uid_map(pid_t pid, intinside_id, int outside_id)
{
       charfile[256];
       sprintf(file,"/proc/%d/uid_map", pid);
       set_map(file,inside_id, outside_id);
}

static void set_gid_map(pid_t pid, intinside_id, int outside_id)
{
       charfile[256];
       sprintf(file,"/proc/%d/gid_map", pid);
       set_map(file,inside_id, outside_id);
}

static int child_main(void *arg)
{
       sleep(1);//wait for 1 second to make certain uid_map and gid_map is written
       printf("child\n");
       system("mount-t proc none /proc");
       mount("/home/baohua/test-dir","/mnt", "none", MS_BIND, NULL);
       sethostname("container",10);
       execlp("/bin/bash","bash",NULL,NULL);
       return1;
}

int main()
{
       pid_tchild_pid;

       child_pid= clone(child_main,child_stack+STACK_SIZE,SIGCHLD | CLONE_NEWPID |
                     CLONE_NEWNS| CLONE_NEWNET | CLONE_NEWUTS | CLONE_NEWUSER, NULL);
       if(child_pid == -1)
              errExit("clone");

       set_uid_map(child_pid,0, getuid());
       set_gid_map(child_pid,0, getgid());
       wait(NULL);
       return0;
}

上述代码中,父进程会通过set_uid_map()和set_gid_map()这2个函数,进行新名称空间内部的用户0与主机的用户1000的映射。由于子进程执行bash之前延迟了1秒,所以我们在子进程进入shell的时候,它已经直接是root用户了:

$ ./a.out
child
root@container:~/develop/training/namespace#

那么,它针对主机资源的实际权限是不是root呢,实验一下它是否可以访问/dev/sda1:

root@container:~/develop/training/namespace#cat /dev/sda1
cat: /dev/sda1:Permission denied

下面我们在bash里面启动一个stress进程:

root@container:~/develop/training/namespace#stress --cpu 8 --io 4 --vm 2 --vm-bytes 128M --timeout 100000s&
[1] 46

直接在新名称空间内看ps:
Linux名称空间 —— Docker背后的故事

但是我们在主机里面看ps呢?
Linux名称空间 —— Docker背后的故事
我们则发现,所有的stress进程都是对应用户baohua的。
所以这个关系类似:
Linux名称空间 —— Docker背后的故事

Linux名称空间 —— Docker背后的故事

上一篇:最简单Linux内核和驱动考试题(含答案与最详细解析)


下一篇:跨私网传输用法解析