搞懂容器技術的基石: namespace (上)

張晉濤發表於2021-12-15

大家好,我是張晉濤。

目前我們所提到的容器技術、虛擬化技術(不論何種抽象層次下的虛擬化技術)都能做到資源層面上的隔離和限制。

對於容器技術而言,它實現資源層面上的限制和隔離,依賴於 Linux 核心所提供的 cgroup 和 namespace 技術。

我們先對這兩項技術的作用做個概括:

  • cgroup 的主要作用:管理資源的分配、限制;
  • namespace 的主要作用:封裝抽象,限制,隔離,使名稱空間內的程式看起來擁有他們自己的全域性資源;

在上一篇文章中,我們重點聊了 cgroup 。本篇,我們重點來聊 namespace 。

Namespace 是什麼?

我們引用 wiki 上對 namespace 的定義:

Namespaces are a feature of the Linux kernel that partitions kernel resources such that one set of processes sees one set of resources while another set of processes sees a different set of resources. The feature works by having the same namespace for a set of resources and processes, but those namespaces refer to distinct resources.

namespace 是 Linux 核心的一項特性,它可以對核心資源進行分割槽,使得一組程式可以看到一組資源;而另一組程式可以看到另一組不同的資源。該功能的原理是為一組資源和程式使用相同的 namespace,但是這些 namespace 實際上引用的是不同的資源。

這樣的說法未免太繞了些,簡單來說 namespace 是由 Linux 核心提供的,用於程式間資源隔離的一種技術。將全域性的系統資源包裝在一個抽象裡,讓程式(看起來)擁有獨立的全域性資源例項。同時 Linux 也預設提供了多種 namespace,用於對多種不同資源進行隔離。

在之前,我們單獨使用 namespace 的場景比較有限,但 namespace 卻是容器化技術的基石。

我們先來看看它的發展歷程。

Namespace 的發展歷程

img

圖 1 ,namespace 的歷史過程

最早期 - Plan 9

namespace 的早期提出及使用要追溯到 Plan 9 from Bell Labs ,貝爾實驗室的 Plan 9。這是一個分散式作業系統,由貝爾實驗室的計算科學研究中心在八幾年至02年開發的(02年釋出了穩定的第四版,距離92年釋出的第一個公開版本已10年打磨),現在仍然被作業系統的研究者和愛好者開發使用。在 Plan 9 的設計與實現中,我們著重提以下3點內容:

  • 檔案系統:所有系統資源都列在檔案系統中,以 Node 標識。所有的介面也作為檔案系統的一部分呈現。
  • Namespace:能更好的應用及展示檔案系統的層次結構,它實現了所謂的 “分離”和“獨立”。
  • 標準通訊協議:9P協議(Styx/9P2000)。

img

圖 2 ,Plan 9 from Bell Labs 圖示

開始加入 Linux Kernel

Namespace 開始進入 Linux Kernel 的版本是在 2.4.X,最初始於 2.4.19 版本。但是,自 2.4.2 版本才開始實現每個程式的 namespace。

img

圖 3 ,Linux Kernel Note

img

圖 4 ,Linux Kernel 對應的各作業系統版本

Linux 3.8 基本實現

Linux 3.8 中終於完全實現了 User Namespace 的相關功能整合到核心。這樣 Docker 及其他容器技術所用到的 namespace 相關的能力就基本都實現了。

img

圖 5 ,Linux Kernel 從 2001 到2013 逐步演變,完成了 namespace 的實現

Namespace 型別

namespace名稱使用的標識 - Flag控制內容
CgroupCLONE_NEWCGROUPCgroup root directory cgroup 根目錄
IPCCLONE_NEWIPCSystem V IPC, POSIX message queues訊號量,訊息佇列
NetworkCLONE_NEWNETNetwork devices, stacks, ports, etc.網路裝置,協議棧,埠等等
MountCLONE_NEWNSMount points掛載點
PIDCLONE_NEWPIDProcess IDs程式號
TimeCLONE_NEWTIME時鐘
UserCLONE_NEWUSER使用者和組 ID
UTSCLONE_NEWUTS系統主機名和 NIS(Network Information Service) 主機名(有時稱為域名)

Cgroup namespaces

Cgroup namespace 是程式的 cgroups 的虛擬化檢視,通過 /proc/[pid]/cgroup/proc/[pid]/mountinfo 展示。

使用 cgroup namespace 需要核心開啟 CONFIG_CGROUPS 選項。可通過以下方式驗證:

(MoeLove) ➜ grep CONFIG_CGROUPS /boot/config-$(uname -r)
CONFIG_CGROUPS=y

cgroup namespace 提供的了一系列的隔離支援:

  • 防止資訊洩漏(容器不應該看到容器外的任何資訊)。
  • 簡化了容器遷移。
  • 限制容器程式資源,因為它會把 cgroup 檔案系統進行掛載,使得容器程式無法獲取上層的訪問許可權。

每個 cgroup namespace 都有自己的一組 cgroup 根目錄。這些 cgroup 的根目錄是在 /proc/[pid]/cgroup 檔案中對應記錄的相對位置的基點。當一個程式用 CLONE_NEWCGROUP(clone(2) 或者 unshare(2)) 建立一個新的 cgroup namespace時,它當前的 cgroups 的目錄就變成了新 namespace 的 cgroup 根目錄。

(MoeLove) ➜ cat /proc/self/cgroup 
0::/user.slice/user-1000.slice/session-2.scope

當一個目標程式從 /proc/[pid]/cgroup 中讀取 cgroup 關係時,每個記錄的路徑名會在第三欄位中展示,會關聯到正在讀取的程式的相關 cgroup 分層結構的根目錄。如果目標程式的 cgroup 目錄位於正在讀取的程式的 cgroup namespace 根目錄之外時,那麼,路徑名稱將會對每個 cgroup 層次中的上層節點顯示 ../

我們來看看下面的示例(這裡以 cgroup v1 為例,如果你想看 v2 版本的示例,請在留言中告訴我):

  1. 在初始的 cgroup namespace 中,我們使用 root (或者有 root 許可權的使用者),在 freezer 層下建立一個子 cgroup 名為 moelove-sub,同時,將程式放入該 cgroup 進行限制。
(MoeLove) ➜ mkdir -p /sys/fs/cgroup/freezer/moelove-sub
(MoeLove) ➜ sleep 6666666 & 
[1] 1489125                  
(MoeLove) ➜ echo 1489125 > /sys/fs/cgroup/freezer/moelove-sub/cgroup.procs
  1. 我們在 freezer 層下建立另外一個子 cgroup,名為 moelove-sub2, 並且再放入執行程式號。可以看到當前的程式已經納入到 moelove-sub2的 cgroup 下管理了。
(MoeLove) ➜ mkdir -p /sys/fs/cgroup/freezer/moelove-sub2
(MoeLove) ➜ echo $$
1488899
(MoeLove) ➜ echo 1488899 > /sys/fs/cgroup/freezer/moelove-sub2/cgroup.procs 
(MoeLove) ➜ cat /proc/self/cgroup |grep freezer
7:freezer:/moelove-sub2
  1. 我們使用 unshare(1) 建立一個程式,這裡使用了 -C參數列示是新的 cgroup namespace, 使用了 -m參數列示是新的 mount namespace。
(MoeLove) ➜ unshare -Cm bash
root@moelove:~#
  1. 從用 unshare(1) 啟動的新 shell 中,我們可以在 /proc/[pid]/cgroup 檔案中看到,新 shell 和以上示例中的程式:
root@moelove:~# cat /proc/self/cgroup | grep freezer
7:freezer:/
root@moelove:~# cat /proc/1/cgroup | grep freezer
7:freezer:/..

# 第一個示例程式
root@moelove:~# cat /proc/1489125/cgroup | grep freezer
7:freezer:/../moelove-sub
  1. 從上面的示例中,我們可以看到新 shell 的 freezer cgroup 關係中,當新的 cgroup namespace 建立時,freezer cgroup 的根目錄與它的關係也就建立了。
root@moelove:~# cat /proc/self/mountinfo | grep freezer
1238 1230 0:37 /.. /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,freezer
  1. 第四個欄位 ( /..) 顯示了在 cgroup 檔案系統中的掛載目錄。從 cgroup namespaces 的定義中,我們可以知道,程式當前的 freezer cgroup 目錄變成了它的根目錄,所以這個欄位顯示 /.. 。我們可以重新掛載來處理它。
root@moelove:~# mount --make-rslave /
root@moelove:~# umount /sys/fs/cgroup/freezer
root@moelove:~# mount -t cgroup -o freezer freezer /sys/fs/cgroup/freezer
root@moelove:~# cat /proc/self/mountinfo | grep freezer
1238 1230 0:37 / /sys/fs/cgroup/freezer rw,relatime - cgroup freezer rw,freezer
root@moelove:~# mount |grep freezer
freezer on /sys/fs/cgroup/freezer type cgroup (rw,relatime,freezer)

IPC namespaces

IPC namespaces 隔離了 IPC 資源,如 System V IPC objects、 POSIX message queues。每個 IPC namespace 都有著自己的一組 System V IPC 識別符號,以及 POSIX 訊息佇列系統。在一個 IPC namespace 中建立的物件,對所有該 namespace 下的成員均可見(對其他 namespace 下的成員均不可見)。

使用 IPC namespace 需要核心支援 CONFIG_IPC_NS 選項。如下:

(MoeLove) ➜ grep CONFIG_IPC_NS /boot/config-$(uname -r)
CONFIG_IPC_NS=y

可以在 IPC namespace 中設定以下 /proc 介面:

  • /proc/sys/fs/mqueue - POSIX 訊息佇列介面
  • /proc/sys/kernel - System V IPC 介面 (msgmax, msgmnb, msgmni, sem, shmall, shmmax, shmmni, shm_rmid_forced)
  • /proc/sysvipc - System V IPC 介面

當 IPC namespace 被銷燬時(空間裡的最後一個程式都被停止刪除時),在 IPC namespace 中建立的 object 也會被銷燬。

Network namepaces

Network namespaces 隔離了與網路相關的系統資源(這裡羅列一些):

  • network devices - 網路裝置
  • IPv4 and IPv6 protocol stacks - IPv4、IPv6 的協議棧
  • IP routing tables - IP 路由表
  • firewall rules - 防火牆規則
  • /proc/net (即 /proc/PID/net)
  • /sys/class/net
  • /proc/sys/net 目錄下的檔案
  • 埠、socket
  • UNIX domain abstract socket namespace

使用 Network namespaces 需要核心支援 CONFIG_NET_NS 選項。如下:

(MoeLove) ➜ grep CONFIG_NET_NS /boot/config-$(uname -r)
CONFIG_NET_NS=y

一個物理網路裝置只能存在於一個 Network namespace 中。當一個 Network namespace 被釋放時(空間裡的最後一個程式都被停止刪除時),物理網路裝置將被移動到初始的 Network namespace 而不是上層的 Network namespace。

一個虛擬的網路裝置(veth(4)) ,在 Network namespace 間通過一個類似管道的方式進行連線。這使得它能存在於多個 Network namespace,但是,當 Network namespace 被摧毀時,該空間下包含的 veth(4) 裝置可能被破壞。

Mount namespaces

Mount namespaces 最早出現在 Linux 2.4.19 版本。Mount namespaces 隔離了各空間中掛載的程式例項。每個 mount namespace 的例項下的程式會看到不同的目錄層次結構。

每個程式在 mount namespace 中的描述可以在下面的檔案檢視中看到:

  • /proc/[pid]/mounts
  • /proc/[pid]/mountinfo
  • /proc/[pid]/mountstats

一個新的 Mount namespace 的建立標識是 CLONE_NEWNS ,使用了 clone(2) 或者 unshare(2) 。

  • 如果 Mount namespace 用 clone(2) 建立,子 namespace 的掛載列表是從父程式的 mount namespace 拷貝的。
  • 如果 Mount namespace 用 unshare(2) 建立,新 namespace 的掛載列表是從呼叫者之前的 moun namespace 拷貝的。

如果 mount namespace 發生了修改,會引起什麼樣的連鎖反應?下面,我們就在 共享子樹中談談。

每個 mount 都被可以有如下標記 :

  • MS_SHARED - 與組內每個成員分享 events 。也就是說相同的 mount 或者 unmount 將自動發生在組內其他的 mounts 中。反之,mount 或者 unmount 事件 也會影響這次的 event 動作。
  • MS_PRIVATE - 這個 mount 是私有的。mount 或者 unmount events 都不會影響這次的 event 動作。
  • MS_SLAVE - mount 或者 unmount events 會從 master 節點傳入影響該節點。但是這個節點下的 mount 或者 unmount events 不會影響組內的其他節點。
  • MS_UNBINDABLE - 這也是個私有的 mount 。任何嘗試繫結的 mount 在這個設定下都將失敗。

在檔案 /proc/[pid]/mountinfo 中可以看到 propagation 型別的欄位。每個對等組都會由核心生成唯一的 ID ,同一對等組的 mount 都是這個 ID(即,下文中的 X )。

(MoeLove) ➜ cat /proc/self/mountinfo  |grep root  
65 1 0:33 /root / rw,relatime shared:1 - btrfs /dev/nvme0n1p6 rw,seclabel,compress=zstd:1,ssd,space_cache,subvolid=256,subvol=/root
1210 65 0:33 /root/var/lib/docker/btrfs /var/lib/docker/btrfs rw,relatime shared:1 - btrfs /dev/nvme0n1p6 rw,seclabel,compress=zstd:1,ssd,space_cache,subvolid=256,subvol=/root
  • shared:X - 在組 X 中共享。
  • master:X - 對於組 X 而言是 slave,即,從屬於 ID 為 X 的主。
  • propagate_from:X - 接收從組 X 發出的共享 mount。這個標籤總是個 master:X 一同出現。
  • unbindable - 表示不能被繫結,即,不與其他關聯從屬。

新 mount namespace 的傳播型別取決於它的父節點。如果父節點的傳播型別是 MS_SHARED ,那麼新 mount namespace 的傳播型別是 MS_SHARED ,不然會預設為 MS_PRIVATE。

關於 mount namespaces 我們還需要注意以下幾點:

(1)每個 mount namespace 都有一個 owner user namespace。如果新的 mount namespace 和拷貝的 mount namespace 分屬於不同的 user namespace ,那麼,新的 mount namespace 優先順序低。

(2)當建立的 mount namespace 優先順序低時,那麼,slave 的 mount events 會優先於 shared 的 mount events。

(3)高優先順序和低優先順序的 mount namespace 有關聯被鎖定在一起時,他們都不能被單獨解除安裝。

(4)mount(2) 標識和 atime 標識會被鎖定,即,不能被傳播影響而修改。

小結

以上就是關於 Linux 核心中 namespace 的一些介紹了,篇幅原因,剩餘部分以及 namespace 在容器中的應用我們放在下一篇中介紹,敬請期待!


歡迎訂閱我的文章公眾號【MoeLove】

TheMoeLove

相關文章