PID namespaces用來隔離進程的ID空間,使得不一樣pid namespace裏的進程ID能夠重複且相互之間不影響。 html
PID namespace能夠嵌套,也就是說有父子關係,在當前namespace裏面建立的全部新的namespace都是當前namespace的子namespace。父namespace裏面能夠看到全部子孫後代namespace裏的進程信息,而子namespace裏看不到祖先或者兄弟namespace裏的進程信息。linux
目前PID namespace最多能夠嵌套32層,由內核中的宏MAX_PID_NS_LEVEL來定義面試
Linux下的每一個進程都有一個對應的/proc/PID目錄,該目錄包含了大量的有關當前進程的信息。 對一個PID namespace而言,/proc目錄只包含當前namespace和它全部子孫後代namespace裏的進程的信息。shell
在Linux系統中,進程ID從1開始日後不斷增長,而且不能重複(固然進程退出後,ID會被回收再利用),進程ID爲1的進程是內核啓動的第一個應用層進程,通常是init進程(如今採用systemd的系統第一個進程是systemd),具備特殊意義,當系統中一個進程的父進程退出時,內核會指定init進程成爲這個進程的新父進程,而當init進程退出時,系統也將退出。ubuntu
除了在init進程裏指定了handler的信號外,內核會幫init進程屏蔽掉其餘任何信號,這樣能夠防止其餘進程不當心kill掉init進程致使系統掛掉。不過有了PID namespace後,能夠經過在父namespace中發送SIGKILL或者SIGSTOP信號來終止子namespace中的ID爲1的進程。segmentfault
因爲ID爲1的進程的特殊性,因此每一個PID namespace的第一個進程的ID都是1。當這個進程運行中止後,內核將會給這個namespace裏的全部其餘進程發送SIGKILL信號,導致其餘全部進程都中止,因而namespace被銷燬掉。bash
本篇全部例子都在ubuntu-server-x86_64 16.04下執行經過dom
#查看當前pid namespace的ID dev@ubuntu:~$ readlink /proc/self/ns/pid pid:[4026531836] #啓動新的pid namespace #這裏同時也啓動了新的uts和mount namespace #新的uts是爲了設置一個新的hostname,便於和老的namespace區分 #新的mount namespace是爲了方便咱們修改新namespace裏面的mount信息, #由於這樣不會對老namespace形成影響 #這裏--fork是爲了讓unshare進程fork一個新的進程出來,而後再用bash替換掉新的進程 #這是pid namespace自己的限制,進程所屬的pid namespace在它建立的時候就肯定了,不能更改, #因此調用unshare和nsenter後,原來的進程仍是屬於老的namespace, #而新fork出來的進程才屬於新的namespace dev@ubuntu:~$ sudo unshare --uts --pid --mount --fork /bin/bash root@ubuntu:~# hostname container001 root@ubuntu:~# exec bash root@container001:~# #查看進程間關係,當前bash(31646)確實是unshare的子進程 root@container001:~# pstree -pl ├─sshd(955)─┬─sshd(17810)───sshd(17891)───bash(17892)───sudo(31644)── ─unshare(31645)───bash(31646)───pstree(31677) #他們屬於不一樣的pid namespace root@container001:~# readlink /proc/31645/ns/pid pid:[4026531836] root@container001:~# readlink /proc/31646/ns/pid pid:[4026532469] #但爲何經過這種方式查看到的namespace仍是老的呢? root@container001:~# readlink /proc/$$/ns/pid pid:[4026531836] #因爲咱們實際上已是在新的namespace裏了,而且當前bash是當前namespace的第一個進程 #因此在新的namespace裏看到的他的進程ID是1 root@container001:~# echo $$ 1 #但因爲咱們新的namespace的掛載信息是從老的namespace拷貝過來的, #因此這裏看到的仍是老namespace裏面的進程號爲1的信息 root@container001:~# readlink /proc/1/ns/pid pid:[4026531836] #ps命令依賴/proc目錄,因此ps的輸出仍是老namespace的視圖 root@container001:~# ps ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 7月07 ? 00:00:06 /sbin/init root 2 0 0 7月07 ? 00:00:00 [kthreadd] ... root 31644 17892 0 7月14 pts/0 00:00:00 sudo unshare --uts --pid --mount --fork /bin/bash root 31645 31644 0 7月14 pts/0 00:00:00 unshare --uts --pid --mount --fork /bin/bash #因此咱們須要從新掛載咱們的/proc目錄 root@container001:~# mount -t proc proc /proc #從新掛載後,能看到咱們新的pid namespace ID了 root@container001:~# readlink /proc/$$/ns/pid pid:[4026532469] #ps的輸出也正常了 root@container001:~# ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 7月14 pts/0 00:00:00 bash root 44 1 0 00:06 pts/0 00:00:00 ps -ef
調用unshare或者setns函數後,當前進程的namespace不會發生變化,不會加入到新的namespace,而它的子進程會加入到新的namespace。也就是說進程屬於哪一個namespace是在進程建立的時候決定的,而且之後再也沒法更改。ssh
在一個PID namespace裏的進程,它的父進程可能不在當前namespace中,而是在外面的namespace裏面(這裏外面的namespace指當前namespace的祖先namespace),這類進程的ppid都是0。好比新namespace裏面的第一個進程,他的父進程就在外面的namespace裏。經過setns的方式加入到新namespace中的進程的父進程也在外面的namespace中。socket
能夠在祖先namespace中看到子namespace的全部進程信息,且能夠發信號給子namespace的進程,但進程在不一樣namespace中的PID是不同的。
#--------------------------第一個shell窗口---------------------- #記下最外層的namespace ID dev@ubuntu:~$ readlink /proc/$$/ns/pid pid:[4026531836] #建立新的pid namespace, 這裏--mount-proc參數是讓unshare自動從新mount /proc目錄 dev@ubuntu:~$ sudo unshare --uts --pid --mount --fork --mount-proc /bin/bash root@ubuntu:~# hostname container001 root@ubuntu:~# exec bash root@container001:~# readlink /proc/$$/ns/pid pid:[4026532469] #再建立新的pid namespace root@container001:~# unshare --uts --pid --mount --fork --mount-proc /bin/bash root@container001:~# hostname container002 root@container001:~# exec bash root@container002:~# readlink /proc/$$/ns/pid pid:[4026532472] #再建立新的pid namespace root@container002:~# unshare --uts --pid --mount --fork --mount-proc /bin/bash root@container002:~# hostname container003 root@container002:~# exec bash root@container003:~# readlink /proc/$$/ns/pid pid:[4026532475] #目前namespace container003裏面就一個bash進程 root@container003:~# pstree -p bash(1)───pstree(22) #這樣咱們就有了三層pid namespace, #他們的父子關係爲container001->container002->container003 #--------------------------第二個shell窗口---------------------- #在最外層的namespace中查看上面新建立的三個namespace中的bash進程 #從這裏能夠看出,這裏顯示的bash進程的PID和上面container003裏看到的bash(1)不同 dev@ubuntu:~$ pstree -pl|grep bash|grep unshare |-sshd(955)-+-sshd(17810)---sshd(17891)---bash(17892)---sudo(31814)-- -unshare(31815)---bash(31816)---unshare(31842)---bash(31843)-- -unshare(31864)---bash(31865) #各個unshare進程的子bash進程分別屬於上面的三個pid namespace dev@ubuntu:~$ sudo readlink /proc/31816/ns/pid pid:[4026532469] dev@ubuntu:~$ sudo readlink /proc/31843/ns/pid pid:[4026532472] dev@ubuntu:~$ sudo readlink /proc/31865/ns/pid pid:[4026532475] #PID在各個namespace裏的映射關係能夠經過/proc/[pid]/status查看到 #這裏31865是在最外面namespace中看到的pid #45,23,1分別是在container001,container002和container003中的pid dev@ubuntu:~$ grep pid /proc/31865/status NSpid: 31865 45 23 1 #建立一個新的bash並加入container002 dev@ubuntu:~$ sudo nsenter --uts --mount --pid -t 31843 /bin/bash root@container002:/# #這裏bash(23)就是container003裏面的pid 1對應的bash root@container002:/# pstree -p bash(1)───unshare(22)───bash(23) #unshare(22)屬於container002 root@container002:/# readlink /proc/22/ns/pid pid:[4026532472] #bash(23)屬於container003 root@container002:/# readlink /proc/23/ns/pid pid:[4026532475] #爲何上面pstree的結果裏面沒看到nsenter加進來的bash呢? #經過ps命令咱們發現,咱們新加進來的那個/bin/bash的ppid是0,難怪pstree裏面顯示不出來 #從這裏能夠看出,跟最外層namespace不同的地方就是,這裏能夠有多個進程的ppid爲0 #從這裏的TTY也能夠看出哪些命令是在哪些窗口執行的, #pts/0對應第一個shell窗口,pts/1對應第二個shell窗口 root@container002:/# ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 04:39 pts/0 00:00:00 bash root 22 1 0 04:39 pts/0 00:00:00 unshare --uts --pid --mount --fork --mount-proc /bin/bash root 23 22 0 04:39 pts/0 00:00:00 bash root 46 0 0 04:52 pts/1 00:00:00 /bin/bash root 59 46 0 04:53 pts/1 00:00:00 ps -ef #--------------------------第三個shell窗口---------------------- #建立一個新的bash並加入container001 dev@ubuntu:~$ sudo nsenter --uts --mount --pid -t 31816 /bin/bash root@container001:/# #經過pstree和ps -ef咱們可看到全部三個namespace中的進程及他們的關係 #bash(1)───unshare(22)屬於container001 #bash(23)───unshare(44)屬於container002 #bash(45)屬於container003,而68和84兩個進程分別是上面兩次經過nsenter加進來的bash #同上面ps的結果比較咱們能夠看出,一樣的進程在不一樣的namespace裏面擁有不一樣的PID root@container001:/# pstree -pl bash(1)───unshare(22)───bash(23)───unshare(44)───bash(45) root@container001:/# ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 04:37 pts/0 00:00:00 bash root 22 1 0 04:39 pts/0 00:00:00 unshare --uts --pid --mount --fork --mount-proc /bin/bash root 23 22 0 04:39 pts/0 00:00:00 bash root 44 23 0 04:39 pts/0 00:00:00 unshare --uts --pid --mount --fork --mount-proc /bin/bash root 45 44 0 04:39 pts/0 00:00:00 bash root 68 0 0 04:52 pts/1 00:00:00 /bin/bash root 84 0 0 05:00 pts/2 00:00:00 /bin/bash root 95 84 0 05:00 pts/2 00:00:00 ps -ef #發送信號給contain002中的bash root@container001:/# kill 68 #--------------------------第二個shell窗口---------------------- #回到第二個窗口,發現bash已經被kill掉了,說明父namespace是能夠發信號給子namespace中的進程的 root@container002:/# exit dev@ubuntu:~$
當一個進程的父進程被kill掉後,該進程將會被當前namespace中pid爲1的進程接管,而不是被最外層的系統級別的init進程接管。
當pid爲1的進程中止運行後,內核將會給這個namespace及其子孫namespace裏的全部其餘進程發送SIGKILL信號,導致其餘全部進程都中止,因而當前namespace及其子孫後代的namespace都被銷燬掉。
#仍是繼續以上面三個namespace爲例 #--------------------------第一個shell窗口---------------------- #在003裏面啓動兩個新的bash,使他們的繼承關係以下 root@container003:~# bash root@container003:~# bash root@container003:~# pstree bash───bash───bash───pstree #利用unshare、nohup和sleep的組合,模擬出咱們想要的父子進程 #unshare --fork會使unshare建立一個子進程 #nohup sleep sleep 3600&會讓這個子進程在後臺運行而且sleep一小時 root@container003:~# unshare --fork nohup sleep 3600& [1] 77 #因而咱們獲得了咱們想要的進程間關係結構 root@container003:~# pstree -p bash(1)───bash(26)───bash(36)─┬─pstree(80) └─unshare(77)───sleep(78) #如咱們所指望的,kill掉unshare(77)後, sleep就被當前pid namespace的bash(1)接管了 root@container003:~# kill 77 root@container003:~# pstree -p bash(1)─┬─bash(26)───bash(36)───pstree(82) └─sleep(78) #從新回到剛纔的狀態,後面將嘗試在第三個窗口中kill掉這裏的unshare進程 root@container003:~# kill 78 root@container003:~# unshare --fork nohup sleep 3600& root@container003:~# pstree -p bash(1)───bash(26)───bash(36)─┬─pstree(85) └─unshare(83)───sleep(84) #--------------------------第三個shell窗口---------------------- #來到第三個窗口 root@container001:/# pstree -p bash(1)───unshare(22)───bash(23)───unshare(44)───bash(45)───bash(113)─ ──bash(123)───unshare(170)───sleep(171) #kill掉sleep(171)的父近程unshare(170), root@container001:/# kill 170 #結果顯示sleep(171)被bash(45)接管了,而不是bash(1), #進一步說明container003裏的進程只會被container003裏的pid 1進程接管, #而不會被外面container001的pid 1進程接管 root@container001:/# pstree -p bash(1)───unshare(22)───bash(23)───unshare(44)── ─bash(45)─┬─bash(113)───bash(123) └─sleep(171) #kill掉container002中pid 1的bash進程,在container001中,對應的是bash(23) root@container001:/# kill 23 #根本沒反應,說明bash不接收TERM信號(kill默認發送SIGTERM信號) root@container001:/# pstree -p bash(1)───unshare(22)───bash(23)───unshare(44)── ─bash(45)─┬─bash(113)───bash(123) └─sleep(171) #試試SIGSTOP,貌似也不行 root@container001:/# kill -SIGSTOP 23 root@container001:/# pstree -p bash(1)───unshare(22)───bash(23)───unshare(44)── ─bash(45)─┬─bash(113)───bash(123) └─sleep(171) #最後試試殺手鐗SIGKILL,馬到成功 root@container001:/# kill -SIGKILL 23 root@container001:/# pstree -p bash(1) #--------------------------第一個shell窗口---------------------- #container003和container002的bash退出了, #第一個shell窗口直接退到了container001的bash root@container003:~# Killed root@container001:~# #--------------------------第二個shell窗口---------------------- #經過nsenter方式加入到container002的bash也被kill掉了 root@container002:/# Killed dev@ubuntu:~$ #從結果能夠看出,container002的「init」進程被殺死後, #內核將會發送SIGKILL給container002裏的全部進程, #這樣致使container002及它全部子孫namespace裏的進程都殺死, #同時container002和container003也被銷燬
man-pages裏面說SIGSTOP也能夠kill掉子namespace裏的「init」進程,但我在上面試了下,沒效果,具體緣由未知。
一般狀況下,若是PID namespace中的進程都退出了,這個namespace將會被銷燬,但就如在前面「Namespace概述」裏介紹的,有兩種狀況會致使就算進程都退出了,這個namespace還會存在。但對於PID namespace來講,就算namespace還在,因爲裏面沒有「init」進程,Kernel不容許其它進程加入到這個namespace,因此這個存在的namespace沒有意義
當一個PID經過UNIX domain socket在不一樣的PID namespace中傳輸時(請參考unix(7)裏面的SCM_CREDENTIALS),PID將會自動轉換成目的namespace中的PID.