理論:https://blog.csdn.net/qq_37133717/article/details/86359947
當談論docker時,常常會聊到docker的實現方式。很多開發者都知道,docker容器本質上是宿主機的進程,Docker通過namespace實現了資源隔離,通過cgroups實現了資源限制,通過寫時復制機制(copy-on-write)實現了高效的文件操作。當進一步深入namespace和cgroups等技術細節時,大部分開發者都會感到茫然無措。尤其是接下來解釋libcontainer的工作原理時,我們會接觸大量容器核心知識。所以在這里,希望先帶領大家走進linux內核,了解namespa和cgroups的技術細節。
namespace資源隔離
linux內核提拱了6種namespace隔離的系統調用,如下圖所示,但是真正的容器還需要處理許多其他工作。
namespace | 系統調用參數 | 隔離內容 |
---|---|---|
UTS | CLONE_NEWUTS | 主機名或域名 |
IPC | CLONE_NEWIPC | 信號量、消息隊列和共享內存 |
PID | CLONE_NEWPID | 進程編號 |
Network | CLONE_NEWNET | 網絡設備、網絡戰、端口等 |
Mount | CLONE_NEWNS | 掛載點(文件系統) |
User | CLONE_NEWUSER | 用戶組和用戶組 |
實際上,linux內核實現namespace的主要目的,就是為了實現輕量級虛擬化技術服務。在同一個namespace下的進程合一感知彼此的變化,而對外界的進程一無所知。這樣就可以讓容器中的進程產生錯覺,仿佛自己置身一個獨立的系統環境中,以達到隔離的目的。
需要注意的是,本文所討論的namespace實現針對的是linux內核3.8及以后版本。
PID namespace
PID namespace隔離非常實用,它對進程PID重新標號,即兩個不同namespace下的進程可以有相同的PID。每個PID namespace都有自己的計數程序。內核為所有的PID namespace維護了一個樹狀結構,最頂層的是系統初始時創建的,被稱為root namespace,它創建的新PID namespace被稱為child namespace(樹的子節點),而原來的PID namespace就是新創建的PID namespace的parent namespace(樹的父節點)。通過這種方式,不同的PID namespace會形成一個層級體系。所屬的父節點可以看到子節點中的進程,並可以通過信號等方式對子節點中的進程產生影響。反過來,子節點卻不能看到父節點PID namespace中的任何內容,由此產生如下結論。
- 每個PID namespace中的第一個進程“PID 1”,都會像全通Linux中的init進程一樣擁有特權,其特殊作用。
- 一個namespace中的進程,不可能通過kill或ptrace影響父節點或者兄弟節點中的進程,因為其他幾點的PID在這個namespace沒有任何意義。
- 如果你在新的PID namespace中重新掛載/proc文件系統,會發現其下只顯示同屬一個PID namespace中的其他進程。
- 在root namespace中看到所有的進程,並且遞歸包含所有子節點中的進程。到這里,讀者可能已經聯想到了一種在Docker外部監控運行程序的方法了,就是監控Docker daemon所在的PID namespace下的所有進程及子進程,在進行篩選即可。
-
PID namespace中的init進程
在傳統的Unix系統中,PID為1的進程時init,地位非常特殊。它作為所有進程的父進程,維護一張進程表,不斷檢查進程狀態,一旦某個子進程因為父進程錯誤成為了“孤兒”進程,init就會負責收養這個子進程並最終回收資源,結束進程。所以在要實現的容器中,啟動的第一個進程也需要實現類似init的功能,維護所有后續啟動進程的狀態。
當系統中存在的樹狀嵌套結構的PID namespace時,若某個子進程成為了孤兒進程,收養孩子進程的責任就交給了孩子進程所屬的PID namespace中的init進程。
至此,讀者可以明白內核設計的良苦用心。PID namespace維護這樣一個樹狀結構,有利於系統的資源的控制與回收。因此,如果確實需要在一個Docker容器中運行多個進程,最先啟動的命令進程應該是具有資源監控與回收等管理能力的,如bash。 -
信號與init進程
內核還為PID namespace中的init進程賦予了其他特權—信號屏蔽。如果init中沒有編寫處理某個信號的代碼邏輯,那么與init在同一個PID namespace下的進程(即使有超級權限)發送非他的信號都會屏蔽。這個功能主要作用就是防止init進程被誤殺。
那么,父節點PID namespace中的進程發送同樣的信號給子節點中的init的進程,這會被忽略嗎?父節點中的進程發送的信號,如果不是SIGKILL(銷毀進程)或SIGSTOPO(暫停進程)也會誒忽略。但如果發送SIGKILL或SIGSTOP,子節點的init會強制執行(無法通過代碼捕捉進行特殊處理),也就是說父節點中的進程有權終止子進程。
一旦init進程被銷毀,同一PID namespace中的其他進程也所致接收到SIGKIKLL信號而被銷毀。理論上,該PID namespace也不復存在了。但如果/proc/[pid]/ns/pid處於被掛載或打開的狀態,namespace就會被保留下來。然而,被保留下來的namespace無法通過setns()或者fork()創建進程,所以實際上並沒有什么作用。
當一個容器內存在多個進程時,容器內的init進程可以對信號進行捕獲,當SIGTERM或SIGINT等信號到來時,對其子進程做信息保存、資源回收等處理工作。在Docker daemon的源碼中也可以看到類似的處理方式,當結束信號來臨時,結束容器進程並回收相應資源。 -
掛載proc文件系統
前文提到,如果在新的PID namespace中使用使用ps命令查看,看到的還是所有的進程,因為與PID直接相關的/proc文件系統(procfs)沒有掛載到一個與原/proc不同的位置。如果只想看到PID namespace本身應該看到的進程,需要重新掛載/proc,命令如下。
$ mount -t proc proc /proc
$ ps a
- 1
- 2
-
unshare()和setns()
本文開頭就談到了unshare()和setns()這兩個API,在PID namespace中使用,也有一些特別之處需要注意。
unshare()允許用戶在原有進程中建立命名空間進行隔離。但創建了PID namespace后,原先unshare()調用者進程並不進入新的PID namespace,接下來創建的子進程才會進入新的namespace,這個子進程也就隨之成為新的namespace中的init進程。
類似地,調用setns()創建新PID namespace時,調用者進程也不進入新的PID namespace,而是隨后創建的子進程進入。
為什么創建其他namespace時unshare()和setns()會直接進入新的namespace,二唯獨PID namespace例外呢?因為調用getpid()函數得到的PID是根據調用者所在的PID namespace而決定返回哪個PID,進入新的PID namespace會導致PID產生變化。而對用戶態的程序和庫函數來說,他們都認為進程的PID是一個常量,PID的變化會引起這些進程崩潰。
換句話說,一旦程序進程創建以后,那么它的PID namespace的關系就確定下來了,進程不會變更它們對應的PID namespace。在Docker中,docker exec會使用setns()函數加入已經存在的命名空間,但是最終還是會調用clone()函數,原因就在於此。 -
mount namespace
mount namespace通過隔離文件系統掛載點對隔離文件系統提供支持,它是歷史上第一個Linux namespace,所以標示位比較特殊,就是CLONE_NEWNS。隔離后,不同的mount namespace中的文件結構發生變化也互不影響。也可以通過/proc/[pid]/mounts查看到所有掛載在當前namespace中的文件系統,還可以通過/proc/[pid]/mountstats看到mount namespace中文件設備的統計信息,包括掛載文件的名字、文件系統的類型、掛載位置等。
進程在創建mount namespace時,會把當前的文件結構復制給新的namespace。新namespace中的所有mount操作都只影響自身的文件系統,對外界不會產生任何影響。這種做法非常嚴格的實現了隔離,但對某些狀況可能並不適用。比如父節點namespace中的進程掛載了一張CD-ROM,這時子節點namespace復制的目錄結構是無法自動掛載上這張CD-ROM的,因為這種操作會影響到父節點的文件系統。
一個掛載狀態可能為以下一種:
- 共享掛載
- 從屬掛載
- 共享/從屬掛載
- 私有掛載
- 不可綁定掛載
傳播事件的掛載對象稱為共享掛載;接收傳播事件的掛載對象稱為從屬掛載;同時兼有前述兩者特征的掛載對象為共享/從屬掛載;既不傳播也不接受事件的掛載對象稱為私有掛載;另一種特殊的掛載對象稱為不可綁定掛載,它們與私有掛載相似,但不允許執行綁定掛載,即創建mount namespace時這塊文件對象不可被復制。
1.docker run
[root@localhost ~]# docker run -it --name my-busybox2 docker.io/busybox /bin/sh
/ #
另啟一個窗口在宿主機上
[root@localhost proc]# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES cb465fa71b06 docker.io/busybox "/bin/sh" 37 seconds ago Up 36 seconds my-busybox2 9b8c4b4cce0d docker.io/busybox "/bin/sh" 28 hours ago Up 6 minutes my-busybox1 [root@localhost proc]# docker top my-busybox2 UID PID PPID C STIME TTY TIME CMD root 2117 2101 0 06:09 pts/4 00:00:00 /bin/sh [root@localhost proc]# ##進入2117容器進程,pid的namespace的id為4026532746
2.docker exec
[root@localhost ~]# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES cb465fa71b06 docker.io/busybox "/bin/sh" 10 minutes ago Up 10 minutes my-busybox2 9b8c4b4cce0d docker.io/busybox "/bin/sh" 28 hours ago Up 16 minutes my-busybox1 [root@localhost ~]# docker exec -it my-busybox2 /bin/sh / #
主機上查看一下,docker-run 的進程2117和docker exec進程2354 在同一個namespace (pid對應的namespace的id 是一樣)
所以在兩個進程中看到的內容是一樣的
在兩個窗口中執行top
在docker exec中運行ping命令
在宿主機上查看namespace,發現ping和docker run 及docker exec及top都在同一個namespace中
[root@localhost ns]# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES cb465fa71b06 docker.io/busybox "/bin/sh" 27 minutes ago Up 27 minutes my-busybox2 9b8c4b4cce0d docker.io/busybox "/bin/sh" 29 hours ago Up 33 minutes my-busybox1 [root@localhost ns]# docker top my-busybox2 UID PID PPID C STIME TTY TIME CMD root 2117 (docker exec) 2101 0 06:09 pts/4 00:00:00 /bin/sh root 2354(docker run) 2340 0 06:20 pts/5 00:00:00 /bin/sh root 2519(docker run 中運行的top) 2354 0 06:33 pts/5 00:00:00 top root 2536 (docker exec中運行的ping命令) 2117 0 06:35 pts/4 00:00:00 ping www.baidu.com [root@localhost ns]# cd ../../2536/ns [root@localhost ns]# ll total 0 lrwxrwxrwx 1 root root 0 Aug 16 06:37 cgroup -> cgroup:[4026531835] lrwxrwxrwx 1 root root 0 Aug 16 06:37 ipc -> ipc:[4026532745] lrwxrwxrwx 1 root root 0 Aug 16 06:37 mnt -> mnt:[4026532743] lrwxrwxrwx 1 root root 0 Aug 16 06:37 net -> net:[4026532748] lrwxrwxrwx 1 root root 0 Aug 16 06:37 pid -> pid:[4026532746] lrwxrwxrwx 1 root root 0 Aug 16 06:37 pid_for_children -> pid:[4026532746] lrwxrwxrwx 1 root root 0 Aug 16 06:37 user -> user:[4026531837] lrwxrwxrwx 1 root root 0 Aug 16 06:37 uts -> uts:[4026532744] [root@localhost ns]# pwd /proc/2536/ns [root@localhost ns]#
3.單進程模式控制
https://blog.csdn.net/M2l0ZgSsVc7r69eFdTj/article/details/102028724