大話Docker(五):Linux Namespace(下)


大話Docker(四):Linux Namespace(上) (可點(diǎn)擊) 中我們了解了,UTD、IPC、PID、Mount 四個(gè)namespace,我們模仿 Docker 做了一個(gè)相當(dāng)相當(dāng)山寨的鏡像。在這一篇中,主要想向大家介紹 Linux 的 User 和 Network 的 Namespace。

下面我們就介紹一下還剩下的這兩個(gè)Namespace。
User Namespace
User Namespace 主要是用了 CLONE_NEWUSER 的參數(shù)。使用了這個(gè)參數(shù)后,內(nèi)部看到的 UID 和 GID 已經(jīng)與外部不同了,默認(rèn)顯示為65534。那是因?yàn)槿萜髡也坏狡湔嬲?UID 所以,設(shè)置上了最大的 UID(其設(shè)置定義在/proc/sys/kernel/overflowuid)。
要把容器中的uid和真實(shí)系統(tǒng)的 UID 給映射在一起,需要修改 /proc/<pid>/uid_map/proc/<pid>/gid_map 這兩個(gè)文件。這兩個(gè)文件的格式為:
**ID-inside-ns ID-outside-ns length**
其中:

  • 第一個(gè)字段ID-inside-ns表示在容器顯示的UID或GID,
  • 第二個(gè)字段ID-outside-ns表示容器外映射的真實(shí)的UID或GID。
  • 第三個(gè)字段表示映射的范圍,一般填1,表示一一對(duì)應(yīng)。
    比如,把真實(shí)的uid=1000映射成容器內(nèi)的uid=0:
$ cat /proc/2465/uid_map         0       1000          1

再比如下面的示例:表示把namespace內(nèi)部從0開(kāi)始的uid映射到外部從0開(kāi)始的uid,其最大范圍是無(wú)符號(hào)32位整形:

$ cat /proc/$$/uid_map         0          0          4294967295

另外,需要注意的是:

  • 寫(xiě)這兩個(gè)文件的進(jìn)程需要這個(gè)namespace中的CAP_SETUID (CAP_SETGID)權(quán)限
  • 寫(xiě)入的進(jìn)程必須是此user namespace的父或子的user namespace進(jìn)程。
  • 另外需要滿如下條件之一:1)父進(jìn)程將effective uid/gid映射到子進(jìn)程的user namespace中,2)父進(jìn)程如果有CAP_SETUID/CAP_SETGID權(quán)限,那么它將可以映射到父進(jìn)程中的任一uid/gid。
    這些規(guī)則看著都煩,我們來(lái)看程序吧:
