Linux namespace - Docker 背後的故事

名稱空間是在OS之上實現容器與主機隔離,以及容器之間互相隔離的Linux內核核心技術。根據《Docker 最初的2小時(Docker從入門到入門)》一文,名稱空間本質上就是在不同的工作組裏面封官許願,讓大家在各自的部門裏面都是manager,而且彼此不衝突。本文接下來從細節做一些討論。

由於本文敲的命令既有可能位於主機,又有可能位於新的名稱空間(模擬容器),爲了避免搞亂你的腦子,下面主機命令一概採用本顏色,而模擬容器類的命令一概採用本顏色。色盲讀者,敬請諒解。

名稱空間是什麼?

名稱空間(Namespace),它表示着一個標識符(identifier)的可見範圍。一個標識符可在多個名稱空間中定義,它在不同命名空間中的含義是互不相干的。這樣,在一個新的名稱空間中可定義任何標識符,它們不會與任何已有的標識符發生衝突,只要已有的定義都處於其他命名空間中。再次回憶一下這個封官許願圖,大家都是官:

名稱空間是C++、Java裏面常見的概念。比如下面最簡單的程序,在2個獨立的名稱空間裏面各自的函數都是叫func(),func就是一個標識符(identifier),可以並存於多個名稱空間。

 


 
  1. #include <iostream>

  2. using namespace std;

  3.  
  4. // 第一個命名空間

  5. namespace first_space{

  6. void func(){

  7. cout << "Inside first_space" << endl;

  8. }

  9. }

  10. // 第二個命名空間

  11. namespace second_space{

  12. void func(){

  13. cout << "Inside second_space" << endl;

  14. }

  15. }

  16. int main ()

  17. {

  18.  
  19. // 調用第一個命名空間中的函數

  20. first_space::func();

  21.  
  22. // 調用第二個命名空間中的函數

  23. second_space::func();

  24.  
  25. return 0;

  26. }

Docker要營造OS級別的虛擬化,需要實現一點,讓每個容器都感覺自己擁有整個的獨立OS,但是實際上,在Docker下,多個容器實際上是運行於相同的OS內核上面:

所以,內核需要提供某種意義上的抽象,讓各個容器感覺自己擁有獨立的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。

這種映射關係類似於:

進程間通信(IPC)

與PID類似,在容器內部的進程間通信應該被從全局的Linux的進程間通信隔離開來。在沒有名稱空間的情況下,Linux System V IPC都會有各自的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

但是在各個容器內部,ID與ID之間應該互相隔離。容器內部應該看不到主機的IPC,而一個容器也看不到另外一個容器的IPC。譬如在這臺主機上跑Ubuntu 14.04的bash,目前還沒有發現IPC:

baohua@baohua-VirtualBox:~$ docker run -it --rm ubuntu:14.04 bash
root@0c7951083f70:/# ipcs
------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status      
------ Semaphore Arrays --------
key        semid      owner      perms      nsems     
------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    

主機名稱(UTS)

要讓容器各自感覺獨立,那麼從底層的主機名獨立也是很重要的。比如,我的主機名現在可以通過hostname命令獲取:

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”這個機器上面跑。

如果我們docker run中不指定hostname,會有一個隨機分配的數值做hostname:

baohua@baohua-VirtualBox:~$ docker run -it --rm ubuntu:14.04 bash
root@0c7951083f70:/# hostname
0c7951083f70

