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 由三個系統(tǒng)調(diào)用(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_*標(biāo)志 控制clone()操作的各個方面,包括父進(jìn)程和子進(jìn)程是否共享資源,例如虛擬內(nèi)存、打開的文件描述符和信號配置。
如果在調(diào)用中指定了CLONE_NEW* 之一,則會創(chuàng)建相應(yīng)類型的 新命名空間 ,并且新進(jìn)程將成為該****命名空間的成員;可以在flags中指定多個 CLONE_NEW* 。
在本文中,我們將研究 clone系統(tǒng)調(diào)用的 PID 命名空間部分,以及內(nèi)核如何組織 PID 命名空間的各種ID。本文分析基于內(nèi)核版本 linux-5.15.60。
一、PID命名空間基本概念
PID命名空間隔離的全局資源是“進(jìn)程ID編號”空間。這意味著“不同PID命名空間”中的進(jìn)程可以具有“相同的進(jìn)程ID”。PID命名空間用于“在主機(jī)系統(tǒng)之間遷移的容器”,同時保持容器內(nèi)部進(jìn)程的相同進(jìn)程ID。
與傳統(tǒng)Linux(或UNIX)系統(tǒng)上的進(jìn)程一樣,在PID命名空間中的進(jìn)程ID是唯一的,并且從 PID 1開始按順序分配。同樣地,與傳統(tǒng)Linux系統(tǒng)一樣,PID 1——init進(jìn)程是特殊的:它是在命名空間內(nèi)創(chuàng)建的第一個進(jìn)程,并且在命名空間內(nèi)執(zhí)行某些管理任務(wù)。
通過調(diào)用帶有 CLONE_NEWPID 標(biāo)志的clone()函數(shù)可以“創(chuàng)建一個新的PID命名空間”。我們將展示一個簡單的示例程序,使用clone()函數(shù)創(chuàng)建一個新的PID命名空間,并使用該程序來解釋PID命名空間的一些基本概念。
主程序使用clone()函數(shù)創(chuàng)建一個新的PID命名空間,并顯示生成子進(jìn)程的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);
新創(chuàng)建的子進(jìn)程在childFunc()中開始執(zhí)行,該函數(shù)接收clone()調(diào)用的最后一個參數(shù)(argv[1])作為它的參數(shù)。這個參數(shù)后面再解釋。childFunc()函數(shù)顯示由clone()創(chuàng)建的子進(jìn)程的進(jìn)程ID和父進(jìn)程ID,并最后執(zhí)行標(biāo)準(zhǔn)的sleep程序:
printf("childFunc(): PID = %ldn", (long) getpid());
printf("ChildFunc(): PPID = %ldn", (long) getppid());
...
execlp("sleep", "sleep", "1000", (char *) NULL);
當(dāng)我們運(yùn)行這個程序時,輸出的前幾行如下:
[root@haha demo]# ./pidns_init_sleep /proc30
PID returned by clone(): 25070
childFunc(): PID = 1
childFunc(): PPID = 0
Mounting
procfs at /proc30
前兩行輸出顯示了從兩個不同PID命名空間的角度來看子進(jìn)程的PID:調(diào)用clone()的“調(diào)用者的命名空間”和“子進(jìn)程所在的命名空間”。
換句話說,子進(jìn)程有兩個PID:在父命名空間中為 25070,在clone()調(diào)用創(chuàng)建的新PID命名空間中為1。下一行輸出顯示了子進(jìn)程在所在PID命名空間中的父進(jìn)程ID(即getppid()返回的值)。
父進(jìn)程PID為0,展示了PID命名空間操作的一個小特殊情況。
正如我們后面詳細(xì)介紹的那樣,PID命名空間形成了一個層次結(jié)構(gòu):一個進(jìn)程只能看到“自己所在的PID命名空間”和 嵌套在該P(yáng)ID命名空間下的“子命名空間中”的進(jìn)程。
由于由clone()“創(chuàng)建的子進(jìn)程的父進(jìn)程”處于不同的命名空間中,子進(jìn)程無法“看到”父進(jìn)程;因此,getppid()將父進(jìn)程PID報(bào)告為零。
要解釋pidns_init_sleep的最后一行輸出,我們需要回到一個我們在討論childFunc()函數(shù)實(shí)現(xiàn)時跳過的代碼片段。
在Linux系統(tǒng)上,每個進(jìn)程都有一個特殊的目錄路徑"/proc/PID",其中PID表示進(jìn)程的ID。這個目錄包含了描述該進(jìn)程的虛擬文件。
這個機(jī)制被稱為PID命名空間模型。在一個PID命名空間中,只有屬于該命名空間或其子命名空間的進(jìn)程的信息會顯示在對應(yīng)的"/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命名空間對應(yīng)的"/proc/PID"目錄可見,需要將proc文件系統(tǒng)掛載到該P(yáng)ID命名空間。我們可以在一個PID命名空間內(nèi)的shell中,運(yùn)行 mount命令來實(shí)現(xiàn):
mount -t proc proc /mount_point
另外,也可以使用mount()系統(tǒng)調(diào)用來掛載procfs,我們程序的childFunc()函數(shù)就是這樣的:
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命名空間中可見的進(jìn)程的PID子目錄,而在/proc30 上掛載的procfs將顯示駐留在子PID命名空間中的進(jìn)程的PID子目錄。
讓我們回到運(yùn)行pidns_init_sleep的shell會話。我們停止程序并使用ps命令在父命名空間的上下文中檢查父進(jìn)程和子進(jìn)程的一些細(xì)節(jié)。
上述輸出的最后一行中的"PPID"值(25069)顯示“執(zhí)行sleep的進(jìn)程”的父進(jìn)程是執(zhí)行pidns_init_sleep的進(jìn)程。
通過使用readlink命令來顯示/proc/PID/ns/pid符號鏈接,我們可以看到這兩個進(jìn)程位于不同的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來獲取有關(guān)新PID命名空間中進(jìn)程的信息,從該命名空間的角度來看。首先,我們可以使用以下命令獲取該命名空間中的PID列表:
[root@haha demo]# ls -d /proc30/[1-9]*
/proc30/1
如上所示,PID命名空間只包含一個進(jìn)程,其PID(在該命名空間內(nèi))為1。我們還可以使用/proc/PID/status文件作為另一種方法,獲取關(guān)于該進(jìn)程的一些相同信息,就像我們之前在shell會話中看到的那樣:
[root@haha demo]# cat /proc30/1/status | egrep '^(Name|PP*id)'
Name: sleep
Pid: 1
PPid: 0
[root@haha
demo]#
文件中的PPid字段為0,與getppid()報(bào)告子進(jìn)程的父進(jìn)程ID為0的事實(shí)相匹配。(子命名空間看不到父命名空間的進(jìn)程)
二、嵌套的PID命名空間
如前所述,PID(進(jìn)程標(biāo)識符)命名空間以父子關(guān)系的層級嵌套方式存在。在一個PID命名空間內(nèi),可以看到同一命名空間中的所有其他進(jìn)程,以及屬于后代命名空間的所有進(jìn)程。
在這里,“看到”意味著能夠進(jìn)行基于特定PID的系統(tǒng)調(diào)用(例如,使用kill()向進(jìn)程發(fā)送信號)。子PID命名空間中的進(jìn)程無法看到僅存在于父PID命名空間(或更遠(yuǎn)的祖先命名空間)中的進(jìn)程。
一個進(jìn)程在PID命名空間層級中的每一層都會有一個PID,從其所在的PID命名空間一直到根PID命名空間。調(diào)用getpid()始終報(bào)告與進(jìn)程所在命名空間相關(guān)聯(lián)的PID。
我們可以使用這里顯示的程序(multi_pidns.c)來展示進(jìn)程在每個可見的命名空間中具有不同的PID。為簡潔起見,我們將簡單地解釋程序的功能,而不是逐行解析其代碼。
該程序以嵌套PID命名空間中的子進(jìn)程遞歸方式創(chuàng)建一系列子進(jìn)程。在調(diào)用程序時指定的命令行參數(shù)確定要創(chuàng)建多少個子進(jìn)程和PID命名空間:
./multi_pidns 5
除了創(chuàng)建一個新的子進(jìn)程,每個遞歸步驟還在一個唯一命名的掛載點(diǎn)上掛載procfs文件系統(tǒng)。在遞歸的最后,最后一個子進(jìn)程執(zhí)行了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,我們可以看到每個連續(xù)的procfs "級別"包含的PID越來越少,這也表示了每個PID命名空間只顯示屬于該P(yáng)ID命名空間或其后代命名空間的進(jìn)程。
讓我們看下在所有可見的命名空間中,遞歸結(jié)束時的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 ) 中,執(zhí)行sleep的進(jìn)程的 PID 為 1,而在創(chuàng)建的最頂層 PID 命名空間 ( /proc4 ) 中,該進(jìn)程的 PID 為 5。
三、內(nèi)核實(shí)現(xiàn)PID命名空間
要了解內(nèi)核如何組織和管理進(jìn)程ID,首先要知道進(jìn)程ID 的類型:
內(nèi)核中進(jìn)程ID 的類型用 pid_type 來描述,它定義在 includelinuxpid.h 中
enum pid_type {
PIDTYPE_PID,
PIDTYPE_TGID,
PIDTYPE_PGID,
PIDTYPE_SID,
PIDTYPE_MAX,
};
- PID 是內(nèi)核唯一區(qū)分每個進(jìn)程的ID。使用 fork 或 clone 系統(tǒng)調(diào)用時生成的進(jìn)程將被內(nèi)核分配一個新的唯一 PID 值。
- TGID 是線程組ID。在一個進(jìn)程中,如果使用 clone_THREAD 標(biāo)志來調(diào)用 clone創(chuàng)建的進(jìn)程,那么它就是該進(jìn)程的一個線程(即輕量級進(jìn)程,Linux沒有嚴(yán)格的進(jìn)程概念),它們在一個線程組中。同一線程組中所有進(jìn)程都有相同的TGID,但由于是不同的進(jìn)程,所以它們的PID不同;線程的領(lǐng)導(dǎo)者(也稱為主線程)的TGID 與其 PID 相同。
- PGID 獨(dú)立進(jìn)程可以組成進(jìn)程組(使用 setpgrp 系統(tǒng)調(diào)用),進(jìn)程組可以簡化向組內(nèi)所有進(jìn)程發(fā)送信號的操作。例如,通過管道連接的連接屬于同一個進(jìn)程組。進(jìn)程組ID 稱為 PGID。進(jìn)程組中所有的進(jìn)程都有相同的 PGID,等于組長的 PID。
- SID 可以將多個進(jìn)程組組成一個會話組(使用 setsid 系統(tǒng)調(diào)用),可用于終端編程。會話組中所有進(jìn)程都有相同的SID,該SID 存儲在 task_struct 的 session 成員中。
PID命名空間的層級關(guān)系如下:有 4 個命名空間。父命名空間派生兩個子命名空間,其中一個子命名空間派生另一個子命名空間。
由于每個命名空間是相互隔離的,所以每個命名空間可以有一個 PID 為1的進(jìn)程。由于命名空間的層次性,父命名空間是知道子命名空間的存在的,所以子命名空間需要映射到父命名空間,
因此上圖中 第 1 級 的兩個兩個子命名空間中的 6 個進(jìn)程 都映射到 其父命名空間的 PID 號 5~ 10.
系統(tǒng)使用 struct task_struct 表示一個進(jìn)程,進(jìn)程中存儲了全局ID 和 本地ID。
全局ID ---- 內(nèi)核本身和初始命名空間中的唯一ID。 系統(tǒng)啟動時 init 進(jìn)程屬于初始命名空間。全局ID 包括 pid_t pid 和 pid_t tgid 。默認(rèn)情況下 pid_t 用 int 表示。
本地ID ---- 對于一個特定的命名空間來說,它在其命名空間中分配的ID就是本地ID。本地ID 用 struct pid * thread_pid 表示。
PID 數(shù)據(jù)結(jié)構(gòu)
成員 tasks 是一個數(shù)組,每個數(shù)組項(xiàng)是一個哈希表頭,對應(yīng)一個ID 類型,因此一個ID 可用于多個進(jìn)程(比如多個進(jìn)程的進(jìn)程組相同)。
struct upid {
int nr;// ID 的具體值
struct pid_namespace* ns;
};
struct pid {
refcount_t count;// 引用數(shù), 一個PID 可能用于多個進(jìn)程
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]; // 柔性數(shù)組,特定命名空間可見的信息, 數(shù)組大小為level
};
PID 命名空間結(jié)構(gòu)
struct pid_namespace {
struct idr idr;
struct rcu_head rcu;
unsigned int pid_allocated; // 已分配多少個pid
struct task_struct* child_reaper; // 指向當(dāng)前命名空間的 init 進(jìn)程,每個命名空間都有一個相當(dāng)于全局init進(jìn)程的進(jìn)程
struct kmem_cache* pid_cachep; // 指向分配pid 的slab地址
unsigned int level;// 當(dāng)前命名空間的級別。初始命名空間的級別為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;
假設(shè)一個進(jìn)程組中有A、B 兩個進(jìn)程,且進(jìn)程組組長為A,進(jìn)程A 是在 2 級命名空間中創(chuàng)建的,它的pid為45 ,映射到1級命名空間,分配給它的pid為123;然后它被映射到級別 0 的命名空間,分配給它的 pid 是 27760。
進(jìn)程A 創(chuàng)建了一個線程 A1, 那么 A, A1, B 的命名空間和進(jìn)程的關(guān)系如下圖所示:
- 進(jìn)程 A 的成員 struct pid* thread_pid 是內(nèi)核對進(jìn)程標(biāo)識符的內(nèi)部表示方式。
- struct pid 以哈希鏈表的方式存儲,可以通過數(shù)字pid值快速找到它和它所引用的進(jìn)程。
- struct pid 保存了 嵌套的多個命名空間的指針 和 進(jìn)程在此命名空間的進(jìn)程標(biāo)識符 nr。
- 命名空間使用基數(shù)樹保存當(dāng)前命名空間的 所有 struct pid,基數(shù)樹的索引就是 進(jìn)程在此命名空間的進(jìn)程標(biāo)識符。
最后有個問題:如何通過PID 快速找到 task_struct?
內(nèi)核代碼通過 find_task_by_vpid 來實(shí)現(xiàn)這個功能,其實(shí)通過上面這張圖就可以得出結(jié)論,簡單的步驟如下:
首先,通過 pid 和 命名空間nr,在基數(shù)樹上找到對應(yīng)的 struct pid;
然后,通過 pid_type 在 struct pid 找到對應(yīng)的節(jié)點(diǎn)struct hlist_node;
最后,根據(jù)內(nèi)核的 container_of 機(jī)制 和 struct hlist_node 可以找到 struct task_struct 結(jié)構(gòu)體。
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)
-
存儲器
+關(guān)注
關(guān)注
38文章
7528瀏覽量
164194 -
Linux系統(tǒng)
+關(guān)注
關(guān)注
4文章
595瀏覽量
27470 -
PID控制
+關(guān)注
關(guān)注
10文章
460瀏覽量
40201
發(fā)布評論請先 登錄
相關(guān)推薦
評論