前言
Docker系列文章:
此篇是Docker系列的第九篇,之前的文章裡面或多或少的提到Docker的隔離技術,但是沒有很清楚的去聊這個技術,但是經過這麼多文章大家一定對Docker使用和概念有了一定的理解,接下來我們聊下底層一些技術,幫助大家解解惑,先從隔離技術開始吧。此外大家一定要按照我做的Demo都手敲一遍,印象會更加深刻的,加油!
如何理解Namespace
進入一個容器內部:
docker exec -it ad9342449b86 /bin/bash
通過ps命令檢視容器內部的程序,容器內部只有兩個程序在執行,看不到作業系統的其他程序,一個程序執行的/bin/bash,另外一個執行的ps,說明此刻容器被 Docker 隔離在了一個跟宿主機在不同的環境中。

對於宿主機來說相當於我們在宿主機上執行一個/bin/bash的程序,宿主機給這個程序起一個獨一無二名字,比如叫PID=800,用來區分與其他程序的不同,Docker在執行/bin/bash的時候,會與宿主機上的其他程序進行隔離,讓他看不到其他程序執行狀況,並且重新計算程序號,也就是我們看到202,但是這個程序實際上是宿主機的程序,這種技術,其實就是對被隔離應用的程序空間做了手腳,使得這些程序只能看到重新計算過的程序編號,這就是Linux裡面的Namespace機制,也就是Docker採用的隔離技術。
總結一下,Namespace是Linux核心用來隔離核心資源的方式。通過Namespace可以讓一些程序只能看到與自己相關的一部分資源,而另外一些程序也只能看到與它們自己相關的資源,這兩撥程序根本就感覺不到對方的存在。具體的實現方式是把一個或多個程序的相關資源指定在同一個Namespace中。Linux Namespace是對全域性系統資源的一種封裝隔離,使得處於不同Namespace的程序擁有獨立的全域性系統資源,改變一個Namespace中的系統資源只會影響當前Namespace裡的程序,對其他Namespace中的程序沒有影響。
Namespace型別介紹
目前,Linux 核心實現了6種 Namespace:

六種名稱空間:
UTS Namespace:
UTS Namespace 對主機名和域名進行隔離。為什麼要隔離主機名?因為主機名可以代替IP來訪問。如果不隔離,同名訪問會出衝突。
IPC Namespace
Linux 提供很多種程序通訊機制,IPC Namespace 針對 System V 和 POSIX 訊息佇列,這些 IPC 機制會使用識別符號來區別不同的訊息佇列,然後兩個程序通過識別符號找到對應的訊息佇列。IPC namespace使得相同的識別符號在兩個Namespace代表不同的訊息佇列,因此兩個Namespace 中的程序不能通過 IPC 來通訊。
PID Namespace
PID Namespace 用來隔離程序的 PID 空間,使得不同 PID Namespace 裡的程序 PID 可以重複且互不影響。PID Namespace 對容器類應用特別重要, 可以實現容器內程序的暫停/恢復等功能,還可以支援容器在跨主機的遷移前後保持內部程序的 PID 不發生變化。
Mount Namespace
Mount Namespace 為程序提供獨立的檔案系統檢視。可以這麼理解,Mount Namespace 用來隔離檔案系統的掛載點,這樣程序就只能看到自己的Mount Namespace中的檔案系統掛載點。程序的Mount Namespace中的掛載點資訊可以在 /proc/[pid]/mounts、/proc/[pid]/mountinfo 和 /proc/[pid]/mountstats 這三個檔案中找到。在一個 Namespace 裡掛載、解除安裝的動作不會影響到其他 Namespace。
Network Namespace
Network Namespace 在邏輯上是網路堆疊的一個副本,它有自己的路由、防火牆規則和網路裝置。預設情況下,子程序繼承其父程序的 Network Namespace。每個新建立的 Network Namespace 預設有一個本地環回介面 lo,除此之外,所有的其他網路裝置(物理/虛擬網路介面,網橋等)只能屬於一個 Network Namespace。每個 socket 也只能屬於一個 Network Namespace。
User Namespace
User Namespace 用於隔離安全相關的資源,包括 user IDs and group IDs,keys, 和 capabilities。同樣一個使用者的 user ID 和 group ID 在不同的User Namespace 中可以不一樣(與 PID Namespace 類似)。可以這樣理解,一個使用者可以在一個User Namespace中是普通使用者,但在另一個User Namespace中是root使用者。
Namespace原理介紹
Linux Namespace 是 Linux 提供的一種核心級別環境隔離的方法,因此Linux 提供了多個 API 用來操作 Namespace,它們是 clone()、setns() 和 unshare() 函式,簡單介紹一下三個系統呼叫的功能:
clone() : 實現執行緒的系統呼叫,用來建立一個新的程序,並可以通過設計上述系統呼叫引數達到隔離的目的; unshare() : 使某程序脫離某個 namespace; setns() : 把某程序加入到某個 namespace;
為了確定隔離的到底是哪項 Namespace,在使用這些 API 時,通常需要指定一些呼叫引數:CLONE_NEWIPC、CLONE_NEWNET、CLONE_NEWNS、CLONE_NEWPID、CLONE_NEWUSER、CLONE_NEWUTS 和 CLONE_NEWCGROUP。下圖是各個Namespace對應的呼叫引數和Linux核心:

