鑑於docker底層和lxc底層相同,這裏整理下研究lxc時對於namespace的研究。
在其他虛擬化的系統中,一臺物理計算機可以運行多個內核,可能是並行的多個不同的操作系統。而容器只使用一個內核在一臺物理計算機上運行,通過命名空間隔離資源來虛擬os運行環境,但核心的底層服務則由一個kernel完成。與應用相關的全局資源都通過命名空間抽象起來,使得可以將一組進程放置到容器中,各個容器彼此隔離。隔離可以使容器的成員與其他容器毫無關係。但也可以通過允許容器進行一定的共享,來降低容器之間的分隔。例如,容器可以設置爲使用自身的PID集合,但仍然與其他容器共享部分文件系統。本質上,命名空間建立了系統的不同視圖。此前的每一項全局資源都必須包裝到容器數據結構中,只有資源和包含資源的命名空間構成的二元組仍然是全局唯一的。雖然在給定容器內部資源是自足的,但無法提供在容器外部具有唯一性的ID。
新的命名空間可以用下面兩種方法創建。
Ø 在用fork或clone系統調用創建新進程時,有特定的選項可以控制是與父進程共享命名空間,還是建立新的命名空間。該選項就是一些標誌位,每個命名空間都有一個對應的標誌:
#define CLONE_NEWUTS 0x04000000 /* 創建新的utsname組 */
#define CLONE_NEWIPC 0x08000000 /* 創建新的IPC命名空間 */
#define CLONE_NEWUSER 0x10000000 /* 創建新的用戶命名空間 */
#define CLONE_NEWPID 0x20000000 /* 創建新的PID命名空間 */
#define CLONE_NEWNET 0x40000000 /* 創建新的網絡命名空間 */
Ø unshare系統調用將進程的某些部分從父進程分離,其中也包括命名空間。
命名空間的實現需要兩個部分:每個子系統的命名空間結構,將此前所有的全局組件包裝到命名空間中;將給定進程關聯到所屬各個命名空間的機制。structnsproxy用於彙集指向特定於子系統的命名空間包裝器的指針:
Ø UTS命名空間包含了運行內核的名稱、版本、底層體系結構類型等信息。
Ø IPC命名空間包含了與進程間通信有關的信息。
Ø MNT命名空間包含了文件系統的視圖。
Ø PID命名空間包含了有關進程ID的信息。
Ø USER命名空間包含了用於限制每個用戶資源使用的信息,最近的更新包含了容器權限控制的實現部分。
Ø NET命名空間包含所有網絡相關的參數。目前只是部分還不夠完善。
下面分別說明下各個命名空間。
1.1.1. PID命名空間
Linux用ID來管理進程,常用的有PID,但每個進程除了PID之外,還有其他的ID。共有四種ID定義在pid_type中。PIDTYPE_MAX表示ID類型的數目:
enum pid_type
{
PIDTYPE_PID,
PIDTYPE_PGID,
PIDTYPE_SID,
PIDTYPE_MAX
};
處於某個線程組中的所有進程都有統一的線程組ID(TGID)。如果進程沒有使用線程,則其PID和TGID相同。
線程組中的主進程被稱作組長(group leader)。通過clone創建的所有線程的task_struct的group_leader成員,會指向組長的task_struct實例。
獨立進程可以合併成進程組,進程組成員的task_struct的pgrp屬性值都是相同的,即進程組組長的 PID。進程組簡化了向組的所有成員發送信號的操作。
幾個進程組可以合併成一個會話。會話中的所有進程都有同樣的會話ID,保存在task_struct的session成員中。SID可以使用 setsid系統調用設置。終端中運行的程序一般具有相同的SID。
PID命名空間可以是多層嵌套,一個命名空間是父命名空間,衍生了兩個子命名空間。如果每個容器都配置了新的命名空間,如果容器爲系統級容器,每個容器都有自身的init進程,PID爲0,子命名空間都有PID爲0的init進程,以及PID分別爲2和3的進程。子命名空間不瞭解父命名空間,但父命名空間知道子命名空間的存在,也可以看到其中執行的所有進程,因此子命名空間的進程ID在父命名空間中是唯一的,這也就說明了自命名空間擁有多個pid,這些pid分別對應其以上的命名空間。那麼頂層ID爲全局ID,子命名空間ID爲局部ID。
全局ID是在內核本身和初始命名空間中的唯一ID號,在系統啓動期間開始的init進程即屬於初始命名空間。對每個ID類型,都有一個給定的全局 ID,保證在整個系統中是唯一的。局部ID屬於某個特定的命名空間,不具備全局有效性。對每個ID類型,它們在所屬的命名空間內部有效,但類型相同、值也相同的ID可能出現在不同的命名空間中。
全局PID和TGID直接保存在task_struct中,分別是task_struct的pid和tgid成員:
struct task_struct {
...
pid_t pid;
pid_t tgid;
...
}
PID命名空間用pid_namespace表示:
structpid_namespace {
struct kref kref;
struct pidmap pidmap[PIDMAP_ENTRIES];
int last_pid;
struct task_struct *child_reaper;
struct kmem_cache *pid_cachep;
unsigned int level;
struct pid_namespace *parent;
#ifdefCONFIG_PROC_FS
struct vfsmount *proc_mnt;
#endif
#ifdefCONFIG_BSD_PROCESS_ACCT
struct bsd_acct_struct *bacct;
#endif
gid_t pid_gid;
int hide_pid;
int reboot; /*group exit code if this pidns was rebooted */
};
child_reaper指向的進程作用相當於全局命名空間的init進程,其中一個目的是對孤兒進程進行回收。Level則表明自己所處的命名空間在系統命名空間裏面的深度,這是一個重要的標記,因爲層次高的命名空間可以看到低級別的所有信息。系統的命名空間從0開始技術,然後累加。parent是指向父命名空間的指針。
PID的管理圍繞兩個數據結構展開:structpid是內核對PID的內部表示,而structupid則表示特定的命名空間中可見的信息。兩個結構的定義如下:
struct pid
{
atomic_tcount;//引用計數
structhlist_head tasks[PIDTYPE_MAX];//該pid被使用的task鏈表
structrcu_head rcu;//互斥訪問
intlevel;//該pid所能到達的最大深度
structupid numbers[1];//每一層次(level)的upid
}
一個pid可以屬於不同的級別,每一級別又包含一個upid
struct upid
{
intnr;//真正的pid值
structpid_namespace *ns;//該nr屬於哪個pid_namespace
structhlist_node pid_chain;//所有upid的hash鏈表 find_pid
}
struct pid的定義首先是一個引用計數器count。tasks是一個數組,每個數組項都是一個散列表頭,對應於一個ID類型。這樣做是必要的,因爲一個ID可能用於幾個進程。所有共享同一給定ID的task_struct實例,都通過該列表連接起來。一個進程可能在多個命名空間中可見,而其在各個命名空間中的局部ID各不相同。level表示可以看到該進程的命名空間的數目(換言之,即包含該進程的命名空間在命名空間層次結構中的深度),而numbers是一個upid實例的數組,每個數組項都對應於一個命名空間。注意該數組形式上只有一個數組項,如果一個進程只包含在全局命名空間中,那麼確實如此。由於該數組位於結構的末尾,因此只要分配更多的內存空間,即可向數組添加附加的項。對於structupid,nr表示ID的數值,ns是指向該ID所屬的命名空間的指針。所有的upid實例都保存在一個散列表中。
一個task_struct通過pid_link的hlist_node掛接到struct pid的鏈表上面去。同時task_struct又是用過pid_link找到pid,通過pid遍歷tasks鏈表又能夠得到所有的任務,當然也可以讀取numbers數字獲取每一個命名空間裏面的數字信息。
由於所有共享同一ID的task_struct實例都按進程存儲在一個散列表中,因此需要在structtask_struct中增加一個散列表元素:
struct task_struct {
...
/* PID與PID散列表的聯繫。 */
struct pid_link pids[PIDTYPE_MAX];
...
};
輔助數據結構pid_link可以將task_struct連接到表頭在struct pid中的散列表上:
struct pid_link
{
struct hlist_node node;
struct pid *pid;
};
pid指向進程所屬的pid結構實例,node用作散列表元素。
爲在給定的命名空間中查找對應於指定PID數值的pid結構實例,使用了一個散列表:
static struct hlist_head *pid_hash;
1.1.2. MNT命名空間
進程創建的時候,每一個進程都有自己的文件掛載點,定義在structtask_struct中:
structtask_struct {
……
/* filesysteminformation */
struct fs_struct *fs;
/* open fileinformation */
struct files_struct *files;
/* namespaces*/
struct nsproxy *nsproxy;
……
}
structfs_struct {
int users;
spinlock_t lock;
seqcount_t seq;
int umask;
int in_exec;
struct path root, pwd;
};
在一個系統啓動的時候,0號進程就設置好了自己所在的根目錄以及當前目錄。在創建子進程的時候,通過CLONE_FS來指明父子之間的共享信息,如果設置了兩者共享同一個結構,沒有設置標記的話,子進程創建一個新的拷貝,兩者之間互不影響。如果設置了CLONE_FS,通過chroot(2), chdir(2), or umask(2)的調用結果兩者之間會相互影響,反之兩者是獨立的。
老的chroot機制只會更改fs_struct中的root指針指向新的目錄的dentry結構,pwd不會更改,新的MNT命名空間技術不但更改了pwd和root,而使用的是mount,這將會重新創建一個vfs_mount實例。
舉例說明MNT命名空間的效果:
在系統目錄裏面創建一個container目錄,在這個目錄裏面爲每一個容器創建了獨立的目錄,爲1和2。在目錄1和2裏面分別創建相應容器的根目錄文件系統。在容器裏看到的文件系統視圖如下所示。
容器1和容器2之間的文件系統是互不可見的,而且容器也看不到除了根目錄之外的其他文件目錄。爲了和系統或者其他容器共享文件,需要映射特定目錄到容器的根文件系統,達到部分隔離以及共享的效果。
1.1.3. NET命名空間
NET的命名空間隔離做的工作相對而言是最多的,在目前的內核裏面路由表,arp表,netfilter表,設備等等都已經命名空間化了,雖然共享一個網絡協議棧,但是協議棧在處理數據的時候都會從各自的命名空間中取出相關的配置,這樣數據就被隔離開來,看上去像多個協議棧。
NET命名空間用structnet表示:
struct net {
atomic_t passive; /* To decided when the network
* namespace should be freed.
*/
atomic_t count; /* To decided when the network
* namespace should be shut down.
*/
#ifdefNETNS_REFCNT_DEBUG
atomic_t use_count; /* To track references we
* destroy on demand
*/
#endif
spinlock_t rules_mod_lock;
struct list_head list; /* list of networknamespaces */
struct list_head cleanup_list; /* namespaces ondeath row */
struct list_head exit_list; /* Use onlynet_mutex */
struct proc_dir_entry *proc_net;
struct proc_dir_entry *proc_net_stat;
#ifdefCONFIG_SYSCTL
struct ctl_table_set sysctls;
#endif
struct sock *rtnl; /* rtnetlink socket */
struct sock *genl_sock;
struct list_head dev_base_head;
struct hlist_head *dev_name_head;
struct hlist_head *dev_index_head;
unsigned int dev_base_seq; /* protected by rtnl_mutex */
/* core fib_rules */
struct list_head rules_ops;
struct net_device *loopback_dev; /* The loopback */
struct netns_core core;
struct netns_mib mib;
struct netns_packet packet;
struct netns_unix unx;
struct netns_ipv4 ipv4;
#ifIS_ENABLED(CONFIG_IPV6)
struct netns_ipv6 ipv6;
#endif
#ifdefined(CONFIG_IP_DCCP) || defined(CONFIG_IP_DCCP_MODULE)
struct netns_dccp dccp;
#endif
#ifdefCONFIG_NETFILTER
struct netns_xt xt;
#ifdefined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)
struct netns_ct ct;
#endif
struct sock *nfnl;
struct sock *nfnl_stash;
#endif
#ifdefCONFIG_WEXT_CORE
struct sk_buff_head wext_nlevents;
#endif
struct net_generic __rcu *gen;
/* Note : following structs are cache linealigned */
#ifdefCONFIG_XFRM
struct netns_xfrm xfrm;
#endif
struct netns_ipvs *ipvs;
};
Struct net這個結構實在較大,裏面基本包含的是鏈路層以上的內容。在一個NET命名空間創建的時候,會做一些初始化。系統定義了一個回調函數,讓感興趣的模塊註冊。結構如下:
一個新的NET命名空間被創建的時候,註冊模塊的init被調用。同理,一個NET命名空間銷燬的時候,exit也會被調用。
MNT命名空間通過mount一些公共的目錄進行共享,NET命名空間與外界聯繫則有多種方式,通常會使用設備對,搭建網橋的方式與容器外通信。一個設備對即A設備接收到的時候自動發送到B設備,反之亦然。
對於容器來說,veth0需要配置一個IP地址,但是veth1和物理網卡配置在同一個橋接設備上。Veth0和veth1是網絡設備對。Veth1和網卡是通過橋接來完成轉發,但是veth0和veth1之間是通過設備對來完成數據轉發,這樣數據就在netnamespace 1和init netnamespace之間傳輸。
NET命名空間隔離的內容比較多,例如ip分片、路由表和netfilter等。
1.1.4. UTS命名空間
UTS命名空間很簡單,所有相關信息都彙集到下列結構的一個實例中:
struct uts_namespace {
struct kref kref;
struct new_utsname name;
};
kref是一個嵌入的引用計數器,可用於跟蹤內核中有多少地方使用了structuts_namespace的實例。uts_namespace所提供的屬性信息本身包含在structnew_utsname中:
structnew_utsname
{
charsysname[65];//系統名稱
charnodename[65];//主機名
charrelease[65];//內核版本號
charversion[65];//內核版本日期
charmachine[65];//體系結構
chardomainname[65];
}
各個字符串分別存儲了系統的名稱(Linux...)、內核發佈版本、機器名,等等。使用uname工具可以取得這些屬性的當前值,也可以在/proc/sys/kernel/中看到:
初始設置保存在init_uts_ns中:
struct uts_namespace init_uts_ns = {
.name = {
.sysname = UTS_SYSNAME,
.nodename = UTS_NODENAME,
.release = UTS_RELEASE,
.version = UTS_VERSION,
.machine = UTS_MACHINE,
.domainname = UTS_DOMAINNAME,
},
};
相關的預處理器常數在內核中各處定義。UTS結構的某些部分不能修改。例如,把sysname換成Linux以外的其他值是沒有意義的,但改變機器名是可以的。
1.1.5. IPC命名空間
IPC是一個較爲簡單的扁平化進程間通信工具,命名空間之間不存在層級。
IPC 命名空間之間的關係是並列的:
共享內存,消息隊列和信號量的實現比較簡單,這裏不放代碼分析了。