Linux Namespace系列(05):pid namespace (CLONE_NEWPID)

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

PID namespace嵌套

  • 調用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:~$

「init」示例

當一個進程的父進程被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.

參考

相關文章
相關標籤/搜索