#define _GNU_SOURCE#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <sys/wait.h>#include <sys/mount.h>#include <sys/capability.h>#include <stdio.h>#include <sched.h>#include <signal.h>#include <unistd.h>#define STACK_SIZE (1024 * 1024)static char container_stack[STACK_SIZE];
char* const container_args[] = {    "/bin/bash",
    NULL
};
int pipefd[2];
void set_map(char* file, int inside_id, int outside_id, int len) {
    FILE* mapfd = fopen(file, "w");    if (NULL == mapfd) {
        perror("open file error");            return;
    }
    fprintf(mapfd, "%d %d %d", inside_id, outside_id, len);
    fclose(mapfd);
}
void set_uid_map(pid_t pid, int inside_id, int outside_id, int len) {
    char file[256];
    sprintf(file, "/proc/%d/uid_map", pid);        set_map(file, inside_id, outside_id, len);
}
void set_gid_map(pid_t pid, int inside_id, int outside_id, int len) {
    char file[256];
    sprintf(file, "/proc/%d/gid_map", pid);        set_map(file, inside_id, outside_id, len);
}
int container_main(void* arg)
{        printf("Container [%5d] - inside the container!\n", getpid());           printf("Container: eUID = %ld;  eGID = %ld, UID=%ld, GID=%ld\n",
            (long) geteuid(), (long) getegid(), (long) getuid(), (long) getgid());
    /* 等待父進(jìn)程通知后再往下執(zhí)行(進(jìn)程間的同步) */
    char ch;
    close(pipefd[1]);        read(pipefd[0], &ch, 1);            printf("Container [%5d] - setup hostname!\n", getpid());
    //set hostname
    sethostname("container",10);
    //remount "/proc" to make sure the "top" and "ps" show container's information
    mount("proc", "/proc", "proc", 0, NULL);

    execv(container_args[0], container_args);
    printf("Something's wrong!\n");
    return 1;
}
int main()
{
    const int gid=getgid(), uid=getuid();

    printf("Parent: eUID = %ld;  eGID = %ld, UID=%ld, GID=%ld\n",
            (long) geteuid(), (long) getegid(), (long) getuid(), (long) getgid());
    pipe(pipefd);
    printf("Parent [%5d] - start a container!\n", getpid());
    int container_pid = clone(container_main, container_stack+STACK_SIZE, 
            CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWUSER | SIGCHLD, NULL);
    printf("Parent [%5d] - Container [%5d]!\n", getpid(), container_pid);
    //To map the uid/gid, 
    //   we need edit the /proc/PID/uid_map (or /proc/PID/gid_map) in parent
    //The file format is
    //   ID-inside-ns   ID-outside-ns   length
    //if no mapping, 
    //   the uid will be taken from /proc/sys/kernel/overflowuid
    //   the gid will be taken from /proc/sys/kernel/overflowgid
    set_uid_map(container_pid, 0, uid, 1);
    set_gid_map(container_pid, 0, gid, 1);
    printf("Parent [%5d] - user/group mapping done!\n", getpid());
    /* 通知子進(jìn)程 */
    close(pipefd[1]);
    waitpid(container_pid, NULL, 0);
    printf("Parent - container stopped!\n");
    return 0;
}

上面的程序,我們用了一個(gè)pipe來(lái)對(duì)父子進(jìn)程進(jìn)行同步,為什么要這樣做?因?yàn)樽舆M(jìn)程中有一個(gè) execv 的系統(tǒng)調(diào)用,這個(gè)系統(tǒng)調(diào)用會(huì)把當(dāng)前子進(jìn)程的進(jìn)程空間給全部覆蓋掉,我們希望在 execv 之前就做好 user namespace 的 uid/gid 的映射,這樣,execv 運(yùn)行的 /bin/bash就會(huì)因?yàn)槲覀冊(cè)O(shè)置了 uid 為0的 inside-uid 而變成#號(hào)的提示符。
整個(gè)程序的運(yùn)行效果如下:

hchen@ubuntu:~$ id
uid=1000(hchen) gid=1000(hchen) groups=1000(hchen)
hchen@ubuntu:~$ ./user #<--以hchen用戶運(yùn)行Parent: eUID = 1000;  eGID = 1000, UID=1000, GID=1000 Parent [ 3262] - start a container!
Parent [ 3262] - Container [ 3263]!
Parent [ 3262] - user/group mapping done!
Container [    1] - inside the container!
Container: eUID = 0;  eGID = 0, UID=0, GID=0 #<---Container里的UID/GID都為0了Container [    1] - setup hostname!
root@container:~# id #<----我們可以看到容器里的用戶和命令行提示符是root用戶了uid=0(root) gid=0(root) groups=0(root),65534(nogroup)

