# 系統程式設計 - Namespaces
[TOC]
## References
*Michael Kerrisk* 是 TLPI 的作者,也是 `man-pages` 的維護者~~所以可能聽一聽會以為他在唸照著 `man` 唸~~。如果懶得聽他講的話,也可以直接去看 `man`。同時他在 LWN 上也以 [*Namespaces in Operation*](https://lwn.net/Articles/531114/) 為題,寫了一系列介紹不同種類的 namespace 的文章及範例程式。
> 以下的資料多半會講「namespace 有 7 種」。但在 2020 年一月 (5.6 版的 kernel) 時,新增了一個 Time Namespace。所以實際上現在已經有 8 種了。
### Containers unplugged (Part 1): Linux namespaces - Michael Kerrisk
{%youtube 0kJPa-1FuoI %}
### Rootless Containers from Scratch - Liz Rice, Aqua Security
{%youtube jeTKgAEyhsA %}
### Linux Container Primitives: cgroups, namespaces, and more! (10:09 ~ 20:35)
{%youtube x1npPrzyKfs %}
### Sandboxing a Linux application - Martin Ertsås - NDC TechTown 2021
{%youtube SxK-hccyoTc %}
## Namesapce 簡介
Linux 中的 [`namespace(7)`](https://man7.org/linux/man-pages/man7/namespaces.7.html) 機制是一種~~蒙蔽了行程的雙眼~~「讓一個行程只能看見作業系統中的部分資源」的機制。這些資源可能包含檔案系統、使用者、時間、網路等等。
Namespace 其中一個應用是「隔離」同一個作業系統上的不同行程。把一個行程放在某一個種類的 namespace 中,它就會只看得到該 namespace 下看得到的資源。儘管處在該 namespace 中的行程可能以為自己可以存取整個根目錄、以為自己是它 root,但在 namespace 以外的行程看來只是一個普通權限的行程。而這也是容器 (container) 的基礎。
### Namespace 的種類
namespace 很多種。不同種類的 namespace 可以用來隔離不同的資源。就寫作時間來說總共有 8 種。可以在 [`namespaces(7)`](https://man7.org/linux/man-pages/man7/namespaces.7.html) 的 `man` 中查到。而他們也各自有自己的 `man`:
```c
Namespace Flag Page Isolates
Cgroup CLONE_NEWCGROUP cgroup_namespaces(7) Cgroup root directory
IPC CLONE_NEWIPC ipc_namespaces(7) System V IPC,
POSIX message queues
Network CLONE_NEWNET network_namespaces(7) Network devices,
stacks, ports, etc.
Mount CLONE_NEWNS mount_namespaces(7) Mount points
PID CLONE_NEWPID pid_namespaces(7) Process IDs
Time CLONE_NEWTIME time_namespaces(7) Boot and monotonic
clocks
User CLONE_NEWUSER user_namespaces(7) User and group IDs
UTS CLONE_NEWUTS uts_namespaces(7) Hostname and NIS
domain name
```
## 相關命令
### `unshare` --- 新增 Namespace
不同種類的 namespace 可以隔離出不同種類的資源給該 namespace 下的行程使用。而要使一個行程加入某一個 namespace 中,可以改變 `clone` 的 `flag` 參數,使得新行程建立的同時也建立一個新的 namespace ,並讓這個新行程加入這個新的 namespace; 或是在事後呼叫 `unshare(2)` 或 `setns(2)` 來改變一個行程所處的 namespace。在 [`namespaces(7)`](https://man7.org/linux/man-pages/man7/namespaces.7.html) 中的 *The namespaces API* 有更詳細的說明。
### 例子:`unshare -u` --- 新增 UTS Namespace
UTS 是 *Unix Time-Sharing* 的縮寫。處在不同 UTS namespace 中的行程,可以有不同的 hostname 與 domain name。也就是 `hostname` 跟 `hostname -d` 會得到的那個名稱。
![](https://i.imgur.com/NFNKTCw.png)
舉例來說,開兩個終端機視窗(或 `tmux` 開兩個分割)。假定一開始的 hostname 是 `ubuntu`:
```clike
ubuntu@ubuntu:~$ hostname
ubuntu
```
這時,如果在其中一個視窗使用 `unshare` 的 `-u` 選項新增一個 UTS namespace。那麼「改變 hostname」這件事情只會被該 UTS namespace 中的行程看到,而不是這個 namesapce 中的行程則不會看到 hostname 發生改變。舉例來說:
```clike
ubuntu@ubuntu:~$ sudo unshare -u
root@ubuntu:/home/ubuntu# hostname
ubuntu
root@ubuntu:/home/ubuntu# hostname nu-est
root@ubuntu:/home/ubuntu# hostname
nu-est
```
但是在另外一個終端機視窗使用 `hostname`,會發現裡面的 hostname 還是一樣:
```clike
ubuntu@ubuntu:~$ hostname
ubuntu
```
### `/proc/PID/ns` --- 行程所處的 Namespaces
對於一個 PID 為 `PID` 的行程,其所處的 namespace 可以藉由檢視 `/proc/PID/ns` 目錄底下的 symlink 來得知:
```clike
ls /proc/$$/ns
cgroup mnt pid time user
ipc net pid_for_children time_for_children uts
```
這些 symlink 中的文字代表該 namespace 的種類,而編號則作為 namesapce 的唯一識別。舉例來說,在剛剛新建立的 UTS namespace 中,若讀取該檔案,會出現
```clike
root@ubuntu:/# readlink /proc/$$/ns/uts
uts:[4026532364]
```
但在原先的 namespace 中,則會出現不一樣的編號:
```clike
ubuntu@ubuntu:~$ readlink /proc/$$/ns/uts
uts:[4026531838]
```
這表示兩者處在不同的 UTS namespace。但是另外一方面,如果查看兩者的 mount namespace,則會發現兩者是一樣的。輸出分別是:
```clike
ubuntu@ubuntu:~$ readlink /proc/$$/ns/mnt
mnt:[4026531840]
```
以及:
```clike
root@ubuntu:/home/ubuntu# readlink /proc/$$/ns/mnt
mnt:[4026531840]
```
### `nsenter` --- 進入某個行程所屬的某種 namespace
除了新建一個 namespace 之外,也可以使用 `nsenter` 這個命令去進入已知的 namespace。舉例來說,假定處在剛剛的 UTS namespace 的 `bash` 之 PID 為 `2391`,而另外一個不處在該 namespace 的 `bash` 之 PID 為 `2368`。則 `nsenter` 可以指定「把當下的行程加入某個行程所處的某個 namespace」舉例來說:
```clike
ubuntu@ubuntu:~$ readlink /proc/$$/ns/uts
uts:[4026531838]
ubuntu@ubuntu:~$ sudo nsenter --target 2391 --uts
root@ubuntu:/home/ubuntu# readlink /proc/$$/ns/uts
uts:[4026532364]
```
### `ps` 中的選項
`ps` 這個命令也可以列出行程所屬的某些 namespace。可以在 [`ps(1)`](https://man7.org/linux/man-pages/man1/ps.1.html) 中搜尋 namespace。比如說使用
```clike
root@ubuntu:/home/ubuntu# ps -eo user,uid,pid,comm,utsns
USER UID PID COMMAND UTSNS
root 0 1 systemd 4026531838
root 0 2 kthreadd 4026531838
root 0 3 rcu_gp 4026531838
root 0 4 rcu_par_gp 4026531838
... ... ... ...
root 0 2437 sudo 4026531838
root 0 2438 bash 4026532364
root 0 2445 kworker/1:2-eve 4026531838
root 0 2452 ps 4026532364
```
或是一次列出所有 `ps` 能列出的 namespace:
```shell
$ ps -eo user,pid,comm,utsns,mntns,netns,pidns,ipcns
```
## Mount Namespace
### Mount Namespace = Mount Point 的集合
Linux 上的所有檔案形成一個樹狀結構,而這個樹狀結構的根節點就是 `/`。這個樹狀結構中的不同檔案可能位於不同的裝置上。「掛載一個檔案系統」的意思是在現有的這個由檔案形成的樹當中的某個節點下,額外接上一個由該檔案系統中的檔案形成的樹。而這個接上另外一棵樹的「某個地方」就稱為 mount point。對於一個 mount namespace,處於當中的所有行程都會看到相同的 mount point。而不同 mount space 中,對於 mount point 的改變則
### 例子:在 Mount Namespace 中 `umount`
如果建立一個新的 mount namespace,並且在裡面 `umount` 某些 mount point,那麼這個 `umount` 只會影響這個 mount namespace 中的行程所看到的 mount point。舉例來說,如果建立一個 mount namespace 之後:
```clike
ubuntu@ubuntu:~$ sudo unshare -m
root@ubuntu:/home/ubuntu#
root@ubuntu:/home/ubuntu# cat /proc/$$/mountstats
device /dev/mmcblk0p2 mounted on / with fstype ext4
device udev mounted on /dev with fstype devtmpfs
device devpts mounted on /dev/pts with fstype devpts
device tmpfs mounted on /dev/shm with fstype tmpfs
device mqueue mounted on /dev/mqueue with fstype mqueue
device hugetlbfs mounted on /dev/hugepages with fstype hugetlbfs
device tmpfs mounted on /run with fstype tmpfs
device tmpfs mounted on /run/lock with fstype tmpfs
device tmpfs mounted on /run/snapd/ns with fstype tmpfs
device tmpfs mounted on /run/user/1000 with fstype tmpfs
device sysfs mounted on /sys with fstype sysfs
device securityfs mounted on /sys/kernel/security with fstype securityfs
device cgroup2 mounted on /sys/fs/cgroup with fstype cgroup2
device pstore mounted on /sys/fs/pstore with fstype pstore
device none mounted on /sys/fs/bpf with fstype bpf
device debugfs mounted on /sys/kernel/debug with fstype debugfs
device tracefs mounted on /sys/kernel/tracing with fstype tracefs
device fusectl mounted on /sys/fs/fuse/connections with fstype fusectl
device configfs mounted on /sys/kernel/config with fstype configfs
device proc mounted on /proc with fstype proc
device systemd-1 mounted on /proc/sys/fs/binfmt_misc with fstype autofs
device /dev/loop0 mounted on /snap/core20/1171 with fstype squashfs
device /dev/loop1 mounted on /snap/core20/1244 with fstype squashfs
device /dev/loop2 mounted on /snap/lxd/21863 with fstype squashfs
device /dev/loop3 mounted on /snap/snapd/13643 with fstype squashfs
device /dev/loop5 mounted on /snap/snapd/14063 with fstype squashfs
device /dev/loop4 mounted on /snap/lxd/21901 with fstype squashfs
device /dev/mmcblk0p1 mounted on /boot/firmware with fstype vfat
```
使用 `umount` 的 `-a` 盡可能地卸載所有 mount point:
```clike
root@ubuntu:/home/ubuntu# sudo umount -a
umount: /dev: target is busy.
umount: /: target is busy.
root@ubuntu:/home/ubuntu# cat /proc/$$/mountstats
device /dev/mmcblk0p2 mounted on / with fstype ext4
device udev mounted on /dev with fstype devtmpfs
device devpts mounted on /dev/pts with fstype devpts
device sysfs mounted on /sys with fstype sysfs
device proc mounted on /proc with fstype proc
```
雖然在這個 mount namespace 的 shell 中,明顯少掉了許多 mount point,但對於不處於這個 mount namespace 中的 shell,則會看到跟原先一樣的內容:
```clike
ubuntu@ubuntu:~$ cat /proc/$$/mountinfo
device sysfs mounted on /sys with fstype sysfs
device proc mounted on /proc with fstype proc
device udev mounted on /dev with fstype devtmpfs
device devpts mounted on /dev/pts with fstype devpts
device tmpfs mounted on /run with fstype tmpfs
device /dev/mmcblk0p2 mounted on / with fstype ext4
device securityfs mounted on /sys/kernel/security with fstype securityfs
device tmpfs mounted on /dev/shm with fstype tmpfs
device tmpfs mounted on /run/lock with fstype tmpfs
device cgroup2 mounted on /sys/fs/cgroup with fstype cgroup2
device pstore mounted on /sys/fs/pstore with fstype pstore
device none mounted on /sys/fs/bpf with fstype bpf
device systemd-1 mounted on /proc/sys/fs/binfmt_misc with fstype autofs
device mqueue mounted on /dev/mqueue with fstype mqueue
device hugetlbfs mounted on /dev/hugepages with fstype hugetlbfs
device debugfs mounted on /sys/kernel/debug with fstype debugfs
device tracefs mounted on /sys/kernel/tracing with fstype tracefs
device fusectl mounted on /sys/fs/fuse/connections with fstype fusectl
device configfs mounted on /sys/kernel/config with fstype configfs
device /dev/loop0 mounted on /snap/core20/1171 with fstype squashfs
device /dev/loop1 mounted on /snap/core20/1244 with fstype squashfs
device /dev/loop2 mounted on /snap/lxd/21863 with fstype squashfs
device /dev/loop3 mounted on /snap/snapd/13643 with fstype squashfs
device /dev/loop5 mounted on /snap/snapd/14063 with fstype squashfs
device /dev/loop4 mounted on /snap/lxd/21901 with fstype squashfs
device /dev/mmcblk0p1 mounted on /boot/firmware with fstype vfat
device tmpfs mounted on /run/snapd/ns with fstype tmpfs
device nsfs mounted on /run/snapd/ns/lxd.mnt with fstype nsfs
device tmpfs mounted on /run/user/1000 with fstype tmpfs
```
如果想要進一步將受到這個 mount namespace 中有東西真的被 `umount` 的話,可以先在某個 mount namespace 中 `umount -a`,並且檢視某一個會被 `umount` 的檔案系統。比如 `/sys/kernel/tracing`:
```clike
ubuntu@ubuntu:~$ sudo unshare -m
root@ubuntu:/home/ubuntu# ls /sys/kernel/tracing
README kprobe_profile stack_max_size
available_events max_graph_depth stack_trace
available_filter_functions options stack_trace_filter
available_tracers per_cpu synthetic_events
buffer_percent printk_formats timestamp_mode
buffer_size_kb saved_cmdlines trace
buffer_total_size_kb saved_cmdlines_size trace_clock
current_tracer saved_tgids trace_marker
dyn_ftrace_total_info set_event trace_marker_raw
dynamic_events set_event_notrace_pid trace_options
enabled_functions set_event_pid trace_pipe
error_log set_ftrace_filter trace_stat
events set_ftrace_notrace tracing_cpumask
free_buffer set_ftrace_notrace_pid tracing_max_latency
function_profile_enabled set_ftrace_pid tracing_on
hwlat_detector set_graph_function tracing_thresh
instances set_graph_notrace uprobe_events
kprobe_events snapshot uprobe_profile
root@ubuntu:/home/ubuntu# sudo umount -a
umount: /dev: target is busy.
umount: /: target is busy.
root@ubuntu:/home/ubuntu# ls /sys/kernel/tracing
root@ubuntu:/home/ubuntu#
```
這時候會發現目錄裡面什麼東西都沒有。但是在這個 mount namespace 之外的 `bash`,還是可以看見這個目錄裡面有東西。
## PID Namespace
### 簡介
處於不同 PID namespace 的行程,彼此可以具有重複的 PID。在 container 中的使用時機比如要把一個 container 放到另外一個機器使用時,這個 conainer 中的行程所具有的 PID 有可能會與這台機器上原先的行程有所重複。如果讓上面的行程在獨立的 PID namespace 中執行,就可以避免這個衝突的問題。另外一個例子是:單一作業系統上的多個 PID namespace 中可以各自有 PID 為 1 的行程(比如說各自有自己的 `init` 或 `systemd`),這就會使得這兩個 PID namespace (至少在行程管理上) 像是兩個獨立的作業系統。一個這樣的例子是 Yelp 的 [dumb-init](https://github.com/Yelp/dumb-init)。
舉例來說:
```clike
ubuntu@ubuntu:~$ sudo unshare -p -f bash
root@ubuntu:/home/ubuntu# echo $$
1
```
可以用 `pstree -N pid` 來依照 PID namespace 分類對應的行程:
```clike
ubuntu@ubuntu:~$ sudo unshare -p -f bash
root@ubuntu:/home/ubuntu# pstree -N pid
[4026531836]
systemd─┬─2*[agetty]
├─bluetoothd
├─cron
├─dbus-daemon
├─hciattach
├─irqbalance───{irqbalance}
├─multipathd───6*[{multipathd}]
├─networkd-dispat
├─polkitd───2*[{polkitd}]
├─rsyslogd───3*[{rsyslogd}]
├─snapd───13*[{snapd}]
├─sshd───sshd───sshd───bash───sudo───unshare
├─systemd───(sd-pam)
├─systemd-journal
├─systemd-logind
├─systemd-network
├─systemd-resolve
├─systemd-timesyn───{systemd-timesyn}
├─systemd-udevd
├─udisksd───4*[{udisksd}]
├─unattended-upgr───{unattended-upgr}
└─2*[wpa_supplicant]
[4026532365]
bash───pstree
```
### PID Namespace 有階層
跟前面的 namespace 不一樣的地方是:PID namespace 之間有階層關係。子行程所處的 PID namespace 中的所有行程,可以被所有祖輩行程的 PID namespace 中看見。並且這些行程在每個祖輩行程所處的 PID namespace 中,也都有自己對應的 PID。但相反地,子行程所處的 PID namespace 中,就無法看見祖輩所處的 PID namespace 中的其他行程:
![](https://i.imgur.com/DHzb9zz.jpg)
### `/proc` 中顯示的行程
如果只有幫新行程建立 PID namespace,那麼因為這個新行程仍然與原先的行程處在同一個 mount namespace,所以這時 `/proc` 底下還是看得到原先的行程:
```clike
ubuntu@ubuntu:~$ sudo unshare -p -f bash
root@ubuntu:/home/ubuntu# ls /proc
1 110 124 18 185 1960 2094 27 31 3180 46 96 bootconfig dynamic_debug keys net sysrq-trigger
10 111 13 1805 186 1964 21 2702 3118 3181 47 97 buddyinfo execdomains kmsg pagetypeinfo sysvipc
100 112 133 1806 1868 1965 2100 2784 3121 3188 48 971 bus fb kpagecgroup partitions thread-self
1006 113 136 1807 19 2 2280 2927 3132 32 49 972 cgroups filesystems kpagecount pressure timer_list
101 114 137 1808 1909 20 2284 2987 3135 33 743 98 cmdline fs kpageflags schedstat tty
1012 116 14 1818 1911 2012 2285 2990 3139 36 8 981 consoles interrupts loadavg scsi uptime
102 119 144 1820 192 2022 2286 2991 3149 37 809 982 cpuinfo iomem locks self version
103 12 145 1824 1943 2032 2383 3 3151 38 810 983 crypto ioports mdstat slabinfo version_signature
106 120 146 1828 1947 2037 2384 30 3162 4 9 985 device-tree irq meminfo softirqs vmallocinfo
107 121 15 1830 1950 2042 24 3019 3164 42 905 988 devices kallsyms misc stat vmstat
108 122 165 1834 1954 2043 25 3021 3174 44 937 99 diskstats kcore modules swaps zoneinfo
11 123 17 184 1956 2093 26 3095 3179 45 95 asound driver key-users mounts sys
```
如果這不是期待的行為,那麼可以考慮同時也幫這個行程建立一個 mount namespace,再重新 mount 一次 `/proc`:
```clike
ubuntu@ubuntu:~$ sudo unshare -p -f -m bash
root@ubuntu:/home/ubuntu# mount -t proc nodev /proc
root@ubuntu:/home/ubuntu# ls /proc
root@ubuntu:/home/ubuntu# ls /proc
1 cgroups devices filesystems kallsyms kpagecount misc pressure stat timer_list vmstat
9 cmdline diskstats fs kcore kpageflags modules schedstat swaps tty zoneinfo
asound consoles driver interrupts key-users loadavg mounts scsi sys uptime
bootconfig cpuinfo dynamic_debug iomem keys locks net self sysrq-trigger version
buddyinfo crypto execdomains ioports kmsg mdstat pagetypeinfo slabinfo sysvipc version_signature
bus device-tree fb irq kpagecgroup meminfo partitions softirqs thread-self vmallocinfo
```