用戶(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權限,比如可以在容器內部執行mount。

下面我們驗證容器內部的root權限的作用:容器啓動後,我們在根目錄下創建文件1,並且在其中寫入hello,之後在容器內創建用戶名baohua,以baohua這個用戶,再在1裏面寫入hello就不會有權限:

$ docker run -h container -it --rm ubuntu: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 命名空間的進程看到的文件系統層次不一樣。也就是說,不同的容器,以及容器與主機之間,可以出現不同目錄結構;當然也可以出現相同的目錄結構,但是他們在磁盤的位置可以不一樣。

 

 

另外一個方面,mount()和umount()系統調用的影響不再是全局的而隻影響其調用進程指向的名稱空間。所以容器A裏面mount了xxx到目錄yyy,容器B也看不見,當然主機的yyy 目錄也不會指向xxx。

網絡(network)

網絡名稱空間可以被認爲是隔離的擁有單獨網絡協議資源(網卡、路由轉發表、iptables)的環境,比如在容器裏面可以看到獨立的虛擬網卡。以網絡端口爲例,一個容器裏面佔據掉的某端口,在另外一個容器裏面可以佔據同樣的端口,因爲理念上,它們擁有的網卡、IP都是可以不同的。

當然新的網絡名稱空間內,也是需要網卡的,我們一般可以增加一對虛擬網卡veth peer。veth (虛擬以太網)設備是成對的,把veth中的一個放入名稱空間A,另外一個放入名稱空間B,則這2個名稱空間就可以通過這對veth來通信。當然,這對veth中的一個也可以位於主機。veth的工作原理是:發給veth一端的包可以被另外一個一端收到,這點有些類似具備雙向功能的pipe(但是實際pipe是單向的)。

Linux如何支持名稱空間

地球人都知道,Linux創建一個新的進程可以用fork()、vfork()和clone(),而它們在Linux內核的最底層都九九歸一到do_fork()這個函數。

 

其中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內核增加了幾個CLONE_NEW開頭的flag,它們是CLONE_NEWNS(針對mount)、CLONE_NEWUTS(針對主機名)、CLONE_NEWIPC、CLONE_NEWPID、CLONE_NEWNET、CLONE_NEWUSER,當我們執行clone()時候,每多帶上一個flag,就表明相應的名稱空間會多創建一份,體現“NEW”這個單詞的含義。
下面我們在一個簡單的C程序中逐步增加各種名稱空間的支持,這個C程序中,我們用clone()創建一個子進程,並在子進程中調用exec()執行bash shell。在透過clone()創建子進程時,我們通過不同的clone FLAGS來使得子進程擁有獨特名稱空間。

第一步,無新名稱空間

我們編寫一個最簡單的程序,通過clone()創建一個子進程,子進程再通過exec()函數族執行bash shell(目的是爲了方便在子進程中,運行Linux的命令)


 
  1. #define _GNU_SOURCE

  2. #include <sched.h>

  3. #include <unistd.h>

  4. #include <stdlib.h>

  5. #include <sys/wait.h>

  6. #include <signal.h>

  7. #include <stdio.h>

  8.  
  9. #define STACK_SIZE (1024 * 1024)

  10.  
  11. #define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \

  12. } while (0)

  13.  
  14. static char child_stack[STACK_SIZE];

  15.  
  16. int child_main(void *arg)

  17. {

  18. printf("child\n");

  19. execlp("/bin/bash","bash",NULL,NULL);

  20. return 1;

  21. }

  22.  
  23. int main()

  24. {

  25. pid_t child_pid;

  26. child_pid = clone(child_main,child_stack+STACK_SIZE,SIGCHLD,NULL);

  27.  
  28. if (child_pid == -1)

  29. errExit("clone");

  30.  
  31.  
  32. wait(NULL);

  33.  
  34. return 0;

  35. }

我們編譯運行它,在子進程的bash shell裏面查看自身PID,結果是18374,看起來很正常:

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

此時,clone()的時候一個CLONE_NEWxxx都未帶,所以父子進程共享一樣的名稱空間。

第二步,添加PID名稱空間:

