Try   HackMD

系統程式設計 - Namespaces

References

Michael Kerrisk 是 TLPI 的作者,也是 man-pages 的維護者所以可能聽一聽會以為他在唸照著 man。如果懶得聽他講的話,也可以直接去看 man。同時他在 LWN 上也以 Namespaces in Operation 為題,寫了一系列介紹不同種類的 namespace 的文章及範例程式。

以下的資料多半會講「namespace 有 7 種」。但在 2020 年一月 (5.6 版的 kernel) 時,新增了一個 Time Namespace。所以實際上現在已經有 8 種了。

Containers unplugged (Part 1): Linux namespaces - Michael Kerrisk

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Rootless Containers from Scratch - Liz Rice, Aqua Security

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Linux Container Primitives: cgroups, namespaces, and more! (10:09 ~ 20:35)

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Sandboxing a Linux application - Martin Ertsås - NDC TechTown 2021

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Namesapce 簡介

Linux 中的 namespace(7) 機制是一種蒙蔽了行程的雙眼「讓一個行程只能看見作業系統中的部分資源」的機制。這些資源可能包含檔案系統、使用者、時間、網路等等。

Namespace 其中一個應用是「隔離」同一個作業系統上的不同行程。把一個行程放在某一個種類的 namespace 中,它就會只看得到該 namespace 下看得到的資源。儘管處在該 namespace 中的行程可能以為自己可以存取整個根目錄、以為自己是它 root,但在 namespace 以外的行程看來只是一個普通權限的行程。而這也是容器 (container) 的基礎。

Namespace 的種類

namespace 很多種。不同種類的 namespace 可以用來隔離不同的資源。就寫作時間來說總共有 8 種。可以在 namespaces(7)man 中查到。而他們也各自有自己的 man

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 中,可以改變 cloneflag 參數,使得新行程建立的同時也建立一個新的 namespace ,並讓這個新行程加入這個新的 namespace; 或是在事後呼叫 unshare(2)setns(2) 來改變一個行程所處的 namespace。在 namespaces(7) 中的 The namespaces API 有更詳細的說明。

例子:unshare -u - 新增 UTS Namespace

UTS 是 Unix Time-Sharing 的縮寫。處在不同 UTS namespace 中的行程,可以有不同的 hostname 與 domain name。也就是 hostnamehostname -d 會得到的那個名稱。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

舉例來說,開兩個終端機視窗(或 tmux 開兩個分割)。假定一開始的 hostname 是 ubuntu

ubuntu@ubuntu:~$ hostname
ubuntu

這時,如果在其中一個視窗使用 unshare-u 選項新增一個 UTS namespace。那麼「改變 hostname」這件事情只會被該 UTS namespace 中的行程看到,而不是這個 namesapce 中的行程則不會看到 hostname 發生改變。舉例來說:

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 還是一樣:

ubuntu@ubuntu:~$ hostname
ubuntu

/proc/PID/ns - 行程所處的 Namespaces

對於一個 PID 為 PID 的行程,其所處的 namespace 可以藉由檢視 /proc/PID/ns 目錄底下的 symlink 來得知:

ls /proc/$$/ns
cgroup  mnt  pid               time               user
ipc     net  pid_for_children  time_for_children  uts

這些 symlink 中的文字代表該 namespace 的種類,而編號則作為 namesapce 的唯一識別。舉例來說,在剛剛新建立的 UTS namespace 中,若讀取該檔案,會出現

root@ubuntu:/# readlink /proc/$$/ns/uts
uts:[4026532364]

但在原先的 namespace 中,則會出現不一樣的編號:

ubuntu@ubuntu:~$ readlink /proc/$$/ns/uts
uts:[4026531838]

這表示兩者處在不同的 UTS namespace。但是另外一方面,如果查看兩者的 mount namespace,則會發現兩者是一樣的。輸出分別是:

ubuntu@ubuntu:~$ readlink /proc/$$/ns/mnt
mnt:[4026531840]

以及:

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」舉例來說:

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) 中搜尋 namespace。比如說使用

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:

$ 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 之後:

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:

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,則會看到跟原先一樣的內容:

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

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 的行程(比如說各自有自己的 initsystemd),這就會使得這兩個 PID namespace (至少在行程管理上) 像是兩個獨立的作業系統。一個這樣的例子是 Yelp 的 dumb-init

舉例來說:

ubuntu@ubuntu:~$ sudo unshare -p -f bash
root@ubuntu:/home/ubuntu# echo $$
1

可以用 pstree -N pid 來依照 PID namespace 分類對應的行程:

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 中的其他行程:

/proc 中顯示的行程

如果只有幫新行程建立 PID namespace,那麼因為這個新行程仍然與原先的行程處在同一個 mount namespace,所以這時 /proc 底下還是看得到原先的行程:

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

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