Linux Namespace : 簡介

在初步的瞭解 docker 後,筆者指望經過理解 docker 背後的技術原理來深刻的學習和使用 docker,接下來的幾篇文章簡單的介紹下 linux namespace 的概念以及基本用法。html

namespace 的概念

namespace 是 Linux 內核用來隔離內核資源的方式。經過 namespace 可讓一些進程只能看到與本身相關的一部分資源,而另一些進程也只能看到與它們本身相關的資源,這兩撥進程根本就感受不到對方的存在。具體的實現方式是把一個或多個進程的相關資源指定在同一個 namespace 中。node

Linux namespaces 是對全局系統資源的一種封裝隔離,使得處於不一樣 namespace 的進程擁有獨立的全局系統資源,改變一個 namespace 中的系統資源只會影響當前 namespace 裏的進程,對其餘 namespace 中的進程沒有影響。linux

namespace 的用途

可能絕大多數的使用者和我同樣,是在使用 docker 後纔開始瞭解 linux 的 namespace 技術的。實際上,Linux 內核實現 namespace 的一個主要目的就是實現輕量級虛擬化(容器)服務。在同一個 namespace 下的進程能夠感知彼此的變化,而對外界的進程一無所知。這樣就可讓容器中的進程產生錯覺,認爲本身置身於一個獨立的系統中,從而達到隔離的目的。也就是說 linux 內核提供的 namespace 技術爲 docker 等容器技術的出現和發展提供了基礎條件。
咱們能夠從 docker 實現者的角度考慮該如何實現一個資源隔離的容器。好比是否是能夠經過 chroot 命令切換根目錄的掛載點,從而隔離文件系統。爲了在分佈式的環境下進行通訊和定位,容器必需要有獨立的 IP、端口和路由等,這就須要對網絡進行隔離。同時容器還須要一個獨立的主機名以便在網絡中標識本身。接下來還須要進程間的通訊、用戶權限等的隔離。最後,運行在容器中的應用須要有進程號(PID),天然也須要與宿主機中的 PID 進行隔離。也就是說這六種隔離能力是實現一個容器的基礎,讓咱們看看 linux 內核的 namespace 特性爲咱們提供了什麼樣的隔離能力:docker

上表中的前六種 namespace 正是實現容器必須的隔離技術,至於新近提供的 Cgroup namespace 目前尚未被 docker 採用。相信在不久的未來各類容器也會添加對 Cgroup namespace 的支持。shell

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。下面就讓讓咱們看看這些接口的簡要用法。segmentfault

查看進程所屬的 namespace

從版本號爲 3.8 的內核開始,/proc/[pid]/ns 目錄下會包含進程所屬的 namespace 信息,使用下面的命令能夠查看當前進程所屬的 namespace 信息:bash

$ 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 命令檢查下結果:app

很神奇吧,~/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 函數

相關文章
相關標籤/搜索