只改一行代碼:


 
  1. @@ -23,7 +23,7 @@ int child_main(void *arg)

  2. int main()

  3. {

  4. pid_t child_pid;

  5. - child_pid = clone(child_main,child_stack+STACK_SIZE,SIGCHLD,NULL);

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

  7.  
  8. if (child_pid == -1)

  9. errExit("clone");

編譯後運行:

$ sudo setcap all+eip ./a.out
[sudo] password for baohua:
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`
  PIDTTY          TIME CMD
19094 pts/8    00:00:00 bash

如果這個時候,我們在主機環境進入/proc/19094/ns,會發現其中的pid與主機的其他進程的ns內容不一樣。

第三步,mount名稱空間:

我們現在增加CLONE_NEWNS,然後mount proc等。修改2行代碼:


 
  1. @@ -16,6 +16,7 @@ static char child_stack[STACK_SIZE];

  2. int child_main(void *arg)

  3. {

  4. printf("child\n");

  5. + system("mount -t proc proc /proc");

  6. execlp("/bin/bash","bash",NULL,NULL);

  7. return 1;

  8. }

  9. @@ -23,7 +24,7 @@ int child_main(void *arg)

  10. int main()

  11. {

  12. pid_t child_pid;

  13. - child_pid = clone(child_main,child_stack+STACK_SIZE,SIGCHLD | CLONE_NEWPID,NULL);

  14. + child_pid = clone(child_main,child_stack+STACK_SIZE,SIGCHLD | CLONE_NEWPID | CLONE_NEWNS,NULL);

  15.  
  16. if (child_pid == -1)

  17. errExit("clone");

運行之,進入bash子進程看proc目錄,發現只有1和20兩個進程

相反的,主機下的/proc卻含有大量的進程:

/proc中內容的不一樣,自然會導致在主機和新名稱空間內部敲ps、top這種命令看到的結果不一樣,因爲此類命令就是通過讀/proc中的數據來實現的。

下面我們在主機的家目錄(/home/baohua)創建~/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的內容完全不同(還是老樣子,並沒有因爲新的名稱空間內部有重新mount /mnt而發生變化):

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

第四步:網絡名稱空間

現在在前面程序中clone()的flags增加CLONE_NEWNET,修改1行代碼:


 
  1. @@ -27,7 +26,8 @@ int child_main(void *arg)

  2. int main()

  3. {

  4. pid_t child_pid;

  5. - child_pid = clone(child_main,child_stack+STACK_SIZE,SIGCHLD |CLONE_NEWPID | CLONE_NEWNS,NULL);

  6. + child_pid = clone(child_main,child_stack+STACK_SIZE,SIGCHLD |CLONE_NEWPID |

  7. + 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 qdiscnoop state DOWN mode DEFAULT group default
   link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
而主機裏面由於存在真實網卡等,內容十分豐富:

baohua@baohua-VirtualBox:~/develop/training/namespace$ifconfig
docker0  Link encap:Ethernet  HWaddr00: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  HWaddr00: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> mtu65536 qdisc noqueue state UNKNOWN mode DEFAULT group default
   link/loopback 00:00:00:00:00:00 brd 00: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 brd ff: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 brd ff: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 is unreachable

把它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 ofdata.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64time=0.035 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64time=0.022 ms
^C
--- 127.0.0.1 ping statistics ---
2 packets transmitted, 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 `pidof a.out`
  PIDTTY      STAT   TIME COMMAND
19093 pts/8    S     0:00 ./a.out
21405 pts/8    S+    0:00 bash

添加一對虛擬網卡,讓新的名稱空間可以和主機互聯。在主機中敲入如下命令:

$ sudo ip link add name veth0 type vethpeer name veth1 netns 21405

上述命令設置了連接的一對虛擬網絡設備,它是這麼工作的:發送給veth0的數據包將會被veth1收到,發送給veth1數據包將會被veth0收到。

我們進入新的名稱空間的bash,敲如下命令,發現新的名稱空間裏面真的多出來veth1虛擬網卡!

root@baohua-VirtualBox:~/develop/training/namespace#ip link list

1: lo: <LOOPBACK> mtu 65536 qdiscnoop state DOWN mode DEFAULT group default

   link/loopback 00:00:00:00:00:00 brd 00: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> mtu65536 qdisc noqueue state UNKNOWN mode DEFAULT group default

   link/loopback 00:00:00:00:00:00 brd 00: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 brd ff: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 brd ff:ff:ff:ff:ff:ff

24: veth0: <BROADCAST,MULTICAST> mtu 1500 qdisc noopstate 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 veth0 10.1.1.2/24 up

而後我們會發現在新名稱空間可以ping通10.1.1.2,而主機可以ping通10.1.1.1,這樣就實現了雙向通信。

第五步:UTS名稱空間

下面我們繼續安置CLONE_NEWUTS 標記,來實現主機名的分裂。修改2行代碼

 


 
  1. @@ -19,6 +19,7 @@ int child_main(void *arg)

  2. printf("child\n");

  3. system("mount -t proc none /proc");

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

  5. + sethostname("container",10);

  6. execlp("/bin/bash","bash",NULL,NULL);

  7. return 1;

  8. }

  9. @@ -27,7 +28,7 @@ int main()

  10. {

  11. pid_t child_pid;

  12. child_pid = clone(child_main,child_stack+STACK_SIZE,SIGCHLD |CLONE_NEWPID |

  13. - CLONE_NEWNS |CLONE_NEWNET,NULL);

  14. + CLONE_NEWNS | CLONE_NEWNET |CLONE_NEWUTS, NULL);

  15.  
  16. if (child_pid == -1)

  17. 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:


 
  1. #define _GNU_SOURCE

  2. #include <sched.h>

  3. #include <unistd.h>

  4. #include <stdlib.h>

  5. #include <sys/wait.h>

  6. #include <signal.h>

  7. #include <stdio.h>

  8.  
  9. #define STACK_SIZE (1024 * 1024)

  10.  
  11. #define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \

  12. } while (0)

  13.  
  14. static char child_stack[STACK_SIZE];

  15.  
  16. static int child_main(void *arg)

  17. {

  18. printf("child\n");

  19. execlp("/bin/bash","bash",NULL,NULL);

  20. return1;

  21. }

  22.  
  23. int main()

  24. {

  25. pid_tchild_pid;

  26.  
  27. child_pid= clone(child_main,child_stack+STACK_SIZE,SIGCHLD | CLONE_NEWUSER, NULL);

  28. if(child_pid == -1)

  29. errExit("clone");

  30.  
  31. wait(NULL);

  32.  
  33. return0;

  34. }

它的運行結果如下,看起來在子進程裏面(新的名稱空間裏面),我們得到的用戶是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,發現都是65534:

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@。

下面我們用程序實現這個過程:

 


 
  1. #define _GNU_SOURCE

  2. #include <sched.h>

  3. #include <unistd.h>

  4. #include <stdlib.h>

  5. #include <sys/wait.h>

  6. #include <sys/mount.h>

  7. #include <signal.h>

  8. #include <stdio.h>

  9.  
  10. #define STACK_SIZE (1024 * 1024)

  11.  
  12. #define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \

  13. } while (0)

  14.  
  15. static char child_stack[STACK_SIZE];

  16.  
  17. static void set_map(char* file, intinside_id, int outside_id)

  18. {

  19. FILE*mapfd = fopen(file, "w");

  20. if(NULL == mapfd) {

  21. perror("openfile error");

  22. return;

  23. }

  24. fprintf(mapfd,"%d %d %d", inside_id, outside_id, 1);

  25. fclose(mapfd);

  26. }

  27.  
  28. static void set_uid_map(pid_t pid, int inside_id,int outside_id)

  29. {

  30. charfile[256];

  31. sprintf(file,"/proc/%d/uid_map", pid);

  32. set_map(file,inside_id, outside_id);

  33. }

  34.  
  35. static void set_gid_map(pid_t pid, intinside_id, int outside_id)

  36. {

  37. charfile[256];

  38. sprintf(file,"/proc/%d/gid_map", pid);

  39. set_map(file,inside_id, outside_id);

  40. }

  41.  
  42. static int child_main(void *arg)

  43. {

  44. sleep(1);//wait for 1 second to make certain uid_map and gid_map is written

  45. printf("child\n");

  46. system("mount-t proc none /proc");

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

  48. sethostname("container",10);

  49. execlp("/bin/bash","bash",NULL,NULL);

  50. return1;

  51. }

  52.  
  53. int main()

  54. {

  55. pid_tchild_pid;

  56.  
  57. child_pid= clone(child_main,child_stack+STACK_SIZE,SIGCHLD | CLONE_NEWPID |

  58. CLONE_NEWNS| CLONE_NEWNET | CLONE_NEWUTS | CLONE_NEWUSER, NULL);

  59. if(child_pid == -1)

  60. errExit("clone");

  61.  
  62. set_uid_map(child_pid,0, getuid());

  63. set_gid_map(child_pid,0, getgid());

  64.  
  65. wait(NULL);

  66.  
  67. return0;

  68. }

上述代碼中,父進程會通過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:

 

但是我們在主機裏面看ps呢?

我們則發現,所有的stress進程在主機裏面都是對應用戶baohua的,而在新的名稱空間裏面則是root。

所以這個關係類似:

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章