Linux Namespace


 

轉載請注明出處,並保留以上所有對文章內容、圖片、表格的來源的描述。

 

一、Linux Namespace

        Linux Namespace是Linux提供的一種OS-level virtualization的方法。目前在Linux系統上實現OS-level virtualization的系統有Linux VServer、OpenVZ、LXC Linux Container、Virtuozzo等,其中Virtuozzo是OpenVZ的商業版本。以上種種本質來說都是使用了Linux Namespace來進行隔離。

        那么究竟什么是Linux Namespace?Linux很早就實現了一個系統調用chroot,該系統調用能夠為進程提供一個限制的文件系統。雖然文件系統的隔離要比單純的chroot復雜的多,但是至少chroot提供了一種簡單的隔離模式:chroot內部的文件系統無法訪問外部的內容。Linux Namespace在此基礎上,提供了對UTS、IPC、mount、PID、network的隔離機制,例如對不同的PID namespace中的進程無法看到彼此,而且每個PID namespace中的進程PID都是單獨制定的。這一點對OS-level Virtualization非常有用,這是因為:對於不同的Linux運行環境中,都有一個init進程,其PID=0,由於不同的PID namespace中都可以指定自己的0號進程,所以可以通過該技術來進行PID環境的隔離。

        OS-level Virtualization相比其他的虛擬化技術更加輕量級。

        Linux在使用Namespace的時候,需要顯式的在配置中指定將那些Namespace的支持編譯到內核中。

二、數據結構

        我們可以在struct task_struct中找到對應的namespace結構。

<sched.h>

1 struct task_struct { 
2 ... 
3     struct nsproxy *nsproxy; 
4 ... 
5 };

        nsproxy就是每個進程自己的namespace,結構如下:

<nsproxy.h>

1 struct nsproxy { 
2     atomic_t count; 
3     struct uts_namespace *uts_ns; 
4     struct ipc_namespace *ipc_ns; 
5     struct mnt_namespace *mnt_ns; 
6     struct pid_namespace *pid_ns; 
7     struct net          *net_ns; 
8 }; 
9 extern struct nsproxy init_nsproxy;

 

        具體的參考后文,可以看到有一個init_nsproxy,這個全局初始化使用的nsproxy,也是init進程使用的nsproxy。

三、UTS namespace

        我們在這里只介紹兩種namespace:UTS namespace和PID namespace,是因為這兩種namespace比較有代表性。UTS namespace很簡單,也沒有樹形或者很復雜的結構。struct uts_namespace結構如下:

<utsname.h>

1 struct uts_namespace { 
2     struct kref kref; 
3     struct new_utsname name; 
4     struct user_namespace *user_ns; 
5 }; 
6 extern struct uts_namespace init_uts_ns;

 

        可以看到uts_namespace中只關心utsname域(name)和user_namespace域(user_ns)。老版本的Linux user_namespace是在nsproxy中的,新版本放在了uts_namespace中。這部分的結構如下:

85677792[4]

        需要說明的是,這張圖是比較老的Linux Kernel,其中user_namespace還在nsproxy結構中。對於uts_namespace,由於不需要維護像pid_namespace那樣復雜的樹狀結構和復雜的搜索需求,所以對於每個uts_namespace只需要維護一個實例就可以了。

四、進程的若干個ID的意義
  • PID:Process ID,進程ID,即進程的唯一標識
  • TGID:處於某個線程組中的所有進程都有統一的線程組ID(Thread Group IP,TGID)。線程可以用clone加CLONE_THREAD來創建。線程組中的主進程成為group leader,可以通過線程組中任何線程的的task_struct->group_leader成員獲得。
  • 獨立進程可以合並成進程組(使用setpgrp系統調用)。進程組成員的task_struct->pgrp屬性值都是相同的(PGID),即進程組組長的PID。用管道連接的進程在一個進程組中。
  • 幾個進程組可以合並成一個會話。會話中所有進程都有同樣的SID(Session ID,會話ID),保存在task_struct->session中。SID可以通過setsid系統調用設置。
五、PID namespace

        task_struct中有幾個成員是與PID有關的,如下:

<sched.h>

1 struct task_struct{ 
2 ... 
3     pid_t pid; 
4     pid_t tgid; 
5 ... 
6     struct pid_link pids[PIDTYPE_MAX]; 
7 ... 
8 }

 

        對於所有的進程來說,都有兩種ID(包含PID、TGID、PGRP、SID):一個是全局的ID,保存在task_struct->pid中;另一個是局部的ID,即屬於某個特定的命名空間的ID。對於task_struct->pids數組,數組編號的定義如下:

<pid.h>

1 enum pid_type 
2 { 
3     PIDTYPE_PID, 
4     PIDTYPE_PGID, 
5     PIDTYPE_SID, 
6     PIDTYPE_MAX 
7 };

 

        可以看到這里只定義了PID、PGID和SID,至於TGID(線程組ID)無非是線程組組長的ID,所以就不需要用特殊的結構來組織了。

        下面給出struct pid_link的結構:

<pid.h>

1 struct pid_link 
2 { 
3     struct hlist_node node; 
4     struct pid *pid; 
5 };

 

        pid_link包含兩個成員,node用來組織struct task_struct和struct pid的關系,后面會降到,pid則指向對應的struct pid結構。可以看到,給出指定的task_struct,可以通過task_struct->pids[pid_type]->pid來找到對應的pid結構。下面給出struct pid結構定義:

<pid.h>

1 struct pid 
2 { 
3     atomic_t count; 
4     unsigned int level; 
5     /* lists of tasks that use this pid */ 
6     struct hlist_head tasks[PIDTYPE_MAX]; 
7     struct rcu_head rcu; 
8     struct upid numbers[1]; 
9 };

 

        其中count表示該pid結構被引用的次數,level表示該pid結構對應多少層的namespace,tasks則反向指向與該pid相連的task_struct,即上文所述pid_link->node就是組織在pid->tasks中的,rcu是將所有struct pid組織起來的輔助結構,numbers成員中存儲的是struct upid結構,該結構是pid與pid_namespace相關聯的結構。這里需要注意,雖然numbers成員數組大小只有1,其實這種定義只是說明了:struct pid中的numbers事實上是一個數組,其長度至少為1(即只有一層namespace的情況);對於pid屬於多層namespace的情況,該數組是可以動態擴張的。struct pid定義如下:

<pid.h>

1 struct upid { 
2     /* Try to keep pid_chain in the same cacheline as nr for find_vpid */ 
3     int nr; 
4     struct pid_namespace *ns; 
5     struct hlist_node pid_chain; 
6 };

 

        nr表示ID的數值,我們使用ls命令得到的進程的各種ID就是保存在這里,ns存儲的是指向namespace的結構。此外,所有的upid都保存在一個散列表中,通過upid->pid_chain組織。散列表的表頭定義在:

<kernel/pid.c>

1 static struct hlist_head *pidhash;

 

        對於upid指向的pid_namespace,定義如下:

<pid_namespace.h>

 1 struct pid_namespace { 
 2     struct kref kref; 
 3     struct pidmap pidmap[PIDMAP_ENTRIES]; 
 4     int last_pid; 
 5     struct task_struct *child_reaper; 
 6     struct kmem_cache *pid_cachep; 
 7     unsigned int level; 
 8     struct pid_namespace *parent; 
 9 #ifdef CONFIG_PROC_FS 
10     struct vfsmount *proc_mnt; 
11 #endif 
12 #ifdef CONFIG_BSD_PROCESS_ACCT 
13     struct bsd_acct_struct *bacct; 
14 #endif 
15 };

        其中,kref保存實例被引用的次數;pidmap是一個位圖,保存該namespace中pid的分配情況;last_pid保存上一個分配的pid;對於每個namespace來說,都有一個進程來扮演Linux中init進程的角色,對所有的僵死進程執行wait4操作,child_reaper指向的就是這個進程(一般來說都是當前namespace中的0號進程);pid_cachep是一個kmem_cache對象,用來加速pid的分配,具體可以參考“Linux的物理內存管理”;level表示該namespace在整個命名空間的層次;parent表示該namespace的父namespace;proc_mnt表示在/proc文件系統的顯示結構;bacct保存BSD進程加速的結構。后兩者我們在這里不涉及。

        整個數據結構的組織如下圖:

3710358[4]

        可以看到圖分為四個部分:

  • 左下角的部分是PID namespace自己的組織結構,即是一個樹形的結構。struct pid_namespace自成體系,除了child_reaper之外並沒有更多和外部的聯系。
  • 右下角是所有的struct upid組成的散列表,通過pidhash為表頭的散列表組織,便於對upid的查找。
  • 右上角是使用pid的struct task_struct,不同的進程可能對應到同一個struct pid,例如同一個進程組的task_struct->pid[PIDTYPE_PGID],即同一進程組進程的進程組ID是相同的,指向同一個pid實例。
  • 中間處於最核心地位的是struct pid,可以看到struct pid可以通過其task成員數組來找到所有指向該pid的pid_link結構,從而找到對應的task_struct;此外struct pid還可以通過其numbers成員數組來獲得該struct pid所屬的不同層次的pid_namespace,二者的連接通過struct upid建立。

        總結來說,struct task_struct和其對應的struct pid通過struct pid_link連接,每個struct pid實例通過struct upid來和對應不同層次的struct pid_namespace連接。真正的PID值保存在struct upid結構中,這也是為什么需要將struct upid通過pidhash組織起來的原因。

六、getpid系統調用路徑

        getpid調用到系統調用sys_getpid,定義在timer.c中的SYSCALL_DEFINE0(getpid),之后的調用路徑為:

 

