PID Namespace 的特性#

PID Namespace 隔離行程編號(Process ID)。在新的 PID Namespace 內:

  • 第一個被建立的行程 PID 為 1,扮演類似 init 的角色
  • 該 PID 1 行程一旦結束,整個 namespace 內所有行程都會被殺掉
  • 父 namespace 仍然可以看到子 namespace 的所有行程,但 PID 編號不同
  • 子 namespace 看不到父 namespace 中的任何行程

PID 1 在 namespace 內負責處理孤兒行程(orphan)回收與訊號預設行為,若應用程式本身不具備 init 能力,常會搭配 tini 之類的小型 init。

用 unshare 體驗 PID Namespace#

最快速體驗 PID Namespace 的方式是 unshare(2) 指令:

sudo unshare --pid --fork /bin/bash

說明:

  • --pid:建立新的 PID Namespace
  • --fork:先 fork 一個子行程,由子行程進入新 namespace 當 PID 1(unshare 本身不能直接成為 PID 1)

進入後,可以執行:

echo $$        # 預期看到 1(這個 shell 自己是 PID 1)
ps -ef         # 但這裡看到的可能還是 host 的所有行程

為什麼 ps 看到的不是「乾淨的」行程列表?因為 ps 是讀 /proc,而我們還沒有重新掛載 procfs。這個陷阱會在下一章 Mount Namespace 進一步說明。

觀察 /proc 的 PID 對應#

在 host 上,可以透過 /proc/<pid>/status 看到一個行程在不同 PID Namespace 的編號:

# 在 host 上對某個容器內的行程
cat /proc/<host_pid>/status | grep -E 'NSpid|Pid'

NSpid: 欄位會列出該行程在「自己所屬的每一層 PID Namespace」中的 PID,由外向內排列。例如最外層為 host PID,最內層為容器內看到的 PID。

不同 Kernel 版本欄位略有差異,重點是同一個行程會擁有多組 PID,每組對應一層 PID Namespace。

巢狀 PID Namespace#

PID Namespace 可以巢狀(nested):

  • namespace A 裡面再建立 namespace B,B 中行程在 A 裡也有對應 PID
  • 越外層看到的行程越多,越內層越少
  • 行程一旦進入更內層 namespace,就無法回到外層

這也說明了「為什麼從 host 可以看到容器內的行程,但反之不行」。

與其他 Namespaces 的關係#

只開 PID Namespace 通常不夠用:

  • 不開 Mount Namespace:/proc 仍然是 host 的 procfs,看到 host 的所有 PID
  • 不開 Network Namespace:行程仍與 host 共用網路
  • 不開 UTS Namespace:hostname 變更會影響整個系統

因此真正的容器一定是「多種 namespace 同時建立」。

延伸閱讀#

  • man 7 pid_namespaces
  • Linux Kernel 文件:Documentation/admin-guide/namespaces/
  • man 1 unshare