如果要同時隔離多個 Namespace,可以使用 | (按位或)組合這些引數。同時我們還可以通過 /proc 下面的一些檔案來操作 Namespace。下面就讓讓我們看看這些介面的用法:
檢視程序所屬的Namespace
從版本號為 3.8 的核心開始,/proc/[pid]/ns 目錄下會包含程序所屬的 Namespace 資訊,使用下面的命令可以檢視當前程序所屬的 Namespace 資訊:
ls -l /proc/$$/ns

這些 Namespace 檔案都是連結檔案。連結檔案的內容的格式為 xxx:[inode number]。其中的 xxx 為 Namespace 的型別,inode number 則用來標識一個 Namespace,我們也可以把它理解為 Namespace 的 ID。如果兩個程序的某個 Namespace 檔案指向同一個連結檔案,說明其相關資源在同一個 Namespace 中。
clone() 函式
我們可以通過 clone() 在建立新程序的同時建立 Namespace。clone() 在 C 語言庫中的宣告如下:
#define _GNU_SOURCE
#include <sched.h>
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);
實際上,clone() 是在 C 語言庫中定義的一個封裝(wrapper)函式,它負責建立新程序的堆疊並且呼叫對程式設計者隱藏的 clone() 系統呼叫。四個引數代表的意思是:
child_func : 傳入子程序執行的程式主函式; child_stack : 傳入子程序使用的棧空間; flags : 表示使用哪些 CLONE_* 標誌位; args : 用於傳入使用者引數;
clone() 與 fork() 類似,都相當於把當前程序複製了一份,但 clone() 可以更細粒度地控制與子程序共享的資源(可以通過 flags 來控制),包括虛擬記憶體、開啟的檔案描述符和訊號量等等。一旦指定了標誌位 CLONE_NEW*,相對應型別的 Namespace 就會被建立,新建立的程序也會成為該 Namespace 中的一員。
setns() 函式
setns() 函式可以將當前程序加入到已有的 Namespace 中。setns() 在 C 語言庫中的宣告如下:
#define _GNU_SOURCE
#include <sched.h>
int setns(int fd, int nstype);
和 clone() 函式一樣,C 語言庫中的 setns() 函式也是對 setns() 系統呼叫的封裝:
fd:表示要加入 Namespace 的檔案描述符。它是一個指向 /proc/[pid]/ns 目錄中檔案的檔案描述符,可以通過直接開啟該目錄下的連結檔案或者開啟一個掛載了該目錄下連結檔案的檔案得到; nstype:引數 nstype 讓呼叫者可以檢查 fd 指向的 Namespace 型別是否符合實際要求。若把該引數設定為 0 表示不檢查;
unshare() 函式
unshare() 函式可以在原程序上進行 Namespace 隔離。也就是建立並加入新的 Namespace 。unshare() 在 C 語言庫中的宣告如下:
#define _GNU_SOURCE
#include <sched.h>
int unshare(int flags);
unshare() 函式也是對 unshare() 系統呼叫的封裝。呼叫 unshare() 的主要作用就是:不啟動新的程序就可以起到資源隔離的效果,相當於跳出原先的 Namespace 進行操作。
使用PID Namesapce達到Docker執行緒隔離狀態
Linux下的每個程序都有一個對應的 /proc/PID 目錄,該目錄包含了大量的有關當前程序的資訊。 對一個 PID Namespace 而言,/proc 目錄只包含當前 Namespace 和它所有子孫後代 Namespace 裡的程序的資訊。
建立一個新的 PID Namespace 後,如果想讓子程序中的 top、ps 等依賴 /proc 檔案系統的命令工作,還需要掛載 /proc 檔案系統。使用如下命令建立一個新的PID Namespace:
#檢視當前程序的PID
echo $$
#檢視該執行緒對應PID Namespace
readlink /proc/$$/ns/pid
#使用unshare命令建立新的PID Namespace
#該命令會同時建立新的PID和Mount namespace
unshare --pid --mount --fork /bin/bash
#檢視建立的執行緒的PID Namespace
readlink /proc/$$/ns/pid


上圖中新生成的PID Namespace的Id並沒有發生改變,我們看到在新建立的PID Namespace也可以看到其他的程序的執行情況,顯然這個和我們說的隔離是不一致的,接下來我們看下原因是什麼:

#檢視當前程序的PID
echo $$
#檢視當前程序的詳情
ps 1
這個例子說明當前程序被認為是該 PID namespace 中的 1 號程序了,通過PS命名檢視1號程序的詳情,發現這個程序是系統的啟動相關(CentOS 7開始也由systemd取代了init作為預設的系統程序管理工具)。
造成混亂的原因是當前程序沒有正確的掛載 /proc 檔案系統,由於我們新的 Mount Namespace 的掛載資訊是從老的 Namespace 拷貝過來的,所以這裡看到的還是老 Namespace 裡面的程序號為 1 的資訊。執行下面的命令掛載 /proc 檔案系統:
mount -t proc proc /proc
接下來我們再來檢查相關的資訊,會發現新建的 PID namespace 程序看不到宿主機的程序資訊了。

我們也可以使用如下命令來建立程序,
unshare --pid --mount-proc --fork /bin/bash
這樣在建立了 PID 和 Mount Namespace 後,會自動掛載 /proc 檔案系統,就不需要我們手動執行 mount -t proc proc /proc 命令了。
總結
Docker容器是在建立容器程序時,指定了這個程序所需要啟用的一組Namespace引數,這樣容器就只能看到到當前Namespace所限定的資源、檔案、裝置、狀態,或者配置。而對於宿主機以及其他不相關的程式,它就完全看不到了,因此容器本質上就是一個特殊的程序。
結束
歡迎大家點點關注,點點贊!