Docker核心原理:namespace隔離


Namespace資源隔離

namespace 系統調用參數 隔離內容 應用意義
UTS CLONE_NEWUTS 主機與域名 每個容器在網絡中可以被視作一個獨立的節點,而非宿主機的一個進程
IPC CLONE_NEWIPC 信號量、消息隊列和共享內存 隔離容器間、容器與宿主機之間的進程間通信
PID CLONE_NEWPID 進程編號 隔離容器間、容器與宿主機之間的進程PID
Network CLONE_NEWNET 網絡設備、網絡棧、端口等 避免產生容器間、容器與宿主機之間產生端口已占用的問題
Mount CLONE_NEWNS 掛載點(文件系統) 容器間、容器與宿主機之間的文件系統互不影響
User CLONE_NEWUSER 用戶和用戶組 普通用戶(組)在容器內部也可以成為超級用戶(組),從而進行權限管理

進行Namespace API操作的方式

clone()

clone()可以在創建新進程(子進程)的同時創建namespace,是Docker使用namespace最基本的方法。

// child_func: 子進程運行的程序主函數
// child_stack: 子進程所使用的堆棧的位置,通常指向為子堆棧設置的內存空間的最高地址
// flags: 使用的CLONE_*標志位
// args: 傳入子進程的參數
// 在父進程中返回創建的子進程pid
int clone(int (*child_func)(void *), void *child_stack, int flags, void *arg);

相比於調用fork()創建新進程,clone()更加靈活,因為它可以通過改變flags參數來控制其實現的功能,后續將詳細介紹各種flags的用法。

setns()

setns()可以加入一個已經存在的namespace,Docker中的exec命令便調用了該方法:

// fd: 加入的namespace的文件描述符
// nstype: 檢查fd指向的namespace類型是否符合實際要求,為0表示不檢查
int setns(int fd, int nstype);

參數fd表示要加入的namespace的文件描述符,它指向 /proc/[pid]/ns文件夾下中的軟鏈接文件。而這些軟連接文件又指向不同的namespace, 如'cgroup:[4026531835]'中的數字即為其指向的namespace號($$是shell中表示當前運行的進程PID)。

[root@koktlzz proc]# ls -l /proc/$$/ns
總用量 0
lrwxrwxrwx 1 root root 0 1月  29 10:57 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 1月  29 10:57 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 root root 0 1月  29 10:57 mnt -> 'mnt:[4026531840]'
lrwxrwxrwx 1 root root 0 1月  29 10:57 net -> 'net:[4026531992]'
lrwxrwxrwx 1 root root 0 1月  29 10:57 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 1月  29 10:57 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 1月  29 10:57 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 1月  29 10:57 uts -> 'uts:[4026531838]'

參數fd是通過打開這些軟鏈接文件得到的,其中參數O_RDONLY代表以只讀的方式打開文件。

fd = open(link_file, O_RDONLY);

即使一個namespace下的所有進程全部結束,我們也可以通過指向該namespace的文件描述符fd定位並加入其中,這也是Docker實現加入已存在namespace的最基本方式。

unshare()

相比於clone(),unshare()不會啟動一個新進程,因此可以在原進程中進行一些需要隔離namespace的操作。目前Docker沒有調用這個api,其原因在下文中將會介紹。

Namespace隔離的實現方法

UTS(UNIX Time-sharing System)

Docker中,每個鏡像基本都以自身提供的服務來命名鏡像的hostname,其不會對宿主機產生任何影響,其原理就是利用了UTS namespace:

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

#define STACK_SIZE (1024 * 1024)
static char child_stack[STACK_SIZE];
char *const child_args[] = {"/bin/bash", NULL};
// 子進程
int child_main(void *args)
{
    printf("在子進程中\n");
    // sethostname將主機名設置為參數1的值,參數2代表名字的字節數
    sethostname("NewNamespace", 12);
    // exec可以執行用戶命令,常使用"/bin/bash"並接受參數,運行起一個shell
    execv(child_args[0], child_args);
    return 1;
}
// 父進程
int main()
{
    printf("程序開始\n");
    // 創建一個子進程及新的UTS namespace
    // 若未指定SIGCHLD信號,則在子進程終止時不會通知父進程
    int child_pid = clone(child_main, child_stack + STACK_SIZE, CLONE_NEWUTS | SIGCHLD, NULL);
    // waitpid阻塞父進程直到子進程結束
    waitpid(child_pid, NULL, 0);
    printf("已退出\n");
    return 0;
}

