一、前言
其實(shí)兩年前,本站已經(jīng)有了一篇關(guān)于進(jìn)程標(biāo)識的文檔,不過非常的簡陋,而且代碼是來自2.6內(nèi)核。隨著linux container、pid namespace等概念的引入,進(jìn)程標(biāo)識方面已經(jīng)有了天翻地覆的變化,因此我們需要對這部分的內(nèi)容進(jìn)行重新整理。
本文主要分成四個(gè)部分來描述進(jìn)程標(biāo)識這個(gè)主題:在初步介紹了一些入門的各種IDs基礎(chǔ)知識后,在第三章我們描述了pid、pid number、pid namespace等基礎(chǔ)的概念。第四章重點(diǎn)描述了內(nèi)核如何將這些基本概念抽象成具體的數(shù)據(jù)結(jié)構(gòu),最后我們簡單分析了內(nèi)核關(guān)于進(jìn)程標(biāo)識的源代碼(代碼來自linux4.4.6版本)。
二、各種ID概述
所謂進(jìn)程其實(shí)就是執(zhí)行中的程序而已,和靜態(tài)的程序相比,進(jìn)程是一個(gè)運(yùn)行態(tài)的實(shí)體,擁有各種各樣的資源:地址空間(未必使用全部地址空間,而是排布在地址空間上的一段段的memory mappings)、打開的文件、pending的信號、一個(gè)或者多個(gè)thread of execution,內(nèi)核中數(shù)據(jù)實(shí)體(例如一個(gè)或者多個(gè)task_struct實(shí)體),內(nèi)核棧(也是一個(gè)或者多個(gè))等。針對進(jìn)程,我們使用進(jìn)程ID,也就是pid(process ID)。通過getpid和getppid可以獲取當(dāng)前進(jìn)程的pid以及父進(jìn)程的pid。
進(jìn)程中的thread of execution被稱作線程(thread),線程是進(jìn)程中活躍狀態(tài)的實(shí)體。一方面進(jìn)程中所有的線程共享一些資源,另外一方面,線程又有自己專屬的資源,例如有自己的PC值,用戶棧、內(nèi)核棧,有自己的hw context、調(diào)度策略等等。我們一般會說進(jìn)程調(diào)度什么的,但是實(shí)際上線程才是是調(diào)度器的基本單位。對于Linux內(nèi)核,線程的實(shí)現(xiàn)是一種特別的存在,和經(jīng)典的unix都不一樣。在linux中并不區(qū)分進(jìn)程和線程,都是用task_struct來抽象,只不過支持多線程的進(jìn)程是由一組task_struct來抽象,而這些task_struct會共享一些數(shù)據(jù)結(jié)構(gòu)(例如內(nèi)存描述符)。我們用thread ID來唯一標(biāo)識進(jìn)程中的線程,POSIX規(guī)定線程ID在所屬進(jìn)程中是唯一的,不過在linux kernel的實(shí)現(xiàn)中,thread ID是全系統(tǒng)唯一的,當(dāng)然,考慮到可移植性,Application software不應(yīng)該假設(shè)這一點(diǎn)。在用戶空間,通過gettid函數(shù)可以獲取當(dāng)前線程的thread ID。對于單線程的進(jìn)程,process ID和thread ID是一樣的,對于支持多線程的進(jìn)程,每個(gè)線程有自己的thread ID,但是所有的線程共享一個(gè)PID。
為了方便shell進(jìn)行Job controll,我們需要把一組進(jìn)程組織起來形成進(jìn)程組。關(guān)于這方面的概念,在進(jìn)程和終端文檔中描述的很詳細(xì),這里就不贅述了。為了標(biāo)識進(jìn)程組,我們需要引入進(jìn)程組ID的概念。我們一般把進(jìn)程組中的第一個(gè)進(jìn)程的ID作為進(jìn)程組的ID,進(jìn)程組中的所有進(jìn)程共享一個(gè)進(jìn)程組ID。在用戶空間,通過setpgid、getpgid、setpgrp和getpgrp等接口函數(shù)可以訪問process group ID。
經(jīng)過thread ID、process ID、process group ID的層層遞進(jìn),我們終于來到最頂層的ID,也就是session ID,這個(gè)ID實(shí)際上是用來標(biāo)識計(jì)算機(jī)系統(tǒng)中的一次用戶交互過程:用戶登錄入系統(tǒng),不斷的提交任務(wù)(即Job或者說是進(jìn)程組)給計(jì)算機(jī)系統(tǒng)并觀察結(jié)果,最后退出登錄,銷毀該session。關(guān)于session的概念,在進(jìn)程和終端文檔中描述的也很詳細(xì),大家可以參考那份文檔,這里就不贅述了。在用戶空間,我們可以通過getsid、setsid來操作session ID。
三、基礎(chǔ)概念
1、用戶空間如何看到process ID
我們用下面這個(gè)block diagram來描述用戶空間和內(nèi)核空間如何看待process ID的:
從用戶空間來看,每一個(gè)進(jìn)程都可以調(diào)用getpid來獲取標(biāo)識該進(jìn)程的ID,我們稱之PID,其類型是pid_t。因此,我們知道在用戶空間可以通過一個(gè)正整數(shù)來唯一標(biāo)識一個(gè)進(jìn)程(我們稱這個(gè)正整數(shù)為pid number)。在引入容器之后,事情稍微復(fù)雜一點(diǎn),pid這個(gè)正整數(shù)只能是唯一標(biāo)識容器內(nèi)的進(jìn)程。也就是說,如果有容器1和容器2存在于系統(tǒng)中,那么可以同時(shí)存在兩個(gè)pid等于a的進(jìn)程,分別位于容器1和容器2。當(dāng)然,進(jìn)程也可以不在容器里,例如進(jìn)程x和進(jìn)程y,它們就類似傳統(tǒng)的linux系統(tǒng)中的進(jìn)程。當(dāng)然,你也可以認(rèn)為進(jìn)程x和進(jìn)程y位于一個(gè)系統(tǒng)級別的頂層容器0,其中包括進(jìn)程x和進(jìn)程y以及兩個(gè)容器。同樣的概念,容器2中也可以嵌套一個(gè)容器,從而形成了一個(gè)container hierarchy。
容器(linux container)是一個(gè)OS級別的虛擬化方法,基本上是屬于純軟件的方法來實(shí)現(xiàn)虛擬化,開銷小,量級輕,當(dāng)然也有自己的局限。linux container主要應(yīng)用了內(nèi)核中的cgroup和namespace隔離技術(shù),當(dāng)然這些內(nèi)容不是我們這份文檔關(guān)心的,我們這里主要關(guān)心pid namespace。
當(dāng)一個(gè)進(jìn)程運(yùn)行在linux OS之上的時(shí)候,它擁有了很多的系統(tǒng)資源,例如pid、user ID、網(wǎng)絡(luò)設(shè)備、協(xié)議棧、IP以及端口號、filesystem hierarchy。對于傳統(tǒng)的linux,這些資源都是全局性的,一個(gè)進(jìn)程umount了某一個(gè)文件系統(tǒng)掛載點(diǎn),改變了自己的filesystem hierarchy視圖,那么所有進(jìn)程看到的文件系統(tǒng)目錄結(jié)構(gòu)都變化了(umount操作被所有進(jìn)程感知到了)。有沒有可能把這些資源隔離開呢?這就是namespace的概念,而PID namespace就是用來隔離pid的地址空間的。
進(jìn)程是感知不到pid namespace的,它只是知道能夠通過getpid獲取自己的ID,并不知道自己實(shí)際上被關(guān)在一個(gè)pid namespace的牢籠。從這個(gè)角度看,用戶空間是簡單而幸福的,內(nèi)核空間就沒有這么幸運(yùn)了,我們需要使用復(fù)雜的數(shù)據(jù)結(jié)構(gòu)來抽象這些形成層次結(jié)構(gòu)的PID。
最后順便說一句,上面的描述是針對pid而言的,實(shí)際上,tid、pgid和sid都是一樣的概念,原來直接使用這些ID就可以唯一標(biāo)識一個(gè)實(shí)體,現(xiàn)在我們需要用(pid namespace,ID)來唯一標(biāo)識一個(gè)實(shí)體。
2、內(nèi)核空間如何看到process ID
雖然從用戶空間看,一個(gè)pid用一個(gè)正整數(shù)表示就足夠了,但是在內(nèi)核空間,一個(gè)正整數(shù)肯定是不行的,我們用一個(gè)2個(gè)層次的pid namespace來描述(也就是上面圖片的情形)。pid namespace 0是pid namespace 1和2的parent namespace,在pid namespace 1中的pid等于a的那進(jìn)程,對應(yīng)pid namespace 0中的pid等于m的那進(jìn)程,也就是說,內(nèi)核態(tài)實(shí)際需要兩個(gè)不同namespace中的正整數(shù)來記錄一個(gè)進(jìn)程的ID信息。推廣開來,我們可以這么描述,在一個(gè)n個(gè)level的pid namespace hieraray中,位于x level的進(jìn)程需要x個(gè)正整數(shù)ID來表示該該進(jìn)程。
除此之外,內(nèi)核還有記錄pid namespace之間的關(guān)系:誰是根,誰是葉,父子關(guān)系……
四、內(nèi)核態(tài)的數(shù)據(jù)抽象
1、如何抽象pid number?
struct upid {?
??? int nr;?
??? struct pid_namespace *ns;?
??? struct hlist_node pid_chain;?
};
雖然用戶空間使用一個(gè)正整數(shù)來表示各種IDs,但是對于內(nèi)核,我們需要使用(pid namespace,ID number)這樣的二元組來表示,因?yàn)閱渭兊膒id number是沒有意義的,必須限定其pid namespace,只有這樣,那個(gè)ID number才是唯一的。這樣,upid中的nr和ns成員就比較好理解了,分別對應(yīng)ID number和pid namespace。此外,當(dāng)userspace傳遞ID number參數(shù)進(jìn)入內(nèi)核請求服務(wù)的時(shí)候(例如向某一個(gè)ID發(fā)送信號),我們必須需要通過ID number快速找到其對應(yīng)的upid數(shù)據(jù)對象,為了應(yīng)對這樣的需求,內(nèi)核將系統(tǒng)內(nèi)所有的upid保存在哈希表中,pid_chain成員是哈希表中的next node。
2、如何抽象tid、pid、sid、pgid?
struct pid?
{?
??? atomic_t count;?
??? unsigned int level;?
??? struct hlist_head tasks[PIDTYPE_MAX];?
??? struct rcu_head rcu;?
??? struct upid numbers[1];?
};
雖然其名字是pid,不過實(shí)際上這個(gè)數(shù)據(jù)結(jié)構(gòu)抽象了不僅僅是一個(gè)thread ID或者process ID,實(shí)際上還包括了進(jìn)程組ID和session ID。由于多個(gè)task struct會共享pid(例如一個(gè)session中的所有的task struct都會指向同一個(gè)表示該session ID的struct pid數(shù)據(jù)對象),因此存在count這樣的成員也就不奇怪了,表示該數(shù)據(jù)對象的引用計(jì)數(shù)。
在了解了pid namespace hierarchy之后,level成員也不難理解,任何一個(gè)系統(tǒng)分配的PID都是隸屬于某一個(gè)namespace的,而這個(gè)namespace又是位于整個(gè)pid namespace hierarchy的某個(gè)層次上,pid->level指明了該P(yáng)ID所屬的namespace的level。由于pid對其parent pid namespace也是可見的,因此,這個(gè)level值其實(shí)也就表示了這個(gè)pid對象在多少個(gè)pid namespace中可見。
在多少個(gè)pid namespace中可見,就會有多少個(gè)(pid namespace,pid number)對,numbers就是這樣的一個(gè)數(shù)組,表示了在各個(gè)level上的pid number。tasks成員和使用該struct pid的task們關(guān)聯(lián),我們在下一節(jié)描述。
3、進(jìn)程描述符中如何體現(xiàn)tid、pid、sid、pgid?
由于多個(gè)task共享ID(泛指上面說的四種ID),因此在設(shè)計(jì)數(shù)據(jù)結(jié)構(gòu)的時(shí)候我們要考慮兩種情況:
(1)從task struct快速找到對應(yīng)的struct pid
(2)從struct pid能夠遍歷所有使用該pid的task
在這樣的要求下,我們設(shè)計(jì)了一個(gè)輔助數(shù)據(jù)結(jié)構(gòu):
struct pid_link?
{?
??? struct hlist_node node;?
??? struct pid *pid;?
};
其中node是將task串接到struct pid的task struct鏈表中的節(jié)點(diǎn),而pid指向具體的struct pid。這時(shí)候,我們可以在task struct中嵌入一個(gè)pid_link的數(shù)組:
struct task_struct {?
……?
struct pid_link pids[PIDTYPE_MAX];?
……?
}
Task struct中的pids成員是一個(gè)數(shù)組,分別表示該task的tid(pid)、pgid和sid。我們定義pid的類型如下:
enum pid_type?
{?
??? PIDTYPE_PID,?
??? PIDTYPE_PGID,?
??? PIDTYPE_SID,?
??? PIDTYPE_MAX?
};
一直以來我們都是說四種type,tid、pid、sid、pgid,為何這里少定義一種呢?其實(shí)開始版本的內(nèi)核的確是定義了四種type的pid,但是后來為了節(jié)省內(nèi)存,tid和pid合二為一了。OK,現(xiàn)在已經(jīng)引入太多的數(shù)據(jù)結(jié)構(gòu),下面我們用一幅圖片來描述數(shù)據(jù)結(jié)構(gòu)之間的關(guān)系:
對于一個(gè)進(jìn)程中的多個(gè)線程而言,每一個(gè)線程都可以通過task->pids[PIDTYPE_PID].pid找到該線程對應(yīng)的表示thread ID的那個(gè)struct pid數(shù)據(jù)對象。當(dāng)然,任何一個(gè)線程都有其所屬的進(jìn)程,也就是有表示其process id的那個(gè)struct pid數(shù)據(jù)對象。如何找到它呢?這需要一個(gè)橋梁,也就是task struct中定義的thread group 成員(task->group_leader),通過該指針,一個(gè)線程總是很容易的找到其對應(yīng)的線程組leader,而線程組leader對應(yīng)的pid就是該線程的process ID。因此,對于一個(gè)線程,其task->group_leader->pids[PIDTYPE_PID].pid就指向了表示其process id的那個(gè)struct pid數(shù)據(jù)對象。當(dāng)然,對于線程組leader,其thread ID和process ID的struct pid數(shù)據(jù)對象是一個(gè)實(shí)體,對于非線程組leader的那些普通線程,其thread ID和process ID的struct pid數(shù)據(jù)對象指向不同的實(shí)體。
struct pid有三個(gè)鏈表頭,如果該pid僅僅是標(biāo)識一個(gè)thread ID,那么其pid鏈表頭指向的鏈表中只有一個(gè)元素,就是使用該pid的task struct。如果該pid表示的是一個(gè)process ID,那么pid鏈表頭指向的鏈表中多個(gè)task struct,每一個(gè)元素表示了屬于該進(jìn)程的線程的task struct,鏈表中第一個(gè)task struct是thread group leader。如果該pid并不表示一個(gè)process group ID或者session ID,那么struct pid中的pgid鏈表頭和session鏈表頭都是指向null。如果該pid表示一個(gè)process group ID的時(shí)候,其結(jié)構(gòu)如下圖所示:
對于那些multi-thread進(jìn)程,內(nèi)核有若干個(gè)task struct和進(jìn)程對應(yīng),不過為了簡單,在上面圖片中,進(jìn)程x 對應(yīng)的task struct實(shí)際上是thread group leader對應(yīng)的那個(gè)task struct。這些task struct的pgid指針(task->pids[PIDTYPE_PGID].pid)指向了該進(jìn)程組對應(yīng)的struct pid數(shù)據(jù)對象。而該pid中的pgid鏈表頭串聯(lián)了所有使用該pid的task struct(僅僅是串聯(lián)thread group leader對應(yīng)的那些task struct),而鏈表中的第一個(gè)節(jié)點(diǎn)就是進(jìn)程組leader。
session pid的概念是類似的,大家可以自行了解學(xué)習(xí)。
4、如何抽象 pid namespace?
好吧,這個(gè)有點(diǎn)復(fù)雜,暫時(shí)TODO吧。
五、代碼分析
1、如何根據(jù)一個(gè)task struct得到其對應(yīng)的thread ID?
static inline struct pid *task_pid(struct task_struct *task)?
{?
??? return task->pids[PIDTYPE_PID].pid;?
}
同樣的道理,我們也可以很容易得到一個(gè)task對應(yīng)的pgid和sid。process ID有一點(diǎn)繞,我們首先要找到該task的thread group leader對應(yīng)的task,其實(shí)一個(gè)線程的thread group leader對應(yīng)的那個(gè)task的thread ID就是該線程的process ID。
2、如何根據(jù)一個(gè)task struct得到當(dāng)前的pid namespace?
struct pid_namespace *task_active_pid_ns(struct task_struct *tsk)?
{?
??? return ns_of_pid(task_pid(tsk));?
}
這個(gè)操作可以分成兩步,第一步首先找到其對應(yīng)的thread ID,然后根據(jù)thread ID找到當(dāng)前的pid namespace,代碼如下:
static inline struct pid_namespace *ns_of_pid(struct pid *pid)?
{?
??? struct pid_namespace *ns = NULL;?
??? if (pid)?
??????? ns = pid->numbers[pid->level].ns;?
??? return ns;?
}
一個(gè)struct pid實(shí)體是有層次的,對應(yīng)了若干層次的(pid namespace,pid number)二元組,最頂層是root pid namespace,最底層(葉節(jié)點(diǎn))是當(dāng)前的pid namespace,pid->level表示了當(dāng)前的層次,因此pid->numbers[pid->level].ns說明的就是當(dāng)前的pid namespace。
3、getpid是如何實(shí)現(xiàn)的?
當(dāng)陷入內(nèi)核后,我們很容易獲取當(dāng)前的task struct(根據(jù)sp_svc的值),這是起點(diǎn),后續(xù)的代碼如下:
static inline pid_t task_tgid_vnr(struct task_struct *tsk)?
{?
??? return pid_vnr(task_tgid(tsk));?
}
通過task_tgid可以獲取該task對應(yīng)的thread group leader的thread ID,其實(shí)也就是process ID。此外,通過task_active_pid_ns亦可以獲取當(dāng)前的pid namespace,有了這兩個(gè)參數(shù),可以調(diào)用pid_nr_ns獲取該task對應(yīng)的pid number:
pid_t pid_nr_ns(struct pid *pid, struct pid_namespace *ns)?
{?
??? struct upid *upid;?
??? pid_t nr = 0;
if (pid && ns->level <= pid->level) {?
??????? upid = &pid->numbers[ns->level];?
??????? if (upid->ns == ns)?
??????????? nr = upid->nr;?
??? }?
??? return nr;?
}
一個(gè)pid可以貫穿多個(gè)pid namespace,但是并非所有的pid namespace都可以檢視pid,獲取相應(yīng)的pid number。因此,在代碼的開始會進(jìn)行驗(yàn)證,如果pid namespace的層次(ns->level)低于pid當(dāng)前的pid namespace的層次,那么直接返回0。如果pid namespace的level是OK的,那么要檢查該namespace是不是pid當(dāng)前的那個(gè)pid namespace,如果是,直接返回對應(yīng)的pid number,否則,返回0。
對于gettid和getppid這兩個(gè)接口,整體的概念是和getpid類似的,不再贅述。
4、給定線程ID number的情況下,如何找對應(yīng)的task struct?
這里給定的條件包括ID number、當(dāng)前的pid namespace,在這樣的條件下如何找到對應(yīng)的task呢?我們分成兩個(gè)步驟,第一個(gè)步驟是先找到對應(yīng)的struct pid,代碼如下:
struct pid *find_pid_ns(int nr, struct pid_namespace *ns)?
{?
??? struct upid *pnr;
hlist_for_each_entry_rcu(pnr,?
??????????? &pid_hash[pid_hashfn(nr, ns)], pid_chain)?
??????? if (pnr->nr == nr && pnr->ns == ns)?
??????????? return container_of(pnr, struct pid,?
??????????????????? numbers[ns->level]);
return NULL;?
}
整個(gè)系統(tǒng)有那么多的struct pid數(shù)據(jù)對象,每一個(gè)pid又有多個(gè)level的(pid namespace,pid number)對,通過pid number和namespace來找對應(yīng)的pid是一件非常耗時(shí)的操作。此外,這樣的操作是一個(gè)比較頻繁的操作,一個(gè)簡單的例子就是通過kill向指定進(jìn)程(pid number)發(fā)送信號。正是由于操作頻繁而且耗時(shí),系統(tǒng)建立了一個(gè)全局的哈希鏈表來解決這個(gè)問題,pid_hash指向了若干(具體head的數(shù)量和內(nèi)存配置有關(guān))哈希鏈表頭。這個(gè)哈希表用來通過一個(gè)指定pid namespace和id number,來找到對應(yīng)的struct upid。一旦找了upid,那么通過container_of找到對應(yīng)的struct pid數(shù)據(jù)對象。
第二步是從struct pid找到task struct,代碼如下:
struct task_struct *pid_task(struct pid *pid, enum pid_type type)?
{?
??? struct task_struct *result = NULL;?
??? if (pid) {?
??????? struct hlist_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, pids[(type)].node);?
??? }?
??? return result;?
}
5、getpgid是如何實(shí)現(xiàn)的?
SYSCALL_DEFINE1(getpgid, pid_t, pid)?
{?
??? struct task_struct *p;?
??? struct pid *grp;?
??? int retval;
rcu_read_lock();?
??? if (!pid)?
??????? grp = task_pgrp(current);?
??? else {?
??????? retval = -ESRCH;?
??????? p = find_task_by_vpid(pid);?
??????? if (!p)?
??????????? goto out;?
??????? grp = task_pgrp(p);?
??????? if (!grp)?
??????????? goto out;
retval = security_task_getpgid(p);?
??????? if (retval)?
??????????? goto out;?
??? }?
??? retval = pid_vnr(grp);?
out:?
??? rcu_read_unlock();?
??? return retval;?
}
當(dāng)傳入的pid number等于0的時(shí)候,getpgid實(shí)際上是獲取當(dāng)前進(jìn)程的process groud ID number,通過task_pgrp可以獲取該進(jìn)程的使用的表示progress group ID對應(yīng)的那個(gè)pid對象。如果調(diào)用getpgid的時(shí)候給出了非0的process ID number,那么getpgid實(shí)際上是想要獲取指定pid number的gpid。這時(shí)候,我們需要調(diào)用find_task_by_vpid找到該pid number對應(yīng)的task struct。一旦找到task struct結(jié)構(gòu),那么很容易得到其使用的pgid(該實(shí)體是struct pid類型)。至此,無論哪一種參數(shù)情況(傳入的參數(shù)pid number等于0或者非0),我們都找到了該pid number對應(yīng)的struct pid數(shù)據(jù)對象(pgid)。當(dāng)然,最終用戶空間需要的是pgid number,因此我們需要調(diào)用pid_vnr找到該pid在當(dāng)前namespace中的pgid number。
getsid的代碼邏輯和getpid是類似的,不再贅述。
評論
查看更多