Linux 支持以下命名空間類型:
- Mount (CLONE_NEWNS;2.4.19,2002)
- UTS (CLONE_NEWUTS; 2.6.19,2006)
- IPC (CLONE_NEWIPC; 2.6.19,2006)
- PID (CLONE_NEWPID; 2.6.24,2008)
- Network(CLONE_NEWNET;2.6.29,2009)
- User (CLONE_NEWUSER;3.8,2013)
- Cgroup(CLONE_NEWCGROUP;4.6,2016)
命名空間 API 由三個系統調用(clone()、unshare()和setns())以及許多/proc文件組成。CLONE_NEW* 常量包括:
CLONE_NEWIPC,CLONE_NEWNS , CLONE_NEWNET , CLONE_NEWPID ,CLONE_NEWUSER和 CLONE_NEWUTS 。
int clone(int (*child_func)(void *), void *child_stack, int flags, void *arg);
有二十多個不同的CLONE_*標志 控制clone()操作的各個方面,包括父進程和子進程是否共享資源,例如虛擬內存、打開的文件描述符和信號配置。
如果在調用中指定了CLONE_NEW* 之一,則會創建相應類型的 新命名空間 ,并且新進程將成為該****命名空間的成員;可以在flags中指定多個 CLONE_NEW* 。
在本文中,我們將研究 clone系統調用的 PID 命名空間部分,以及內核如何組織 PID 命名空間的各種ID。本文分析基于內核版本 linux-5.15.60。
一、PID命名空間基本概念
PID命名空間隔離的全局資源是“進程ID編號”空間。這意味著“不同PID命名空間”中的進程可以具有“相同的進程ID”。PID命名空間用于“在主機系統之間遷移的容器”,同時保持容器內部進程的相同進程ID。
與傳統Linux(或UNIX)系統上的進程一樣,在PID命名空間中的進程ID是唯一的,并且從 PID 1開始按順序分配。同樣地,與傳統Linux系統一樣,PID 1——init進程是特殊的:它是在命名空間內創建的第一個進程,并且在命名空間內執行某些管理任務。
通過調用帶有 CLONE_NEWPID 標志的clone()函數可以“創建一個新的PID命名空間”。我們將展示一個簡單的示例程序,使用clone()函數創建一個新的PID命名空間,并使用該程序來解釋PID命名空間的一些基本概念。
主程序使用clone()函數創建一個新的PID命名空間,并顯示生成子進程的PID:
child_pid = clone(childFunc,
child_stack + STACK_SIZE, /* Points to start of downwardly growing stack */
CLONE_NEWPID | SIGCHLD, argv[1]);
printf("PID returned by clone(): %ldn", (long) child_pid);
新創建的子進程在childFunc()中開始執行,該函數接收clone()調用的最后一個參數(argv[1])作為它的參數。這個參數后面再解釋。childFunc()函數顯示由clone()創建的子進程的進程ID和父進程ID,并最后執行標準的sleep程序:
printf("childFunc(): PID = %ldn", (long) getpid());
printf("ChildFunc(): PPID = %ldn", (long) getppid());
...
execlp("sleep", "sleep", "1000", (char *) NULL);
當我們運行這個程序時,輸出的前幾行如下:
[root@haha demo]# ./pidns_init_sleep /proc30
PID returned by clone(): 25070
childFunc(): PID = 1
childFunc(): PPID = 0
Mounting
procfs at /proc30
前兩行輸出顯示了從兩個不同PID命名空間的角度來看子進程的PID:調用clone()的“調用者的命名空間”和“子進程所在的命名空間”。
換句話說,子進程有兩個PID:在父命名空間中為 25070,在clone()調用創建的新PID命名空間中為1。下一行輸出顯示了子進程在所在PID命名空間中的父進程ID(即getppid()返回的值)。
父進程PID為0,展示了PID命名空間操作的一個小特殊情況。
正如我們后面詳細介紹的那樣,PID命名空間形成了一個層次結構:一個進程只能看到“自己所在的PID命名空間”和 嵌套在該PID命名空間下的“子命名空間中”的進程。
由于由clone()“創建的子進程的父進程”處于不同的命名空間中,子進程無法“看到”父進程;因此,getppid()將父進程PID報告為零。
要解釋pidns_init_sleep的最后一行輸出,我們需要回到一個我們在討論childFunc()函數實現時跳過的代碼片段。
在Linux系統上,每個進程都有一個特殊的目錄路徑"/proc/PID",其中PID表示進程的ID。這個目錄包含了描述該進程的虛擬文件。
這個機制被稱為PID命名空間模型。在一個PID命名空間中,只有屬于該命名空間或其子命名空間的進程的信息會顯示在對應的"/proc/PID"目錄中。
[root@haha linux-5.15.60]# mount |grep "proc on /proc"
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
proc on /proc2 type proc (rw,relatime)
proc on /proc2 type proc (rw,relatime)
proc on /proc10 type proc (rw,relatime)
proc on /proc20 type proc (rw,relatime)
proc on /proc30 type proc (rw,relatime)
[root@haha linux-5.15.60]#
但是,要使與PID命名空間對應的"/proc/PID"目錄可見,需要將proc文件系統掛載到該PID命名空間。我們可以在一個PID命名空間內的shell中,運行 mount命令來實現:
mount -t proc proc /mount_point
另外,也可以使用mount()系統調用來掛載procfs,我們程序的childFunc()函數就是這樣的:
char *mount_point = arg;
if (mount_point != NULL) {
mkdir(mount_point, 0555); /* Create directory for mount point */
if (mount("proc", mount_point, "proc", 0,NULL) == -1)
errExit("mount");
printf("Mounting procfs at %sn", mount_point);
}
在我們的shell會話中,在/proc上掛載的procfs將顯示父PID命名空間中可見的進程的PID子目錄,而在/proc30 上掛載的procfs將顯示駐留在子PID命名空間中的進程的PID子目錄。
讓我們回到運行pidns_init_sleep的shell會話。我們停止程序并使用ps命令在父命名空間的上下文中檢查父進程和子進程的一些細節。
上述輸出的最后一行中的"PPID"值(25069)顯示“執行sleep的進程”的父進程是執行pidns_init_sleep的進程。
通過使用readlink命令來顯示/proc/PID/ns/pid符號鏈接,我們可以看到這兩個進程位于不同的PID命名空間中:
[root@haha demo]# readlink /proc/25069/ns/pid
pid:[4026531836]
[root@haha demo]# readlink /proc/25070/ns/pid
pid:[4026537948]
[root@haha demo]#
此時,我們還可以使用新掛載的procfs來獲取有關新PID命名空間中進程的信息,從該命名空間的角度來看。首先,我們可以使用以下命令獲取該命名空間中的PID列表:
[root@haha demo]# ls -d /proc30/[1-9]*
/proc30/1
如上所示,PID命名空間只包含一個進程,其PID(在該命名空間內)為1。我們還可以使用/proc/PID/status文件作為另一種方法,獲取關于該進程的一些相同信息,就像我們之前在shell會話中看到的那樣:
[root@haha demo]# cat /proc30/1/status | egrep '^(Name|PP*id)'
Name: sleep
Pid: 1
PPid: 0
[root@haha
demo]#
文件中的PPid字段為0,與getppid()報告子進程的父進程ID為0的事實相匹配。(子命名空間看不到父命名空間的進程)
二、嵌套的PID命名空間
如前所述,PID(進程標識符)命名空間以父子關系的層級嵌套方式存在。在一個PID命名空間內,可以看到同一命名空間中的所有其他進程,以及屬于后代命名空間的所有進程。
在這里,“看到”意味著能夠進行基于特定PID的系統調用(例如,使用kill()向進程發送信號)。子PID命名空間中的進程無法看到僅存在于父PID命名空間(或更遠的祖先命名空間)中的進程。
一個進程在PID命名空間層級中的每一層都會有一個PID,從其所在的PID命名空間一直到根PID命名空間。調用getpid()始終報告與進程所在命名空間相關聯的PID。
我們可以使用這里顯示的程序(multi_pidns.c)來展示進程在每個可見的命名空間中具有不同的PID。為簡潔起見,我們將簡單地解釋程序的功能,而不是逐行解析其代碼。
該程序以嵌套PID命名空間中的子進程遞歸方式創建一系列子進程。在調用程序時指定的命令行參數確定要創建多少個子進程和PID命名空間:
./multi_pidns 5
除了創建一個新的子進程,每個遞歸步驟還在一個唯一命名的掛載點上掛載procfs文件系統。在遞歸的最后,最后一個子進程執行了sleep程序。上述命令行輸出如下:
[root@haha demo]# ls -d /proc4/[1-9]*
/proc4/1 /proc4/2 /proc4/3 /proc4/4 /proc4/5
[root@haha demo]# ls -d /proc3/[1-9]*
/proc3/1 /proc3/2 /proc3/3 /proc3/4
[root@haha demo]# ls -d /proc2/[1-9]*
/proc2/1 /proc2/2 /proc2/3
[root@haha demo]# ls -d /proc1/[1-9]*
/proc1/1 /proc1/2
[root@haha demo]# ls -d /proc0/[1-9]*
/proc0/1
查看每個procfs中的PID,我們可以看到每個連續的procfs "級別"包含的PID越來越少,這也表示了每個PID命名空間只顯示屬于該PID命名空間或其后代命名空間的進程。
讓我們看下在所有可見的命名空間中,遞歸結束時的PID:
[root@haha demo]# grep -H 'Name:.*sleep'/proc?/[1-9]*/status
/proc0/1/status:Name: sleep
/proc1/2/status:Name: sleep
/proc2/3/status:Name: sleep
/proc3/4/status:Name: sleep
/proc4/5/status:Name: sleep
[root@haha demo]#
換句話說,在最深層嵌套的 PID 命名空間 ( /proc0 ) 中,執行sleep的進程的 PID 為 1,而在創建的最頂層 PID 命名空間 ( /proc4 ) 中,該進程的 PID 為 5。
三、內核實現PID命名空間
要了解內核如何組織和管理進程ID,首先要知道進程ID 的類型:
內核中進程ID 的類型用 pid_type 來描述,它定義在 includelinuxpid.h 中
enum pid_type {
PIDTYPE_PID,
PIDTYPE_TGID,
PIDTYPE_PGID,
PIDTYPE_SID,
PIDTYPE_MAX,
};
- PID 是內核唯一區分每個進程的ID。使用 fork 或 clone 系統調用時生成的進程將被內核分配一個新的唯一 PID 值。
- TGID 是線程組ID。在一個進程中,如果使用 clone_THREAD 標志來調用 clone創建的進程,那么它就是該進程的一個線程(即輕量級進程,Linux沒有嚴格的進程概念),它們在一個線程組中。同一線程組中所有進程都有相同的TGID,但由于是不同的進程,所以它們的PID不同;線程的領導者(也稱為主線程)的TGID 與其 PID 相同。
- PGID 獨立進程可以組成進程組(使用 setpgrp 系統調用),進程組可以簡化向組內所有進程發送信號的操作。例如,通過管道連接的連接屬于同一個進程組。進程組ID 稱為 PGID。進程組中所有的進程都有相同的 PGID,等于組長的 PID。
- SID 可以將多個進程組組成一個會話組(使用 setsid 系統調用),可用于終端編程。會話組中所有進程都有相同的SID,該SID 存儲在 task_struct 的 session 成員中。
PID命名空間的層級關系如下:有 4 個命名空間。父命名空間派生兩個子命名空間,其中一個子命名空間派生另一個子命名空間。
由于每個命名空間是相互隔離的,所以每個命名空間可以有一個 PID 為1的進程。由于命名空間的層次性,父命名空間是知道子命名空間的存在的,所以子命名空間需要映射到父命名空間,
因此上圖中 第 1 級 的兩個兩個子命名空間中的 6 個進程 都映射到 其父命名空間的 PID 號 5~ 10.
系統使用 struct task_struct 表示一個進程,進程中存儲了全局ID 和 本地ID。
全局ID ---- 內核本身和初始命名空間中的唯一ID。 系統啟動時 init 進程屬于初始命名空間。全局ID 包括 pid_t pid 和 pid_t tgid 。默認情況下 pid_t 用 int 表示。
本地ID ---- 對于一個特定的命名空間來說,它在其命名空間中分配的ID就是本地ID。本地ID 用 struct pid * thread_pid 表示。
PID 數據結構
成員 tasks 是一個數組,每個數組項是一個哈希表頭,對應一個ID 類型,因此一個ID 可用于多個進程(比如多個進程的進程組相同)。
struct upid {
int nr;// ID 的具體值
struct pid_namespace* ns;
};
struct pid {
refcount_t count;// 引用數, 一個PID 可能用于多個進程
unsigned int level;
spinlock_t lock;
/* lists of tasks that use this pid */
struct hlist_head tasks[PIDTYPE_MAX];
struct hlist_head inodes;
/* wait queue for pidfd notifications */
wait_queue_head_twait_pidfd;
struct rcu_head rcu;
struct upid numbers[1]; // 柔性數組,特定命名空間可見的信息, 數組大小為level
};
PID 命名空間結構
struct pid_namespace {
struct idr idr;
struct rcu_head rcu;
unsigned int pid_allocated; // 已分配多少個pid
struct task_struct* child_reaper; // 指向當前命名空間的 init 進程,每個命名空間都有一個相當于全局init進程的進程
struct kmem_cache* pid_cachep; // 指向分配pid 的slab地址
unsigned int level;// 當前命名空間的級別。初始命名空間的級別為0,其子命名空間級別為1,依次遞增。
struct pid_namespace* parent; // 指向父命名空間
#ifdefCONFIG_BSD_PROCESS_ACCT
struct fs_pin* bacct;
#endif
struct user_namespace* user_ns;
struct ucounts* ucounts;
int reboot;/* group exit code if this pidns was rebooted */
struct ns_common ns;
} __randomize_layout;
假設一個進程組中有A、B 兩個進程,且進程組組長為A,進程A 是在 2 級命名空間中創建的,它的pid為45 ,映射到1級命名空間,分配給它的pid為123;然后它被映射到級別 0 的命名空間,分配給它的 pid 是 27760。
進程A 創建了一個線程 A1, 那么 A, A1, B 的命名空間和進程的關系如下圖所示:
- 進程 A 的成員 struct pid* thread_pid 是內核對進程標識符的內部表示方式。
- struct pid 以哈希鏈表的方式存儲,可以通過數字pid值快速找到它和它所引用的進程。
- struct pid 保存了 嵌套的多個命名空間的指針 和 進程在此命名空間的進程標識符 nr。
- 命名空間使用基數樹保存當前命名空間的 所有 struct pid,基數樹的索引就是 進程在此命名空間的進程標識符。
最后有個問題:如何通過PID 快速找到 task_struct?
內核代碼通過 find_task_by_vpid 來實現這個功能,其實通過上面這張圖就可以得出結論,簡單的步驟如下:
首先,通過 pid 和 命名空間nr,在基數樹上找到對應的 struct pid;
然后,通過 pid_type 在 struct pid 找到對應的節點struct hlist_node;
最后,根據內核的 container_of 機制 和 struct hlist_node 可以找到 struct task_struct 結構體。
struct task_struct* find_task_by_vpid(pid_t vnr) {
return find_task_by_pid_ns(vnr,task_active_pid_ns(current));
}
struct task_struct* find_task_by_pid_ns(pid_t nr, struct pid_namespace* ns) {
RCU_LOCKDEP_WARN(!rcu_read_lock_held(), "find_task_by_pid_ns() needs rcu_read_lock() protection");
return pid_task(find_pid_ns(nr, ns),PIDTYPE_PID);
}
struct pid* find_pid_ns(int nr, struct pid_namespace* ns) {
return idr_find(&ns- >idr, nr);
}
struct task_struct* pid_task(struct pid* pid, enum pid_type type) {
struct task_struct* result = NULL;
if (pid)
{
structhlist_node* first;
first = rcu_dereference_check(hlist_first_rcu(&pid- >tasks[type]),
lockdep_tasklist_lock_is_held());
if (first)
result =hlist_entry(first, struct task_struct, pid_links[(type)]);
}
return result;
}
#define hlist_entry(ptr, type, member) container_of(ptr,type,member)
-
存儲器
+關注
關注
38文章
7636瀏覽量
166449 -
Linux系統
+關注
關注
4文章
603瀏覽量
28321 -
PID控制
+關注
關注
10文章
461瀏覽量
41030
發布評論請先 登錄
評論