進程隔離是容器的關鍵組件,容器的關鍵底層機制之一是命名空間(namespaces),下面將分析命名空間(namespaces)是什麼以及命名空間(namespaces)是如何工作的,通過構建自己的隔離容器能夠更好地理解每一部分。
0x01 命名空間(namespaces)是什麼命名空間(namespaces)是2008 年內核版本2.6.24 中發布的Linux 內核特性。它們為進程提供了自己的系統視圖,從而將獨立的進程相互隔離。換句話說, 命名空間(namespaces)定義了一個進程可以使用的資源集,你不能與你看不到的東西交互。在高層次上,它們允許對全局操作系統資源進行細粒度分區,例如安裝點、網絡堆棧和進程間通信實用程序。命名空間(namespaces)的一個強大方面是它們限制了對系統資源的訪問,而正在運行的進程不知道這些限制。在典型的Linux 方式中,它們表示為/proc/
cryptonite@cryptonite:~$echo$$
4622
cryptonite@cryptonite:~$ls/proc/$$/ns-al
total0
dr-x--x--x2cryptonitecryptonite0Jun2915:00.
dr-xr-xr-x9cryptonitecryptonite0Jun2913:13.
lrwxrwxrwx1cryptonitecryptonite0Jun2915:00cgroup-'cgroup:[4026531835]'
lrwxrwxrwx1cryptonitecryptonite0Jun2915:00ipc-'ipc:[4026531839]'
lrwxrwxrwx1cryptonitecryptonite0Jun2915:00mnt-'mnt:[4026531840]'
lrwxrwxrwx1cryptonitecryptonite0Jun2915:00net-'net:[4026532008]'
lrwxrwxrwx1cryptonitecryptonite0Jun2915:00pid-'pid:[4026531836]'
lrwxrwxrwx1cryptonitecryptonite0Jun2915:00pid_for_children-'pid:[4026531836]'
lrwxrwxrwx1cryptonitecryptonite0Jun2915:00time-'time:[4026531834]'
lrwxrwxrwx1cryptonitecryptonite0Jun2915:00time_for_children-'time:[4026531834]'
lrwxrwxrwx1cryptonitecryptonite0Jun2915:00user-'user:[4026531837]'
lrwxrwxrwx1cryptonitecryptonite0Jun2915:00uts-'uts:[4026531838]'當生成一個新進程時,所有的命名空間(namespaces)都繼承自它的父進程。
#inception
cryptonite@cryptonite:~$/bin/zsh
#fatherPIDverification
╭─cryptonite@cryptonite~
╰─$ps-efj|grep$$
crypton+135604622135604622115:07pts/100:00:02/bin/zsh
╭─cryptonite@cryptonite~
╰─$ls/proc/$$/ns-al
total0
dr-x--x--x2cryptonitecryptonite0Jun2915:10.
dr-xr-xr-x9cryptonitecryptonite0Jun2915:07.
lrwxrwxrwx1cryptonitecryptonite0Jun2915:10cgroup-'cgroup:[4026531835]'
lrwxrwxrwx1cryptonitecryptonite0Jun2915:10ipc-'ipc:[4026531839]'
lrwxrwxrwx1cryptonitecryptonite0Jun2915:10mnt-'mnt:[4026531840]'
lrwxrwxrwx1cryptonitecryptonite0Jun2915:10net-'net:[4026532008]'
lrwxrwxrwx1cryptonitecryptonite0Jun2915:10pid-'pid:[4026531836]'
lrwxrwxrwx1cryptonitecryptonite0Jun2915:10pid_for_children-'pid:[4026531836]'
lrwxrwxrwx1cryptonitecryptonite0Jun2915:10time-'time:[4026531834]'
lrwxrwxrwx1cryptonitecryptonite0Jun2915:10time_for_children-'time:[4026531834]'
lrwxrwxrwx1cryptonitecryptonite0Jun2915:10user-'user:[4026531837]'
lrwxrwxrwx1cryptonitecryptonite0Jun2915:10uts-'uts:[4026531838]'命名空間(namespaces)是使用帶有以下參數之一的clone系統調用創建的:
CLONE_NEWNS - 創建新的掛載命名空間(namespaces);
CLONE_NEWUTS - 創建新的UTS 命名空間(namespaces);
CLONE_NEWIPC - 創建新的IPC 命名空間(namespaces);
CLONE_NEWPID - 創建新的PID 命名空間(namespaces);
CLONE_NEWNET - 創建新的NET 命名空間(namespaces);
CLONE_NEWUSER - 創建新的USR 命名空間(namespaces);
CLONE_NEWCGROUP - 創建一個新的cgroup 命名空間(namespaces)。
命名空間(namespaces)也可以使用unshare系統調用來創建。 clone和unshare的區別在於,clone會在一組新的名稱空間中生成一個新進程,而unshare會在一組新的namespaces中移動當前進程。
0x02 為什麼要使用命名空間(namespaces)如果我們將命名空間(namespaces)想像為包含一些抽象全局系統資源的進程的盒子,這些盒子的一個好處是你可以從一個盒子中添加和刪除內容,並且不會影響其他盒子的內容。或者,如果一個盒子(一組命名空間namespaces)中的進程A 發瘋並決定刪除該盒子中的整個文件系統或網絡堆棧,它不會影響為放置在不同盒子中的另一個進程B 提供的這些資源的抽象。此外,命名空間(namespaces)甚至可以提供細粒度的隔離,允許進程A 和B 共享一些系統資源(例如共享掛載點或網絡堆棧)。當必須在給定機器上執行不受信任的代碼而不影響主機操作系統時,通常會使用命名空間(namespaces)。 Hackerrank、Codeforces、 Rootme等編程競賽平台使用命名空間(namespaces)環境,以便安全地執行和驗證參賽者的代碼,而不會使他們的服務器面臨風險。 PaaS(平台即服務)提供商,例如穀歌云引擎使用命名空間(namespaces)環境在同一硬件上運行多個用戶服務(例如網絡服務器、數據庫),而不會干擾這些服務。因此,命名空間(namespaces)也可以被視為對有效的資源共享很有用。 Docker 或LXC 等其他雲技術也使用命名空間(namespaces)作為進程隔離的手段。這些技術將操作系統進程置於容器隔離環境中。例如,在Docker 容器中運行進程就像在虛擬機中運行一樣。容器和虛擬機之間的區別在於容器直接共享和使用主機操作系統內核,因此由於沒有硬件仿真,它們比虛擬機輕量得多。整體性能的提高主要是由於使用了直接集成在Linux 內核中的命名空間(namespaces)。
0x03 命名空間(namespaces)的類型在當前穩定的Linux Kernel 5.7 版中,有七種不同的命名空間(namespaces):
PID命名空間(namespaces):系統進程樹的隔離;
NET 命名空間(namespaces):主機網絡堆棧的隔離;
MNT 命名空間(namespaces):主機文件系統掛載點的隔離;
UTS 命名空間(namespaces):主機名的隔離;
IPC 命名空間(namespaces):進程間通信實用程序(共享段、信號量)的隔離;
USER 命名空間(namespaces):系統用戶ID 的隔離;
CGROUP 命名空間(namespaces):隔離主機的虛擬cgroup 文件系統。
命名空間(namespaces)是每個進程的屬性。每個進程最多可以感知一個命名空間(namespaces)。換句話說,在任何給定時刻,任何進程P 都恰好屬於每個命名空間(namespaces)的一個實例。例如,當一個給定的進程想要更新系統上的路由表時,內核會向它顯示它當時所屬命名空間(namespaces)的路由表副本。如果進程在系統中詢問其ID,內核將以其當前命名空間(namespaces)中的進程ID 響應(在嵌套命名空間(namespaces)的情況下)。我們將詳細查看每個命名空間(namespaces),以了解它們背後的操作系統機制。了解這一點將幫助我們找到當今容器化技術的本質。
1.PID命名空間(namespaces)歷史上,Linux 內核一直維護著一個單一的進程樹。樹數據結構包含對當前在父子層次結構中運行的每個進程的引用。它還枚舉操作系統中所有正在運行的進程。這個結構在procfs文件系統中維護, 它是實時系統的一個屬性,即它僅在操作系統運行時存在。這種結構允許具有足夠特權的進程附加到其他進程、檢查、通信和kill它們。它還包含有關進程的根目錄、當前工作目錄、打開的文件描述符、虛擬內存地址、可用安裝點等的信息。
#anexampleoftheprocfsstructure
cryptonite@cryptonite:~$ls/proc/1/
arch_statuscoredump_filtergid_mapmountspagemapsetgroupstask
attrcpu_resctrl_groupsiomountstatspatch_statesmapstimens_offsets
cgroupenvironmap_filesnuma_mapsrootstatuid_map
clear_refsexemapsoom_adjschedstatm
.
#anexampleoftheprocesstreestructure
cryptonite@cryptonite:~$pstree|head-n20
systemd-+-ModemManager---2*[{ModemManager}]
|-NetworkManager---2*[{NetworkManager}]
|-accounts-daemon---2*[{accounts-daemon}]
|-acpid
|-avahi-daemon---avahi-daemon
|-bluetoothd
|-boltd---2*[{boltd}]
|-colord---2*[{colord}]
|-containerd---17*[{containerd}]在系統啟動時,大多數現代Linux 操作系統上啟動的第一個進程是systemd(系統守護進程),它位於樹的根節點上。它的父進程是PID=0,它是OS 中不存在的進程。此進程之後負責啟動其他服務/守護進程,這些服務/守護進程表示為其子進程,並且是操作系統正常運行所必需的。這些進程的PID 1,樹結構中的PID 是唯一的。
隨著Process 命名空間(namespaces)(或PID 命名空間(namespaces))的引入可以製作嵌套的流程樹。它允許除systemd (PID=1) 以外的進程通過在子樹的頂部移動來將自己視為根進程,從而在該子樹中獲得PID=1。同一子樹中的所有進程也將獲得與進程命名空間(namespaces)相關的ID。這也意味著某些進程可能最終擁有多個ID,具體取決於它們所在進程命名空間(namespaces)的數量。然而,在每個命名空間(namespaces)中,至多一個進程可以擁有一個給定的PID(進程樹中節點的唯一值)成為每個命名空間(namespaces)的屬性)。這是因為根進程命名空間(namespaces)中的進程之間的關係保持不變。或者換句話說,新PID 命名空間(namespaces)中的進程仍然附加到其父級,因此是其父級PID 命名空間(namespaces)的一部分。所有進程之間的這些關係可以在根進程命名空間(namespaces)中看到,但在嵌套進程命名空間(namespaces)中它們是不可見的。這意味著嵌套進程命名空間(namespaces)中的進程不能與其父進程或上層進程命名空間(namespaces)中的任何其他進程交互。這是因為,在新的PID 命名空間(namespaces)的頂部,進程將其PID 視為1,並且在PID=1 的進程之前沒有其他進程。
在Linux 內核中,PID 表示為一個結構。在內部,我們還可以找到進程所屬的命名空間(namespaces)作為upid struct數組的一部分。
structupid{
intnr;/*thepidvalue*/
structpid_namespace*ns;/*thenamespacethisvalue
*isvisiblein*/
structhlist_nodepid_chain;/*hashchainforfastersearchofPIDSinthegivennamespace*/
};
structpid{
atomic_tcount;/*referencecounter*/
structhlist_headtasks[PIDTYPE_MAX];/*listsoftasks*/
structrcu_headrcu;
intlevel;//numberofupids
structupidnumbers[0];//arrayofpidnamespaces
};要在新的PID 命名空間(namespaces)內創建新進程,必須使用特殊標誌CLONE_NEWPID調用clone()系統調用。而下面討論的其他命名空間(namespaces)也可以使用unshare()系統調用創建,PID 命名空間(namespaces)只能在使用clone()或fork()系統調用產生新進程時創建。
#Let'sstartaprocessinanewpidnamespace;
cryptonite@cryptonite:~$sudounshare--pid/bin/bash
bash:fork:Cannotallocatememory[1]
root@cryptonite:/home/cryptonite#ls
bash:fork:Cannotallocatememory[1]shell卡在兩個命名空間(namespaces)之間。這是因為unshare在執行後沒有進入新的命名空間(namespaces)(execve()調用)。當前的“unshare”進程調用了unshare系統調用,創建了一個新的pid命名空間(namespaces),但是當前的“unshare”進程不在新的pid命名空間(namespaces)中。進程B創建了一個新的命名空間(namespaces),但進程B本身不會被放入新的命名空間(namespaces),只有進程B的子進程才會被放入新的命名空間(namespaces)。創建命名空間(namespaces)後,`unshare程序將執行/bin/bash。然後/bin/bash將分叉幾個新的子進程來做一些工作。這些子進程將有一個相對於新命名空間(namespaces)的PID,當這些進程完成時,它們將退出,退出命名空間(namespaces)但是PID沒有置1。 Linux 內核不喜歡沒有PID=1 進程的PID 命名空間(namespaces)。因此,當命名空間(namespaces)為空時,內核將禁用與該命名空間(namespaces)內的PID 分配相關的一些機制,從而導致此錯誤。
我們必須指示unshare程序在創建命名空間(namespaces)後派生一個新進程。然後這個新進程將設置PID=1 並將執行我們的shell 程序。這樣當/bin/bash的子進程退出時,命名空間(namespaces)仍然會有一個PID=1 的進程。
cryptonite@cryptonite:~$sudounshare--pid--fork/bin/bash
root@cryptonite:/home/cryptonite#echo$$
1
root@cryptonite:/home/cryptonite#ps
PIDTTYTIMECMD
7239pts/000:00:00sudo
7240pts/000:00:00unshare
7241pts/000:00:00bash
7250pts/000:00:00ps但是當我們使用ps時,為什麼我們的shell 沒有PID 1呢?為什麼我們仍然可以從根命名空間(namespaces)看到進程?該PS程序使用的procfs虛擬文件系統,以獲取有關係統中的電流進程的信息。該文件系統安裝在/proc 目錄中。但是,在新命名空間(namespaces)中,該掛載點描述了root PID 命名空間(namespaces)中的進程。有兩種方法可以避免這種情況:
#creatinganewmountnamespaceandmountinganewprocfsinside
cryptonite@cryptonite:~$sudounshare--pid--fork--mount/bin/bash
root@cryptonite:/home/cryptonite#mount-tprocproc/proc
root@cryptonite:/home/cryptonite#ps
PIDTTYTIMECMD
1pts/200:00:00bash
9pts/200:00:00ps
#Orusetheunsharewrapperwiththe--mount-procflag
#whichdoesthesame
cryptonite@cryptonite:~$sudounshare--fork--pid--mount-proc/bin/bash
root@cryptonite:/home/cryptonite#ps
PIDTTYTIMECMD
1pts/100:00:00bash
8pts/100:00:00ps正如我們之前提到的,一個進程可以有多個ID,這取決於該進程所在的命名空間(namespaces)的數量。現在檢查嵌套在兩個命名空間(namespaces)中的shell 的不同PID。
╭cryptonite@cryptonite:~$sudounshare--fork--pid--mount-proc/bin/bash
#thisprocesshasPID4700intherootPIDnamespace
root@cryptonite:/home/cryptonite#unshare--fork--pid--mount-proc/bin/bash
root@cryptonite:/home/cryptonite#ps
PIDTTYTIMECMD
1pts/100:00:00bash
8pts/100:00:00ps
#Let'sinspectthedifferentPIDs
cryptonite@cryptonite:~$sudonsenter--target4700--pid--mount
cryptonite#ps-aux
USERPID%CPU%MEMVSZRSSTTYSTATSTARTTIMECOMMAND
root10.00.0184764000pts/0S
Recommended Comments