Michael Kerrisk 是 TLPI 的作者,也是 man-pages
的維護者所以可能聽一聽會以為他在唸照著 。如果懶得聽他講的話,也可以直接去看 man
唸man
。同時他在 LWN 上也以 Namespaces in Operation 為題,寫了一系列介紹不同種類的 namespace 的文章及範例程式。
以下的資料多半會講「namespace 有 7 種」。但在 2020 年一月 (5.6 版的 kernel) 時,新增了一個 Time Namespace。所以實際上現在已經有 8 種了。
Linux 中的 namespace(7)
機制是一種蒙蔽了行程的雙眼「讓一個行程只能看見作業系統中的部分資源」的機制。這些資源可能包含檔案系統、使用者、時間、網路等等。
Namespace 其中一個應用是「隔離」同一個作業系統上的不同行程。把一個行程放在某一個種類的 namespace 中,它就會只看得到該 namespace 下看得到的資源。儘管處在該 namespace 中的行程可能以為自己可以存取整個根目錄、以為自己是它 root,但在 namespace 以外的行程看來只是一個普通權限的行程。而這也是容器 (container) 的基礎。
namespace 很多種。不同種類的 namespace 可以用來隔離不同的資源。就寫作時間來說總共有 8 種。可以在 namespaces(7)
的 man
中查到。而他們也各自有自己的 man
:
unshare
–- 新增 Namespace不同種類的 namespace 可以隔離出不同種類的資源給該 namespace 下的行程使用。而要使一個行程加入某一個 namespace 中,可以改變 clone
的 flag
參數,使得新行程建立的同時也建立一個新的 namespace ,並讓這個新行程加入這個新的 namespace; 或是在事後呼叫 unshare(2)
或 setns(2)
來改變一個行程所處的 namespace。在 namespaces(7)
中的 The namespaces API 有更詳細的說明。
unshare -u
–- 新增 UTS NamespaceUTS 是 Unix Time-Sharing 的縮寫。處在不同 UTS namespace 中的行程,可以有不同的 hostname 與 domain name。也就是 hostname
跟 hostname -d
會得到的那個名稱。
舉例來說,開兩個終端機視窗(或 tmux
開兩個分割)。假定一開始的 hostname 是 ubuntu
:
這時,如果在其中一個視窗使用 unshare
的 -u
選項新增一個 UTS namespace。那麼「改變 hostname」這件事情只會被該 UTS namespace 中的行程看到,而不是這個 namesapce 中的行程則不會看到 hostname 發生改變。舉例來說:
但是在另外一個終端機視窗使用 hostname
,會發現裡面的 hostname 還是一樣:
/proc/PID/ns
–- 行程所處的 Namespaces對於一個 PID 為 PID
的行程,其所處的 namespace 可以藉由檢視 /proc/PID/ns
目錄底下的 symlink 來得知:
這些 symlink 中的文字代表該 namespace 的種類,而編號則作為 namesapce 的唯一識別。舉例來說,在剛剛新建立的 UTS namespace 中,若讀取該檔案,會出現
但在原先的 namespace 中,則會出現不一樣的編號:
這表示兩者處在不同的 UTS namespace。但是另外一方面,如果查看兩者的 mount namespace,則會發現兩者是一樣的。輸出分別是:
以及:
nsenter
–- 進入某個行程所屬的某種 namespace除了新建一個 namespace 之外,也可以使用 nsenter
這個命令去進入已知的 namespace。舉例來說,假定處在剛剛的 UTS namespace 的 bash
之 PID 為 2391
,而另外一個不處在該 namespace 的 bash
之 PID 為 2368
。則 nsenter
可以指定「把當下的行程加入某個行程所處的某個 namespace」舉例來說:
ps
中的選項ps
這個命令也可以列出行程所屬的某些 namespace。可以在 ps(1)
中搜尋 namespace。比如說使用
或是一次列出所有 ps
能列出的 namespace:
Linux 上的所有檔案形成一個樹狀結構,而這個樹狀結構的根節點就是 /
。這個樹狀結構中的不同檔案可能位於不同的裝置上。「掛載一個檔案系統」的意思是在現有的這個由檔案形成的樹當中的某個節點下,額外接上一個由該檔案系統中的檔案形成的樹。而這個接上另外一棵樹的「某個地方」就稱為 mount point。對於一個 mount namespace,處於當中的所有行程都會看到相同的 mount point。而不同 mount space 中,對於 mount point 的改變則
umount
如果建立一個新的 mount namespace,並且在裡面 umount
某些 mount point,那麼這個 umount
只會影響這個 mount namespace 中的行程所看到的 mount point。舉例來說,如果建立一個 mount namespace 之後:
使用 umount
的 -a
盡可能地卸載所有 mount point:
雖然在這個 mount namespace 的 shell 中,明顯少掉了許多 mount point,但對於不處於這個 mount namespace 中的 shell,則會看到跟原先一樣的內容:
如果想要進一步將受到這個 mount namespace 中有東西真的被 umount
的話,可以先在某個 mount namespace 中 umount -a
,並且檢視某一個會被 umount
的檔案系統。比如 /sys/kernel/tracing
:
這時候會發現目錄裡面什麼東西都沒有。但是在這個 mount namespace 之外的 bash
,還是可以看見這個目錄裡面有東西。
處於不同 PID namespace 的行程,彼此可以具有重複的 PID。在 container 中的使用時機比如要把一個 container 放到另外一個機器使用時,這個 conainer 中的行程所具有的 PID 有可能會與這台機器上原先的行程有所重複。如果讓上面的行程在獨立的 PID namespace 中執行,就可以避免這個衝突的問題。另外一個例子是:單一作業系統上的多個 PID namespace 中可以各自有 PID 為 1 的行程(比如說各自有自己的 init
或 systemd
),這就會使得這兩個 PID namespace (至少在行程管理上) 像是兩個獨立的作業系統。一個這樣的例子是 Yelp 的 dumb-init。
舉例來說:
可以用 pstree -N pid
來依照 PID namespace 分類對應的行程:
跟前面的 namespace 不一樣的地方是:PID namespace 之間有階層關係。子行程所處的 PID namespace 中的所有行程,可以被所有祖輩行程的 PID namespace 中看見。並且這些行程在每個祖輩行程所處的 PID namespace 中,也都有自己對應的 PID。但相反地,子行程所處的 PID namespace 中,就無法看見祖輩所處的 PID namespace 中的其他行程:
/proc
中顯示的行程如果只有幫新行程建立 PID namespace,那麼因為這個新行程仍然與原先的行程處在同一個 mount namespace,所以這時 /proc
底下還是看得到原先的行程:
如果這不是期待的行為,那麼可以考慮同時也幫這個行程建立一個 mount namespace,再重新 mount 一次 /proc
: