Docker 入門筆記 8 - Namespace 簡介(中)

Namespace 簡介(中)

PID Namespace

對於 Docker 來說,PID Namespace可以說非常重要,它可以使容器之間的進程樹互不可見。 通過PID Namespace, 每個容器中都會有一個進程號計數器, 容器內所有進程號會被重新編號。宿主機內核會維護各個容器中的進程樹,在樹的最頂端的進程號變爲1, 也就是 Init 進程。 此進程會作爲容器內其他所有進程的父進程來執行容器環境的初始化工作。

接下來,我們仍然用 unshare 命令測試一下PID Namespace的作用

查看當前狀態

在當前bash中運行

# echo $$
4682

#  ll /proc/$$/ns
總用量 0
dr-x--x--x 2 root root 0 Dec 29 16:04 ./
dr-xr-xr-x 9 root root 0 Dec 29 14:01 ../
lrwxrwxrwx 1 root root 0 Jan  1 20:41 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 root root 0 Jan  1 20:41 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 Jan  1 20:41 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0 Jan  1 20:41 net -> net:[4026531957]
lrwxrwxrwx 1 root root 0 Jan  1 20:41 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 Jan  1 20:41 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Jan  1 20:41 uts -> uts:[4026531838]

創建一個小的能打印自己pid 的c程序如下,

//getpid.c
#include<unistd.h>
#include<stdio.h>
main()
{
    printf("pid=%d  ppid=%d\n",getpid(), getppid());
}

編譯運行 getppid.c

#gcc getpid.c -o getpid
# ./getpid 
pid=4715  ppid=4682

運行一個新的bash並將它加入到新的pid namespace中

我們可以看到新的bash 在自己的名空間中獲得的ID爲1

# unshare --fork --pid /bin/bash
# echo $$
1

在這個新的bash中,如果我們運行小程序 getpid, 可以發現新進程的pid也是從屬於這個namespace的, 而bash進程起到了宿主機中init的作用。

#./getpid
pid=14  ppid=1

那麼原來的init進程呢?我們grep一下可以發現其實它一直都在。

# ps -ef |grep init |grep -v grep
root         1     0  0 08:21 ?        00:00:02 /sbin/init splash

再次在新的bash中運行ps命令

# ps
  PID TTY          TIME CMD
 4681 pts/2    00:00:00 su
 4682 pts/2    00:00:00 bash
 4717 pts/2    00:00:00 unshare
 4718 pts/2    00:00:00 bash
 4733 pts/2    00:00:00 ps


# ps -ef |grep unshare
root      9644  8010  0 16:32 pts/23   00:00:00 unshare --fork --pid /bin/bash
root      9663  9645  0 16:33 pts/23   00:00:00 grep --color=auto unshare


# ps -ef |egrep '8010|9644|9645'|grep -v 'grep'
root      8010  8009  0 13:40 pts/23   00:00:00 bash
root      9644  8010  0 16:32 pts/23   00:00:00 unshare --fork --pid /bin/bash
root      9645  9644  0 16:32 pts/23   00:00:00 /bin/bash
root      9711  9645  0 16:38 pts/23   00:00:00 ps -ef


# pstree 8009
su───bash───unshare───bash───pstree

可以發現雖然新的bash程序以及它的子進程在新的名空間中被分配了新的pid,但並不影響它在parent namesapce中獲得自己的pid。如圖:

這裏寫圖片描述

我們再做一個實驗。在parent pid namespace的shell 中啓動一個隨意一個進程

# gedit &
[1] 4877

進程啓動後我們可以發現它的pid是4887, 然後在新的pid namespace的bash中嘗試kill

# kill -9 4887
bash: kill: (4887) - no such process

實際上在child pid namespace中,我們無法訪問或者操縱parent pid namespace的進程。這就是PID namespace的隔離性質。

那麼爲什麼我們在新的bash裏輸入ps,top等命令,還是可以看得到所有進程呢。這是因爲,像ps,top這些命令會去讀/proc文件系統,而在上面的實驗中所有shell都訪問的是同一個/proc文件系統,所以這些命令顯示的東西都是一樣的。

因此,要想達到充分的隔離,我們還要對文件系統進行隔離,這就是下一節的內容。

Mount Namespace

Mount命名空間是第一個在Linux上實現的命名空間類型,出現在2002年。這個事實說明了相當通用的“NEWNS”綽號(簡稱“新命名空間”):那時沒有人似乎認爲其他 ,將來可能需要不同類型的名稱空間

Mount namespace隔離了名稱空間中的進程所看到的掛載點列表。或者,換一種說法,每個掛載名稱空間都有自己的掛載點列表,這意味着不同名稱空間中的進程可以看到並能夠操作單個目錄層次結構的不同視圖。

在2000 年,Al Viro 爲 Linux 引入了綁定掛載和文件系統名稱空間:
- 綁定掛載(bind mount)允許從任何其他位置訪問任何文件或目錄。
- 文件系統名稱空間(filesystem namespace)是與不同進程相關聯的完全獨立的文件系統樹。

系統第一次啓動時,會有一個單獨的掛載名稱空間,即所謂的“初始名稱空間”。通過使用CLONE_NEWNS標誌與clone()系統調用(在新的名稱空間中創建一個新的子進程)或unshare()系統調用(將調用者移入新的名稱空間)來創建新的Mount Namespace。當創建一個新的Mount Namespace時,它會收到從clone()或unshare()調用方的名稱空間複製的掛載點列表的副本。

在clone() 或unshare() 調用之後,可以在每個名稱空間(通過mount()和umount()))獨立添加和刪除掛載點。對安裝點列表的更改(默認情況下)僅對進程所在的安裝名稱空間中的進程可見;其他安裝名稱空間中的更改不可見。

儘管每個進程使用單獨的文件系統名稱空間在理論上非常有意義,但是在實踐中,完全隔離它們會造成過度限制。例如,假設將一個磁盤加載到一個光盤驅動器中。在最初的實現中,使所有mount namespace都可以看見這個磁盤唯一方法是在每個命名空間中單獨掛載磁盤。,因爲在最初的文件系統名稱空間中執行的掛載無法影響用戶的拷貝。而在許多情況下,用戶希望是執行一個單獨的掛載操作,使得磁盤在系統上的所有掛載命名空間(或者可能是某些子集)中都可見。

爲解決這一問題,Linux 2.6.15中又引入了共享子樹功能(2006年初,大約掛載命名空間的初始實現大約三年後)。共享子樹的主要優點是允許自動控制名稱空間之間掛載和卸載事件的傳播(mount propagation )。這意味着,例如,將光盤安裝在一個安裝名稱空間中,可以在所有其他名稱空間中觸發安裝該磁盤。

在共享子樹功能下,每個安裝點都會標記一個“傳播類型”,該傳播類型確定在此安裝點下創建和刪除的安裝點是否傳播到其他安裝點。有四種不同的傳播類型:

  • MS_SHARED:此掛載點與其他“掛載點”中的其他掛載點共享mount和unmount事件。在此掛載點下添加或刪除掛載點時,此更改將傳播到==peer group== ,以便mount或unmount也能在每個peer mount points發生。傳播也發生在相反的方向上,所以掛載和卸載事件在同級裝載也將傳播到此掛載點。
  • MS_PRIVATE:與shared mount point相反。掛載點不會將事件傳播給任何對等點,也不會從任何對等點接收傳播事件。
  • MS_SLAVE:這種傳播類型位於共享和私有之間。一個slave mount 從屬於一個master(這個master一般是一個shared peer group,master的所有成員會傳播mount和unmount事件到slave mount。但是,slave mount不會將事件傳播到master 對等組。
  • MS_UNBINDABLE:此mount point 是不可綁定的。與私有mount point 一樣,此掛載點不會將事件傳播到對等節點或從對等節點傳播事件。另外,此mount point不能成爲 bind mount 操作的源。

測試 shared peer group

  1. 在根目錄創建三個文件夾 X 、 Y、 Z
#cd /; mkdir X Y Z
  1. 將根目錄標記爲MS_PRIVATE
#mount --make-private /

關於默認傳播類型

這裏要說明的是,雖然從內核的角度來看,創建新設備的默認設置如下:

如果裝載點具有父級(即,它是非根裝入點),並且父級的傳播類型是MS_SHARED,則新裝入的傳播類型也是MS_SHARED。否則,新掛載的傳播類型是MS_PRIVATE。

根據這些規則,根掛載將是MS_PRIVATE,所有的後代掛載默認也是MS_PRIVATE。然而,MS_SHARED可以說是一個更好的默認值,因爲它是更常用的傳播類型。爲此,systemd將所有安裝點的傳播類型設置爲MS_SHARED。因此,在大多數現代Linux發行版中,默認的傳播類型實際上是MS_SHARED。

  1. 以shared傳播類型掛載 X、Y
# mount --make-shared /dev/sda2 /X
# mount --make-shared /dev/sda6 /Y
  1. 查看掛載情況
# cat /proc/self/mountinfo
25 0 8:6 / / rw,relatime - ext4 /dev/sda6 rw,errors=remount-ro,data=ordered
171 25 8:2 / /X rw,relatime shared:1 - fuseblk /dev/sda2 rw,user_id=0,group_id=0,allow_other,blksize=4096
174 25 8:6 / /Y rw,relatime shared:126 - ext4 /dev/sda6 rw,errors=remount-ro,data=ordered

5,在另一consol 用unshare新起一個bash並將起放入新的mount namespace, 並查看掛載情況

# unshare -m bash
# cat /proc/self/mountinfo
178 170 8:6 / / rw,relatime - ext4 /dev/sda6 rw,errors=remount-ro,data=ordered
212 178 8:2 / /X rw,relatime - fuseblk /dev/sda2 rw,user_id=0,group_id=0,allow_other,blksize=4096
223 178 8:6 / /Y rw,relatime - ext4 /dev/sda6 rw,errors=remount-ro,data=ordered

奇怪的是 X,Y在新的mount namespace中的傳播類型變成了 private, 前面不是說 ==”當創建一個新的Mount Namespace時,它會收到從clone()或unshare()調用方的名稱空間複製的掛載點列表的副本“== 嗎,怎麼傳播類型沒有複製過來?

這是因爲,當創建一個新的掛載名稱空間時,unshare假定用戶需要一個完全隔離的名稱空間,並通過執行以下命令(將根目錄下的所有掛載以遞歸方式標記爲private)的等效命令使所有掛載點保持私有狀態:

mount --make-rprivate /

爲了防止這種情況,我們可以在創建新的名稱空間時使用一個額外的選項:

unshare -m --propagation unchanged <cmd>
  1. 以propagation unchange方式重做第五步
# unshare -m --propagation unchanged bash
# cat /proc/self/mountinfo
178 170 8:6 / / rw,relatime - ext4 /dev/sda6 rw,errors=remount-ro,data=ordered
212 178 8:2 / /X rw,relatime shared:1 - fuseblk /dev/sda2 rw,user_id=0,group_id=0,allow_other,blksize=4096
223 178 8:6 / /Y rw,relatime shared:126 - ext4 /dev/sda6 rw,errors=remount-ro,data=ordered
  1. 回到第一個bash,從X mount point創建一個bind mount
# mount --bind /X /Z
# cat /proc/self/mountinfo
25 0 8:6 / / rw,relatime - ext4 /dev/sda6 rw,errors=remount-ro,data=ordered
171 25 8:2 / /X rw,relatime shared:1 - fuseblk /dev/sda2 rw,user_id=0,group_id=0,allow_other,blksize=4096
174 25 8:6 / /Y rw,relatime shared:126 - ext4 /dev/sda6 rw,errors=remount-ro,data=ordered
224 25 8:2 / /Z rw,relatime shared:1 - fuseblk /dev/sda2 rw,user_id=0,group_id=0,allow_other,blksize=4096

對比兩個bash的 mount info我們可以發現
- 上述試驗中生成了兩個 shared peer group。 bash1中的X,Z 以及bash2 中的X’(X的副本) 同屬於 shared peer group 1, 而 bash1 中的Y以及 bash2中的Y’ 同屬於 shared peer group 126.
- bash1 中的掛載點Z, 由於是在bash2 所屬的名空間被創建後掛載的所以未被複制,這是由於它的parent mount(/)標記爲私有。

image

測試對比 MS_SHARED 和 MS_PRIVATE

  1. 在根目錄下創建兩個文件夾 mntP 和 mntS
cd /;mkdir mntP mntS
  1. 以不同傳播方式掛載這兩個目錄到tmpfs文件系統
# mount -t tmpfs -o size=20m tmpfs --make-shared /mntS
# mount -t tmpfs -o size=20m tmpfs --make-private /mntP
# cat /proc/self/mountinfo | grep '/mnt' | sed 's/ - .*//'
170 25 0:47 / /mntS rw,relatime shared:1
171 25 0:52 / /mntP rw,relatime
  1. 在另一consol 用unshare新起一個bash
# unshare -m --propagation unchanged bash
# cat /proc/self/mountinfo | grep '/mnt' | sed 's/ - .*//'
212 178 0:47 / /mntS rw,relatime shared:1
223 178 0:52 / /mntP rw,relatime
  1. 在第二個bash中執行
# mkdir /mntS/a
# mount /dev/sda2 /mntS/a
# mkdir /mntP/b
# mount /dev/sda6 /mntP/b
# cat /proc/self/mountinfo | grep '/mnt' | sed 's/ - .*//'
212 178 0:47 / /mntS rw,relatime shared:1
223 178 0:52 / /mntP rw,relatime
224 212 8:2 / /mntS/a rw,relatime shared:126
258 223 8:6 / /mntP/b rw,relatime

從上面可以看出,/mntS/a被創建爲共享(從其父裝載繼承此設置),/mntP/b被創建爲私有裝載。

  1. 返回到第一個bash並檢查設置,我們看到在共享掛載點/ mntS下創建的新掛載傳播到其對等掛載,但在專用掛載點/ mntP沒有傳播:
# cat /proc/self/mountinfo | grep '/mnt' | sed 's/ - .*//'
170 25 0:47 / /mntS rw,relatime shared:1
171 25 0:52 / /mntP rw,relatime
257 170 8:2 / /mntS/a rw,relatime shared:126

測試MS_SLAVE

  1. 在根目錄下創建兩個文件夾 mntX 和 mntY
cd /;mkdir mntX mntY
  1. 掛載這兩個目錄到tmpfs文件系統
# mount -t tmpfs -o size=20m tmpfs --make-shared /mntX
# mount -t tmpfs -o size=20m tmpfs --make-shared /mntY
# cat /proc/self/mountinfo | grep '/mnt' | sed 's/ - .*//'
170 25 0:47 / /mntX rw,relatime shared:1
171 25 0:52 / /mntY rw,relatime shared:126
  1. 在另一consol 用unshare新起一個bash
# unshare -m --propagation unchanged bash
# cat /proc/self/mountinfo | grep '/mnt' | sed 's/ - .*//'
212 178 0:47 / /mntX rw,relatime shared:1
223 178 0:52 / /mntY rw,relatime shared:126
  1. 在bash2 設置/mntY爲slave
# mount --make-slave /mntY
# cat /proc/self/mountinfo | grep '/mnt' | sed 's/ - .*//'
212 178 0:47 / /mntX rw,relatime shared:1
223 178 0:52 / /mntY rw,relatime master:126

可以看見 /mntY 已經變成了 shared peer group 126的slave mount 了

  1. 在bash2 執行
# mkdir /mntX/a
# mount /dev/sda2 /mntX/a
# mkdir /mntY/b
# mount /dev/sda6 /mntY/b
# cat /proc/self/mountinfo | grep '/mnt' | sed 's/ - .*//'
212 178 0:47 / /mntX rw,relatime shared:1
223 178 0:52 / /mntY rw,relatime master:126
224 212 8:2 / /mntX/a rw,relatime shared:127
258 223 8:6 / /mntY/b rw,relatime

可以看見此時 /mntY/b 被創建爲 private狀態

  1. 在bash1中檢查,可發現bash2 中 /mntY/b的掛載未被傳播
# cat /proc/self/mountinfo | grep '/mnt' | sed 's/ - .*//'
170 25 0:47 / /mntX rw,relatime shared:1
171 25 0:52 / /mntY rw,relatime shared:126
257 170 8:2 / /mntX/a rw,relatime shared:127

注意,此時bash1 中的 /mntY仍屬於 shared peer group 126, 它其實是 bash2 中的/mntY的master

  1. 在bash1中執行以下命令
# mkdir /mntY/c
# mount /dev/sda6 /mntY/c
# cat /proc/self/mountinfo | grep '/mnt' | sed 's/ - .*//'
170 25 0:47 / /mntX rw,relatime shared:1
171 25 0:52 / /mntY rw,relatime shared:126
257 170 8:2 / /mntX/a rw,relatime shared:127
284 171 8:6 / /mntY/c rw,relatime shared:128
  1. 在bash2中檢查可發現 /mntY/c的掛載被傳播到了 bash2的 mount namesapce
# cat /proc/self/mountinfo | grep '/mnt' | sed 's/ - .*//'
212 178 0:47 / /mntX rw,relatime shared:1
223 178 0:52 / /mntY rw,relatime master:126
224 212 8:2 / /mntX/a rw,relatime shared:127
258 223 8:6 / /mntY/b rw,relatime
290 223 8:6 / /mntY/c rw,relatime master:128

測試 MS_UNBINDABLE

Unbindable mounts用於解決所謂的 “mount point explosion”的問題 .這一問題發生在文件樹的一個低級別的掛載點重複執行遞歸綁定更高級子樹時發生的問題.

  1. 創建兩個掛載點 /mntX 和 /mntX/mntY
# mkdir /mntX
# mount -t tmpfs -o size=20m tmpfs --make-shared /mntX
# mount | awk '{print $1, $2, $3}'
/dev/sda6 on /
tmpfs on /mntX
  1. 在另一終端用unshare 新創建一個bash, 並將/ 連同所有子掛載點遞歸設爲 slave(防止在其他mount namespace產生副作用). 然後嘗試將幾個用戶的home目錄遞歸的綁定到/mnt
# unshare -m bash
# mount --make-rslave /
# mkdir -p /home/user1 /home/user2
# mount --rbind / /home/user1
# mount | awk '{print $1, $2, $3}'
/dev/sda6 on /
tmpfs on /mntX
/dev/sda6 on /home/user1
tmpfs on /home/user1/mntX


# mount --rbind / /home/user2
root@chic-X450LD:/# mount | awk '{print $1, $2, $3}'
/dev/sda6 on /
tmpfs on /mntX
/dev/sda6 on /home/user1
tmpfs on /home/user1/mntX
/dev/sda6 on /home/user2
tmpfs on /home/user2/mntX
/dev/sda6 on /home/user2/home/user1
tmpfs on /home/user2/home/user1/mntX
  1. 通過 –make-unbindable 可以阻斷這種情況
# mount --rbind --make-unbindable / /home/user1
# mount --rbind --make-unbindable / /home/user2
  1. 重做步驟1,2
# mount | awk '{print $1, $2, $3}'
/dev/sda6 on /
tmpfs on /mntX
/dev/sda6 on /home/user1
tmpfs on /home/user1/mntX
/dev/sda6 on /home/user2
tmpfs on /home/user2/mntX

這裏寫圖片描述

chroot 與mount namespace

早期Linux可以用chroot改變一個進程的根目錄,也就是通過該命令可將一個子目錄設置爲該應用程序的“/”,如果要正常執行命令和程序等,必須還將系統中/bin, /usr/bin,/usr/lib等目錄拷貝到該“/”下。

這裏寫圖片描述

mount namespace進一步豐富了chroot的功能,它chroot更靈活。它屬於內核名字空間的一部分。 mount namespace可以對系統真正根目錄下的子目錄做到共享、獨享(也就是將擁有一個子目錄的副本)等。

製作一個簡單的小容器

接下來我們再結合chroot做一個小實驗。 假定有一個用戶user1

現在我們希望把user1的目錄侷限在它的主目錄 /home/user1

  1. 用unshare啓動新的bash併爲其指定新的pid 和 mount 名空間
# unshare --mount --fork --pid /bin/bash
  1. 在user1主目錄創建三個子目錄作爲掛載點
# mkdir -p /home/user1/bin /home/user1/lib /home/user1/lib64
# tree /home/user1/
/home/user1/
├── bin
├── lib
└── lib64
  1. 掛載三個目錄,並用chroot將/home/user1變爲根目錄
# mount --rbind /bin/ /home/user1/bin
# mount --rbind /lib/ /home/user1/lib
# mount --rbind /lib64/ /home/user1/lib64
# chroot /home/user1/
bash-4.3# pwd
/
  1. 掛載proc 目錄後再次運行ps 命令,現在我們可以看見進程少多了
# mkdir /proc
# mount -t proc proc /proc
# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
0            1     0  0 10:05 ?        00:00:00 /bin/bash
0           20     1  0 10:10 ?        00:00:00 /bin/bash -i
0           25    20  0 10:12 ?        00:00:00 ps -ef
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章