雖然容器里是root,但其實(shí)這個(gè)容器的/bin/bash進(jìn)程是以一個(gè)普通用戶hchen來(lái)運(yùn)行的。這樣一來(lái),我們?nèi)萜鞯陌踩詴?huì)得到提高。
我們注意到,User Namespace 是以普通用戶運(yùn)行,但是別的 Namespace 需要root權(quán)限,那么,如果我要同時(shí)使用多個(gè) Namespace,該怎么辦呢?一般來(lái)說(shuō),我們先用一般用戶創(chuàng)建User Namespace,然后把這個(gè)一般用戶映射成root,在容器內(nèi)用root來(lái)創(chuàng)建其它的 Namesapce。
Network Namespace
Network的Namespace比較啰嗦。在Linux下,我們一般用ip命令創(chuàng)建Network Namespace(Docker的源碼中,它沒(méi)有用ip命令,而是自己實(shí)現(xiàn)了ip命令內(nèi)的一些功能——是用了Raw Socket發(fā)些“奇怪”的數(shù)據(jù),呵呵)。這里,我還是用ip命令講解一下。
首先,我們先看個(gè)圖,下面這個(gè)圖基本上就是Docker在宿主機(jī)上的網(wǎng)絡(luò)示意圖(其中的物理網(wǎng)卡并不準(zhǔn)確,因?yàn)閐ocker可能會(huì)運(yùn)行在一個(gè)VM中,所以,這里所謂的“物理網(wǎng)卡”其實(shí)也就是一個(gè)有可以路由的IP的網(wǎng)卡)

上圖中,Docker使用了一個(gè)私有網(wǎng)段,172.40.1.0,docker還可能會(huì)使用10.0.0.0和192.168.0.0這兩個(gè)私有網(wǎng)段,關(guān)鍵看你的路由表中是否配置了,如果沒(méi)有配置,就會(huì)使用,如果你的路由表配置了所有私有網(wǎng)段,那么docker啟動(dòng)時(shí)就會(huì)出錯(cuò)了。
當(dāng)你啟動(dòng)一個(gè)Docker容器后,你可以使用ip link show或ip addr show來(lái)查看當(dāng)前宿主機(jī)的網(wǎng)絡(luò)情況(我們可以看到有一個(gè)docker0,還有一個(gè)veth22a38e6的虛擬網(wǎng)卡——給容器用的):

hchen@ubuntu:~$ ip link show1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state ... 
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:002: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc ...
    link/ether 00:0c:29:b7:67:7d brd ff:ff:ff:ff:ff:ff3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 ...
    link/ether 56:84:7a:fe:97:99 brd ff:ff:ff:ff:ff:ff5: veth22a38e6: <BROADCAST,UP,LOWER_UP> mtu 1500 qdisc ...
    link/ether 8e:30:2a:ac:8c:d1 brd ff:ff:ff:ff:ff:ff

那么,要做成這個(gè)樣子應(yīng)該怎么辦呢?我們來(lái)看一組命令:

## 首先,我們先增加一個(gè)網(wǎng)橋lxcbr0,模仿docker0brctl addbr lxcbr0
brctl stp lxcbr0 off
ifconfig lxcbr0 192.168.10.1/24 up #為網(wǎng)橋設(shè)置IP地址## 接下來(lái),我們要?jiǎng)?chuàng)建一個(gè)network namespace - ns1# 增加一個(gè)namesapce 命令為 ns1 (使用ip netns add命令)ip netns add ns1 
# 激活namespace中的loopback,即127.0.0.1(使用ip netns exec ns1來(lái)操作ns1中的命令)ip netns exec ns1   ip link set dev lo up 
## 然后,我們需要增加一對(duì)虛擬網(wǎng)卡# 增加一個(gè)pair虛擬網(wǎng)卡,注意其中的veth類(lèi)型,其中一個(gè)網(wǎng)卡要按進(jìn)容器中ip link add veth-ns1 type veth peer name lxcbr0.1# 把 veth-ns1 按到namespace ns1中,這樣容器中就會(huì)有一個(gè)新的網(wǎng)卡了ip link set veth-ns1 netns ns1# 把容器里的 veth-ns1改名為 eth0 (容器外會(huì)沖突,容器內(nèi)就不會(huì)了)ip netns exec ns1  ip link set dev veth-ns1 name eth0 
# 為容器中的網(wǎng)卡分配一個(gè)IP地址,并激活它ip netns exec ns1 ifconfig eth0 192.168.10.11/24 up# 上面我們把veth-ns1這個(gè)網(wǎng)卡按到了容器中,然后我們要把lxcbr0.1添加上網(wǎng)橋上brctl addif lxcbr0 lxcbr0.1# 為容器增加一個(gè)路由規(guī)則,讓容器可以訪問(wèn)外面的網(wǎng)絡(luò)ip netns exec ns1     ip route add default via 192.168.10.1# 在/etc/netns下創(chuàng)建network namespce名稱(chēng)為ns1的目錄,# 然后為這個(gè)namespace設(shè)置resolv.conf,這樣,容器內(nèi)就可以訪問(wèn)域名了mkdir -p /etc/netns/ns1echo "nameserver 8.8.8.8" > /etc/netns/ns1/resolv.conf

上面基本上就是docker網(wǎng)絡(luò)的原理了,只不過(guò),

  • Docker的resolv.conf沒(méi)有用這樣的方式,而是用了上篇中的Mount Namesapce的那種方式 (可點(diǎn)擊)
  • 另外,docker是用進(jìn)程的PID來(lái)做Network Namespace的名稱(chēng)的。
    了解了這些后,你甚至可以為正在運(yùn)行的docker容器增加一個(gè)新的網(wǎng)卡:
ip link add peerA type veth peer name peerBbrctl addif docker0 peerA 
ip link set peerA up 
ip link set peerB netns ${container-pid} ip netns exec ${container-pid} ip link set dev peerB name eth1 
ip netns exec ${container-pid} ip link set eth1 up ; 
ip netns exec ${container-pid} ip addr add ${ROUTEABLE_IP} dev eth1 ;

上面的示例是我們?yōu)檎谶\(yùn)行的docker容器,增加一個(gè)eth1的網(wǎng)卡,并給了一個(gè)靜態(tài)的可被外部訪問(wèn)到的IP地址。
這個(gè)需要把外部的“物理網(wǎng)卡”配置成混雜模式,這樣這個(gè)eth1網(wǎng)卡就會(huì)向外通過(guò)ARP協(xié)議發(fā)送自己的Mac地址,然后外部的交換機(jī)就會(huì)把到這個(gè)IP地址的包轉(zhuǎn)到“物理網(wǎng)卡”上,因?yàn)槭腔祀s模式,所以eth1就能收到相關(guān)的數(shù)據(jù),一看,是自己的,那么就收到。這樣,Docker容器的網(wǎng)絡(luò)就和外部通了。
當(dāng)然,無(wú)論是Docker的NAT方式,還是混雜模式都會(huì)有性能上的問(wèn)題,NAT不用說(shuō)了,存在一個(gè)轉(zhuǎn)發(fā)的開(kāi)銷(xiāo),混雜模式呢,網(wǎng)卡上收到的負(fù)載都會(huì)完全交給所有的虛擬網(wǎng)卡上,于是就算一個(gè)網(wǎng)卡上沒(méi)有數(shù)據(jù),但也會(huì)被其它網(wǎng)卡上的數(shù)據(jù)所影響。
這兩種方式都不夠完美,我們知道,真正解決這種網(wǎng)絡(luò)問(wèn)題需要使用VLAN技術(shù),于是Google的同學(xué)們?yōu)長(zhǎng)inux內(nèi)核實(shí)現(xiàn)了一個(gè)IPVLAN的驅(qū)動(dòng),這基本上就是為Docker量身定制的。
Namespace
上面就是目前Linux Namespace的玩法。 現(xiàn)在,我來(lái)看一下其它的相關(guān)東西。
讓我們運(yùn)行一下上篇中的那個(gè)pid.mnt的程序(也就是PID Namespace中那個(gè)mount proc的程序),然后不要退出。

$ sudo ./pid.mnt 
[sudo] password for hchen: 
Parent [ 4599] - start a container!
Container [    1] - inside the container!

我們到另一個(gè)shell中查看一下父子進(jìn)程的PID:

hchen@ubuntu:~$ pstree -p 4599pid.mnt(4599)───bash(4600)

我們可以到proc下(/proc//ns)查看進(jìn)程的各個(gè)namespace的id(內(nèi)核版本需要3.8以上)。
下面是父進(jìn)程的:

hchen@ubuntu:~$ sudo ls -l /proc/4599/ns
total 0lrwxrwxrwx 1 root root 0  4月  7 22:01 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0  4月  7 22:01 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0  4月  7 22:01 net -> net:[4026531956]
lrwxrwxrwx 1 root root 0  4月  7 22:01 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0  4月  7 22:01 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0  4月  7 22:01 uts -> uts:[4026531838]

下面是子進(jìn)程的:

hchen@ubuntu:~$ sudo ls -l /proc/4600/ns
total 0lrwxrwxrwx 1 root root 0  4月  7 22:01 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0  4月  7 22:01 mnt -> mnt:[4026532520]
lrwxrwxrwx 1 root root 0  4月  7 22:01 net -> net:[4026531956]
lrwxrwxrwx 1 root root 0  4月  7 22:01 pid -> pid:[4026532522]
lrwxrwxrwx 1 root root 0  4月  7 22:01 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0  4月  7 22:01 uts -> uts:[4026532521]

我們可以看到,其中的ipc,net,user是同一個(gè)ID,而mnt,pid,uts都是不一樣的。如果兩個(gè)進(jìn)程指向的namespace編號(hào)相同,就說(shuō)明他們?cè)谕粋€(gè)namespace下,否則則在不同namespace里面。
這些文件還有另一個(gè)作用,那就是,一旦這些文件被打開(kāi),只要其fd被占用著,那么就算PID所屬的所有進(jìn)程都已經(jīng)結(jié)束,創(chuàng)建的namespace也會(huì)一直存在。比如:我們可以通過(guò):mount –bind /proc/4600/ns/uts ~/uts 來(lái)hold這個(gè)namespace。
另外,我們?cè)谏掀兄v過(guò)一個(gè)setns的系統(tǒng)調(diào)用,其函數(shù)聲明如下:

int setns(int fd, int nstype);

其中第一個(gè)參數(shù)就是一個(gè)fd,也就是一個(gè)open()系統(tǒng)調(diào)用打開(kāi)了上述文件后返回的fd,比如:

fd = open("/proc/4600/ns/nts", O_RDONLY);  // 獲取namespace文件描述符
setns(fd, 0); // 加入新的namespace

轉(zhuǎn)載自:酷 殼 – CoolShell.cn

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 寫(xiě)這個(gè)系列文章主要是對(duì)之前做項(xiàng)目用到的docker相關(guān)技術(shù)做一些總結(jié),包括docker基礎(chǔ)技術(shù)Linux命名空間,...
    __七把刀__閱讀 5,925評(píng)論 0 16
  • 轉(zhuǎn)載自 http://blog.opskumu.com/docker.html 一、Docker 簡(jiǎn)介 Docke...
    極客圈閱讀 10,756評(píng)論 0 120
  • 一、Docker 簡(jiǎn)介 Docker 兩個(gè)主要部件:Docker: 開(kāi)源的容器虛擬化平臺(tái)Docker Hub: 用...
    R_X閱讀 4,521評(píng)論 0 27
  • 五、Docker 端口映射 無(wú)論如何,這些 ip 是基于本地系統(tǒng)的并且容器的端口非本地主機(jī)是訪問(wèn)不到的。此外,除了...
    R_X閱讀 1,961評(píng)論 0 7
  • 書(shū)中提到,docker最核心的就是通過(guò)Linux NameSpace,Cgroups以及Union FS構(gòu)造的。這...
    行書(shū)以鑒閱讀 3,698評(píng)論 2 8

友情鏈接更多精彩內(nèi)容