SYSCALL_DEFINE0(getpid)->task_tgid_vnr(current)->pid_vnr()->pid_nr_ns()

 

        最終獲得當前進程所屬命名空間看到的線程組leader的局部pid。可以通過上述路徑的代碼來得到一個簡單的pid_namespace使用實例。由於代碼很簡單,在這里就不詳細說明了。

七、Kernel中pid_namespace相關的函數
  • void attach_pid(struct task_struct *task, enum pid_type type, struct pid *pid);

    將新申請的struct pid實例連接到指定的task。

  • static inline struct pid *task_pid(struct task_struct *task);
    獲得與task_struct相關聯的PID的struct pid實例。
  • static inline struct pid *task_pgrp(struct task_struct *task);
    獲得與task_struct相關聯的PGRP的struct pid實例。
  • pid_t pid_nr_ns(struct pid *pid, struct pid_namespace *ns);
    獲得struct pid之后,從其numbers數組中的upid得到指定ns中的pid數值。
  • pid_t pid_vnr(struct pid *pid);
    獲得struct pid實例在當前進程所屬pid_namespace中的pid數值。
  • static inline pid_t pid_nr(struct pid *pid);
    獲得struct pid全局的pid數值。
  • static inline pid_t task_pid_nr_ns(struct task_struct *tsk, struct pid_namespace *ns);
    pid_t task_tgid_nr_ns(struct task_struct *tsk, struct pid_namespace *ns);

    static inline pid_t task_pgrp_nr_ns(struct task_struct *tsk, struct pid_namespace *ns);

    static inline pid_t task_session_nr_ns(struct task_struct *tsk, struct pid_namespace *ns);
    分別表示獲得進程在對應namespace中的PID/TGID/PGRP/SID的值。

  • struct pid *find_pid_ns(int nr, struct pid_namespace *ns);
    通過PID數值和指定namespace找到對應的struct pid結構。該函數的實現以來於pidhash組織的散列表。

  • find_task_by_pid_type_ns:通過pid數值、type、namespace找到對應的struct task_struct。
    find_task_by_pid_ns:通過pid數值和進程namespace找到對應的struct task_struct。
    find_task_by_vpid:根據pid數值在當前進程所處namespace(即局部的數字pid)來找到對應的進程。
    find_task_by_pid:根據pid數值在全局的namespace來找到對應的進程

  • struct pid *alloc_pid(struct pid_namespace *ns);
    在指定namespace中分配一個struct pid結構。

        我們來詳細分析一下alloc_pid代碼:

<pid.c>

 1 struct pid *alloc_pid(struct pid_namespace *ns) 
 2 { 
 3     struct pid *pid; 
 4     enum pid_type type; 
 5     int i, nr; 
 6     struct pid_namespace *tmp; 
 7     struct upid *upid; 
 8     // 在ns->pid_cachep指定的cache中分配struct pid 
 9     pid = kmem_cache_alloc(ns->pid_cachep, GFP_KERNEL); 
10     if (!pid) 
11         goto out; 
12     // 對指定namespace中每一層 
13     // 通過alloc_pidmap函數分配在該層namespace對應的pid數值 
14     // 保存在upid的numbers數組中 
15     tmp = ns; 
16     for (i = ns->level; i >= 0; i--) { 
17         nr = alloc_pidmap(tmp); 
18         if (nr < 0) 
19             goto out_free; 
20         pid->numbers[i].nr = nr; 
21         pid->numbers[i].ns = tmp; 
22         tmp = tmp->parent; 
23     } 
24     // get_pid_ns增加對應ns的引用數 
25     get_pid_ns(ns); 
26     // 設置struct pid的層次 
27     pid->level = ns->level; 
28     // 設置struct pid的引用數 
29     atomic_set(&pid->count, 1); 
30     // 初始化pid->tasks數組 
31     for (type = 0; type < PIDTYPE_MAX; ++type) 
32         INIT_HLIST_HEAD(&pid->tasks[type]); 
33     // 將struct pid中numbers數組保存的upid添加到pidhash中 
34     upid = pid->numbers + ns->level; 
35     spin_lock_irq(&pidmap_lock); 
36     for ( ; upid >= pid->numbers; --upid) 
37         hlist_add_head_rcu(&upid->pid_chain, 
38             &pid_hash[pid_hashfn(upid->nr, upid->ns)]); 
39     spin_unlock_irq(&pidmap_lock); 
40 // 成功返回 
41 out: 
42     return pid; 
43 // 錯誤處理 
44 out_free: 
45     while (++i <= ns->level) 
46         free_pidmap(pid->numbers + i); 
47     kmem_cache_free(ns->pid_cachep, pid); 
48     pid = NULL; 
49     goto out; 
50 }

 

 

        文中圖片來自《深入Linux內核架構》,代碼來自Kernel-3.2.0-rc1。轉載請注明出處。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM