在初步的了解 docker 后,筆者期望通過理解 docker 背后的技術原理來深入的學習和使用 docker,接下來的幾篇文章簡單的介紹下 linux namespace 的概念以及基本用法。
namespace 的概念
namespace 是 Linux 內核用來隔離內核資源的方式。通過 namespace 可以讓一些進程只能看到與自己相關的一部分資源,而另外一些進程也只能看到與它們自己相關的資源,這兩撥進程根本就感覺不到對方的存在。具體的實現方式是把一個或多個進程的相關資源指定在同一個 namespace 中。
Linux namespaces 是對全局系統資源的一種封裝隔離,使得處於不同 namespace 的進程擁有獨立的全局系統資源,改變一個 namespace 中的系統資源只會影響當前 namespace 里的進程,對其他 namespace 中的進程沒有影響。
namespace 的用途
可能絕大多數的使用者和我一樣,是在使用 docker 后才開始了解 linux 的 namespace 技術的。實際上,Linux 內核實現 namespace 的一個主要目的就是實現輕量級虛擬化(容器)服務。在同一個 namespace 下的進程可以感知彼此的變化,而對外界的進程一無所知。這樣就可以讓容器中的進程產生錯覺,認為自己置身於一個獨立的系統中,從而達到隔離的目的。也就是說 linux 內核提供的 namespace 技術為 docker 等容器技術的出現和發展提供了基礎條件。
我們可以從 docker 實現者的角度考慮該如何實現一個資源隔離的容器。比如是不是可以通過 chroot 命令切換根目錄的掛載點,從而隔離文件系統。為了在分布式的環境下進行通信和定位,容器必須要有獨立的 IP、端口和路由等,這就需要對網絡進行隔離。同時容器還需要一個獨立的主機名以便在網絡中標識自己。接下來還需要進程間的通信、用戶權限等的隔離。最后,運行在容器中的應用需要有進程號(PID),自然也需要與宿主機中的 PID 進行隔離。也就是說這六種隔離能力是實現一個容器的基礎,讓我們看看 linux 內核的 namespace 特性為我們提供了什么樣的隔離能力:
上表中的前六種 namespace 正是實現容器必須的隔離技術,至於新近提供的 Cgroup namespace 目前還沒有被 docker 采用。相信在不久的將來各種容器也會添加對 Cgroup namespace 的支持。
namespace 的發展歷史
Linux 在很早的版本中就實現了部分的 namespace,比如內核 2.4 就實現了 mount namespace。大多數的 namespace 支持是在內核 2.6 中完成的,比如 IPC、Network、PID、和 UTS。還有個別的 namespace 比較特殊,比如 User,從內核 2.6 就開始實現了,但在內核 3.8 中才宣布完成。同時,隨着 Linux 自身的發展以及容器技術持續發展帶來的需求,也會有新的 namespace 被支持,比如在內核 4.6 中就添加了 Cgroup namespace。
Linux 提供了多個 API 用來操作 namespace,它們是 clone()、setns() 和 unshare() 函數,為了確定隔離的到底是哪項 namespace,在使用這些 API 時,通常需要指定一些調用參數:CLONE_NEWIPC、CLONE_NEWNET、CLONE_NEWNS、CLONE_NEWPID、CLONE_NEWUSER、CLONE_NEWUTS 和 CLONE_NEWCGROUP。如果要同時隔離多個 namespace,可以使用 | (按位或)組合這些參數。同時我們還可以通過 /proc 下面的一些文件來操作 namespace。下面就讓讓我們看看這些接口的簡要用法。
查看進程所屬的 namespace
從版本號為 3.8 的內核開始,/proc/[pid]/ns 目錄下會包含進程所屬的 namespace 信息,使用下面的命令可以查看當前進程所屬的 namespace 信息:
$ ll /proc/$$/ns
首先,這些 namespace 文件都是鏈接文件。鏈接文件的內容的格式為 xxx:[inode number]。其中的 xxx 為 namespace 的類型,inode number 則用來標識一個 namespace,我們也可以把它理解為 namespace 的 ID。如果兩個進程的某個 namespace 文件指向同一個鏈接文件,說明其相關資源在同一個 namespace 中。
其次,在 /proc/[pid]/ns 里放置這些鏈接文件的另外一個作用是,一旦這些鏈接文件被打開,只要打開的文件描述符(fd)存在,那么就算該 namespace 下的所有進程都已結束,這個 namespace 也會一直存在,后續的進程還可以再加入進來。
除了打開文件的方式,我們還可以通過文件掛載的方式阻止 namespace 被刪除。比如我們可以把當前進程中的 uts 掛載到 ~/uts 文件:
$ touch ~/uts $ sudo mount --bind /proc/$$/ns/uts ~/uts
使用 stat 命令檢查下結果:
很神奇吧,~/uts 的 inode 和鏈接文件中的 inode number 是一樣的,它們是同一個文件。
clone() 函數
我們可以通過 clone() 在創建新進程的同時創建 namespace。clone() 在 C 語言庫中的聲明如下:
/* Prototype for the glibc wrapper function */ #define _GNU_SOURCE #include <sched.h> int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);
實際上,clone() 是在 C 語言庫中定義的一個封裝(wrapper)函數,它負責建立新進程的堆棧並且調用對編程者隱藏的 clone() 系統調用。Clone() 其實是 linux 系統調用 fork() 的一種更通用的實現方式,它可以通過 flags 來控制使用多少功能。一共有 20 多種 CLONE_ 開頭的 falg(標志位) 參數用來控制 clone 進程的方方面面(比如是否與父進程共享虛擬內存等),下面我們只介紹與 namespace 相關的 4 個參數:
- fn:指定一個由新進程執行的函數。當這個函數返回時,子進程終止。該函數返回一個整數,表示子進程的退出代碼。
- child_stack:傳入子進程使用的棧空間,也就是把用戶態堆棧指針賦給子進程的 esp 寄存器。調用進程(指調用 clone() 的進程)應該總是為子進程分配新的堆棧。
- flags:表示使用哪些 CLONE_ 開頭的標志位,與 namespace 相關的有CLONE_NEWIPC、CLONE_NEWNET、CLONE_NEWNS、CLONE_NEWPID、CLONE_NEWUSER、CLONE_NEWUTS 和 CLONE_NEWCGROUP。
- arg:指向傳遞給 fn() 函數的參數。
在后續的文章中,我們主要通過 clone() 函數來創建並演示各種類型的 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 表示不檢查。
前面我們提到:可以通過掛載的方式把 namespace 保留下來。保留 namespace 的目的是為以后把進程加入這個 namespace 做准備。在 docker 中,使用 docker exec 命令在已經運行着的容器中執行新的命令就需要用到 setns() 函數。為了把新加入的 namespace 利用起來,還需要引入 execve() 系列的函數(筆者在 《Linux 創建子進程執行任務》一文中介紹過 execve() 系列的函數,有興趣的同學可以前往了解),該函數可以執行用戶的命令,比較常見的用法是調用 /bin/bash 並接受參數運行起一個 shell。
unshare() 函數 和 unshare 命令
通過 unshare 函數可以在原進程上進行 namespace 隔離。也就是創建並加入新的 namespace 。unshare() 在 C 語言庫中的聲明如下:
#define _GNU_SOURCE #include <sched.h> int unshare(int flags);
和前面兩個函數一樣,C 語言庫中的 unshare() 函數也是對 unshare() 系統調用的封裝。調用 unshare() 的主要作用就是:不啟動新的進程就可以起到資源隔離的效果,相當於跳出原先的 namespace 進行操作。
系統還默認提供了一個叫 unshare 的命令,其實就是在調用 unshare() 系統調用。下面的 demo 使用 unshare 命令把當前進程的 user namespace 設置成了 root:
總結
namespace 是 linux 內核提供的特性,為虛擬化而生。隨着 docker 的誕生引爆了容器技術,也把長期在后台默默奉獻的 namespace 技術推到了大家的面前。筆者試圖通過對 namespace 技術的學習和理解來加深對容器技術的認識,所以接下來會通過文章記錄學習 namespace 的點點滴滴,希望能和同學們一起進步。
參考:
Namespace 概述
overview of Linux namespaces
Clone 函數
Setns 函數
Unshare 函數