編譯運行后發現我們在進入子進程的同時,主機名也發生了改變,即進入了一個新創建的UTS namespace。當我們在子進程中的終端輸入exit后,子進程便調用execv()方法結束進程。於是父進程中的waitpid()方法停止阻塞,父進程也隨之退出。

[root@koktlzz home]# ./a.out
程序開始
在子進程中
[root@NewNamespace home]# exit
exit
已退出
[root@koktlzz home]# 

IPC(Inter-Process Communication)

兩個不同IPC namespace下的進程互相不可見,因此也就實現了進程間通信的隔離。其實現方法只需要修改clone()方法中的flag:

int child_pid = clone(child_main, child_stack + STACK_SIZE, CLONE_NEWIPC | SIGCHLD, NULL);

我們可以通過IPC資源之一的消息隊列來驗證IPC namespace的隔離,首先創建並查看在當前namespace下的所有消息隊列:

[root@koktlzz home]# ipcmk -Q
消息隊列 id:0
[root@koktlzz home]# ipcs -q

--------- 消息隊列 -----------
鍵        msqid      擁有者  權限     已用字節數 消息      
0x3ca3eb5d 0          root       644        0            0           

然后我們編譯運行修改后的程序,再次查看消息隊列:

[root@koktlzz home]# ./a.out
程序開始
在子進程中
[root@koktlzz home]# ipcs -q

--------- 消息隊列 -----------
鍵        msqid      擁有者  權限     已用字節數 消息      

[root@koktlzz home]# exit
exit
已退出

此時可以發現該IPC namespace下沒有剛剛創建的消息隊列,因此也就實現了進程間通信的隔離。

PID

PID namespace的隔離會將進程的pid重新分配,這樣兩個不同namespace下的進程的pid即使相同也不會崩潰。內核為所有的PID namespace維護了一個進程樹,最頂端是系統默認創建的root namespace。每個被創建的新PID namespace成為這個進程樹的子節點,而創建者namespace就是它的父節點。父節點可以看到子節點中的進程,並且可以通過信號等方式對子節點中的進程產生影響;反之,子節點卻無法看到父節點PID namespace中的任何內容。其實現方法依然只需要修改clone()方法中的flag:

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

編譯運行后即可看到PID namespace隔離的效果:

[root@koktlzz home]# ./a.out
程序開始
在子進程中
[root@koktlzz home]# echo $$
1
[root@koktlzz home]# exit
exit
已退出
[root@koktlzz home]# echo $$
2615550

值得注意的是,即使進入了新的PID namespace,使用ps -ef命令依然可以看到所有父進程的pid。這是因為我們還未實現文件系統掛載點的隔離,而該命令本質調用的是真實系統中的/proc文件內容,看到的自然是所有的進程。

另外,由於程序認為進程的pid是一個常量,因此調用unshare()和setns()方法的進程並不會進入新的PID namespace中。否則該進程就由原PID namespace進入到了新創建的PIDnamespace中,而PID的變化會導致一些程序(如調用getpid()方法)的崩潰。考慮到這一因素,Docker的exec命令不僅調用setns()加入已存在的namespace,但還是會調用clone()方法創建一個新的進程。

Mount

進程在創建Mount namespace時,會把當前文件結構復制給新的namespace。新namespace中所有的mount操作都只影響自身的文件系統,對外界不會產生任何影響。而掛載傳播則定義了掛載對象之間的關系,包括共享關系和從屬關系:

  • 共享關系:一個掛載對象中的掛載事件會傳播到另一個掛載對象,反之亦然;
  • 從屬關系:一個掛載對象中的掛載事件會傳播到另一個掛載對象,反之不行。

其實現方法依然只需要修改clone()方法中的flag,這里加入CLONE_NEWPID參數的目的是方便進行驗證:

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

編譯並運行程序后,我們發現在重新掛載了/proc文件(mount -t proc proc /proc)后的namespace中使用ps -ef命令后就只能看到子進程的pid了,並且沒有影響到父進程中的/proc文件掛載。

[root@koktlzz home]# ./a.out
程序開始
在子進程中
[root@koktlzz home]# mount -t proc proc /proc
[root@koktlzz home]# ps -ef
UID          PID    PPID  C STIME TTY          TIME CMD
root           1       0  0 12:39 pts/1    00:00:00 /bin/bash
root          17       1  0 12:40 pts/1    00:00:00 ps -ef
[root@koktlzz home]# exit
exit
已退出

Network

Network namespace提供了網絡資源的隔離,包括網絡設備、IPv4和IPv6協議棧、IP路由表、防火牆、socket等。
修改clone()方法中的flag參數:

int child_pid = clone(child_main, child_stack + STACK_SIZE, CLONE_NEWNET | SIGCHLD, NULL);

編譯運行程序后我們發現:在新創建的Network namespace中,無法通過DNS解析域名,甚至連網卡eth0都沒有了。這說明網絡資源已經完全隔離。

[root@koktlzz home]# gcc hello.c && ./a.out
程序開始
在子進程中
[root@koktlzz home]# ping www.baidu.com
ping: www.baidu.com: 未知的名稱或服務
[root@koktlzz home]# ip addr
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
[root@koktlzz home]# exit
exit
已退出
[root@koktlzz home]# ping www.baidu.com
PING www.a.shifen.com (180.101.49.12) 56(84) bytes of data.
64 bytes from 180.101.49.12 (180.101.49.12): icmp_seq=1 ttl=49 time=9.96 ms
64 bytes from 180.101.49.12 (180.101.49.12): icmp_seq=2 ttl=49 time=9.93 ms

Docker采用CNM(Container Network Model)實現network namespace隔離,CNM主要由三個部分構成:

  • 沙盒(sanbox):network namespace
  • 端點(endpoint):veth-pair
  • 網絡(network):linux bridge或vlan

對於Docker來說,linux bridge便是docker0。veth相當於網橋上的端口,它工作在鏈路層不需要配置IP。而docker0自身的IP默認為172.17.0.1/16,即容器的默認網關地址。所有容器都會在docker0的子網范圍內選取一個未占用的ip使用,並通過veth-pair連接到docker0。

20210131140833

Docker daemon創建容器網絡的過程如下:

  • 首先由Docker daemon負責創建一個虛擬網絡對veth-pair。一端綁定到docker0網橋上,另一端接入容器中。
  • 容器內部的初始化進程(即init進程)在管道一端循環等待,直到Docker daemon從管道另一端向其傳輸關於veth-pair設備的信息,隨后關閉管道。
  • 最后,init進程結束等待,啟動它的虛擬網卡"eth0"。

User

一個普通用戶的進程通過clone()方法創建的子進程可以在這個新的User namespace中擁有不同的用戶和用戶組,這意味着容器內部的root用戶可能在宿主機上只是一個普通用戶,從而為容器提高了極大的自由。User namespace與PID namespace類似,同樣是一個層層嵌套的樹狀結構。

其實現方法依然是在flag參數中加入CLONE_NEWUSER創建一個新的User namespace():

int child_pid = clone(child_main, child_stack + STACK_SIZE, CLONE_NEWUSER | SIGCHLD, NULL);

編譯運行后我們可以發現,在創建的新User namespace中,user和group變成了nobody,uid和gid也隨之改變。

[root@koktlzz home]# ./a.out
程序開始
在子進程中
[nobody@koktlzz home]$ id
uid=65534(nobody) gid=65534(nobody) 組=65534(nobody)
[nobody@koktlzz home]$ exit
exit
已退出
[root@koktlzz home]# id
uid=0(root) gid=0(root) 組=0(root)

除此之外,我們還要把子節點namespace中的初始user與其父節點namespace中的某個用戶建立映射關系。這樣子節點User namespace想要給父節點User namespace發送一個信號或操作某一個文件時,系統就會判斷子節點中的用戶在父節點中是否有對應的權限。這是通過在/proc/[pid]/uid_map/proc/[pid]/gid_map文件中寫入對應的映射信息實現的,以uid_map為例:

void set_uid_map(pid_t pid, int inside_id, int outside_id, int length)
{
    char path[256];
    sprintf(path, "/proc/%d/uid_map", getpid());
    FILE *uid_map = fopen(path, "w");
    // inside_id表示新建的User namespace中對應的uid/gid
    // outside_id表示namespace外部映射的uid/gid
    // length表示映射范圍,通常為1,表示只映射一個
    fprintf(uid_map, "%d %d %d", inside_id, outside_id, length);
    fclose(uid_map);
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM