1. 程式人生 > >Linux程序的管理與排程(三) -- Linux程序ID號

Linux程序的管理與排程(三) -- Linux程序ID號

Linux 核心使用 task_struct 資料結構來關聯所有與程序有關的資料和結構,Linux 核心所有涉及到程序和程式的所有演算法都是圍繞該資料結構建立的,是核心中最重要的資料結構之一。

該資料結構在核心檔案include/linux/sched.h中定義,在目前最新的Linux-4.5(截至目前的日期為2016-05-11)的核心中,該資料結構足足有 380 行之多,在這裡我不可能逐項去描述其表示的含義,本篇文章只關注該資料結構如何來組織和管理程序ID的。

程序ID概述

程序ID型別

要想了解核心如何來組織和管理程序ID,先要知道程序ID的型別:

enum pid_type
{
    PIDTYPE_PID,
    PIDTYPE_PGID,
    PIDTYPE_SID,
    PIDTYPE_MAX
};
  • PID 核心唯一區分每個程序的標識


pid是 Linux 中在其名稱空間中唯一標識程序而分配給它的一個號碼,稱做程序ID號,簡稱PID。在使用 fork 或 clone 系統呼叫時產生的程序均會由核心分配一個新的唯一的PID值

這個pid用於核心唯一的區分每個程序

注意它並不是我們使用者空間通過getpid( )所獲取到的那個程序號,至於原因麼,接著往下看

  • TGID 執行緒組(輕量級程序組)的ID標識

在一個程序中,如果以CLONE_THREAD標誌來呼叫clone建立的程序就是該程序的一個執行緒(即輕量級程序,Linux其實沒有嚴格的程序概念),它們處於一個執行緒組,


該執行緒組的所有執行緒的ID叫做TGID。處於相同的執行緒組中的所有程序都有相同的TGID,但是由於他們是不同的程序,因此其pid各不相同;執行緒組組長(也叫主執行緒)的TGID與其PID相同;一個程序沒有使用執行緒,則其TGID與PID也相同。

  • PGID

另外,獨立的程序可以組成程序組(使用setpgrp系統呼叫),程序組可以簡化向所有組內程序傳送訊號的操作

例如用管道連線的程序處在同一程序組內。程序組ID叫做PGID,程序組內的所有程序都有相同的PGID,等於該組組長的PID。

  • SID

幾個程序組可以合併成一個會話組(使用setsid系統呼叫),可以用於終端程式設計。會話組中所有程序都有相同的SID,儲存在task_struct的session成員中

PID名稱空間

pid名稱空間概述

名稱空間是為作業系統層面的虛擬化機制提供支撐,目前實現的有六種不同的名稱空間,分別為mount名稱空間、UTS名稱空間、IPC名稱空間、使用者名稱空間、PID名稱空間、網路名稱空間。名稱空間簡單來說提供的是對全域性資源的一種抽象,將資源放到不同的容器中(不同的名稱空間),各容器彼此隔離。

關於名稱空間的詳細資訊,請參見

名稱空間有的還有層次關係,如PID名稱空間

名稱空間的層次關係圖

在上圖有四個名稱空間,一個父名稱空間衍生了兩個子名稱空間,其中的一個子名稱空間又衍生了一個子名稱空間。以PID名稱空間為例,由於各個名稱空間彼此隔離,所以每個名稱空間都可以有 PID 號為 1 的程序;但又由於名稱空間的層次性,父名稱空間是知道子名稱空間的存在,因此子名稱空間要對映到父名稱空間中去,因此上圖中 level 1 中兩個子名稱空間的六個程序分別對映到其父名稱空間的PID 號5~10。

區域性ID和全域性ID

名稱空間增加了PID管理的複雜性。

回想一下,PID名稱空間按層次組織。在建立一個新的名稱空間時,該名稱空間中的所有PID對父名稱空間都是可見的,但子名稱空間無法看到父名稱空間的PID。但這意味著某些程序具有多個PID,凡可以看到該程序的名稱空間,都會為其分配一個PID。 這必須反映在資料結構中。我們必須區分區域性ID全域性ID

全域性PID和TGID直接儲存在task_struct中,分別是task_struct的pidtgid成員:

  • 全域性ID 在核心本身和初始名稱空間中唯一的ID,在系統啟動期間開始的 init 程序即屬於該初始名稱空間。系統中每個程序都對應了該名稱空間的一個PID,叫全域性ID,保證在整個系統中唯一。

  • 區域性ID 對於屬於某個特定的名稱空間,它在其名稱空間內分配的ID為區域性ID,該ID也可以出現在其他的名稱空間中。

<sched.h> 
struct task_struct
{  
        //...  
        pid_t pid;  
        pid_t tgid;  
        //...  
}

兩項都是pid_t型別,該型別定義為__kernel_pid_t,後者由各個體系結構分別定義。通常定義為int,即可以同時使用232個不同的ID。

會話session和程序group組ID不是直接包含在task_struct本身中,但儲存在用於訊號處理的結構中。

task_ struct->signal->__session表示全域性SID,

而全域性PGID則儲存在task_struct->signal->__pgrp。

輔助函式set_task_session和set_task_pgrp可用於修改這些值。

除了這兩個欄位之外,核心還需要找一個辦法來管理所有名稱空間內部的區域性量,以及其他ID(如TID和SID)。這需要幾個相互連線的資料結構,以及許多輔助函式,並將在下文討論。

下文我將使用ID指代提到的任何程序ID。在必要的情況下,我會明確地說明ID型別(例如,TGID,即執行緒組ID)。

一個小型的子系統稱之為PID分配器(pid allocator)用於加速新ID的分配。此外,核心需要提供輔助函式,以實現通過ID及其型別查詢程序的task_struct的功能,以及將ID的核心表示形式和使用者空間可見的數值進行轉換的功能。

PID名稱空間資料結構pid_namespace

在介紹表示ID本身所需的資料結構之前,我需要討論PID名稱空間的表示方式。我們所需檢視的程式碼如下所示:

名稱空間的結構如下

struct pid_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;
}; 

我們這裡只關心其中的child_reaper,level和parent這三個欄位

欄位 描述
kref 表示指向pid_namespace的個數
pidmap pidmap結構體表示分配pid的點陣圖。當需要分配一個新的pid時只需查詢點陣圖,找到bit為0的位置並置1,然後更新統計資料域(nr_free)
last_pid 用於pidmap的分配。指向最後一個分配的pid的位置。(不是特別確定)
child_reaper 指向的是當前名稱空間的init程序,每個名稱空間都有一個作用相當於全域性init程序的程序
pid_cachep 域指向分配pid的slab的地址。
level 代表當前名稱空間的等級,初始名稱空間的level為0,它的子名稱空間level為1,依次遞增,而且子名稱空間對父名稱空間是可見的。從給定的level設定,核心即可推斷程序會關聯到多少個ID。
parent 指向父名稱空間的指標

PID名稱空間

實際上PID分配器也需要依靠該結構的某些部分來連續生成唯一ID,但我們目前對此無需關注。我們上述程式碼中給出的下列成員更感興趣。

每個PID名稱空間都具有一個程序,其發揮的作用相當於全域性的init程序。init的一個目的是對孤兒程序呼叫wait4,名稱空間區域性的init變體也必須完成該工作。

pid結構描述

pid與upid

PID的管理圍繞兩個資料結構展開:

  • struct upid則表示特定的名稱空間中可見的資訊。

struct upid
{  
    /* Try to keep pid_chain in the same cacheline as nr for find_vpid */
        int nr;  
        struct pid_namespace *ns;  
        struct hlist_node pid_chain;  
};  
欄位 描述
nr 表示ID具體的值
ns 指向名稱空間的指標
pid_chain 指向PID雜湊列表的指標,用於關聯對於的PID

所有的upid例項都儲存在一個散列表中,稍後我們會看到該結構。

struct pid  
{  
        atomic_t count;  
        /* 使用該pid的程序的列表, lists of tasks that use this pid  */
        struct hlist_head tasks[PIDTYPE_MAX];  
        int level;  
        struct upid numbers[1];  
};
欄位 描述
count 是指使用該PID的task的數目;
level 表示可以看到該PID的名稱空間的數目,也就是包含該程序的名稱空間的深度
tasks[PIDTYPE_MAX] 是一個數組,每個陣列項都是一個散列表頭,分別對應以下三種類型
numbers[1] 一個upid的例項陣列,每個陣列項代表一個名稱空間,用來表示一個PID可以屬於不同的名稱空間,該元素放在末尾,可以向陣列新增附加的項。

tasks是一個數組,每個陣列項都是一個散列表頭,對應於一個ID型別,PIDTYPE_PID, PIDTYPE_PGID, PIDTYPE_SID( PIDTYPE_MAX表示ID型別的數目)這樣做是必要的,因為一個ID可能用於幾個程序。所有共享同一給定ID的task_struct例項,都通過該列表連線起來。

這個列舉常量PIDTYPE_MAX,正好是pid_type型別的數目,這裡linux核心使用了一個小技巧來由編譯器來自動生成id型別的數目

此外,還有兩個結構我們需要說明,就是pidmap和pid_link

  • pidmap當需要分配一個新的pid時查詢可使用pid的點陣圖,其定義如下

  • 而pid_link則是pid的雜湊表儲存結構

pidmap用於分配pid的點陣圖

struct pidmap
{  
    atomic_t nr_free;  
    void *page; 
};
欄位 描述
nr_free 表示還能分配的pid的數量
page 指向的是存放pid的物理頁

pidmap[PIDMAP_ENTRIES]域表示該pid_namespace下pid已分配情況

pid_link雜湊表儲存

pids[PIDTYPE_MAX]指向了和該task_struct相關的pid結構體。
pid_link的定義如下

struct pid_link  
{  
struct hlist_node node;  
struct pid *pid;  
};

task_struct中程序ID相關資料結構

task_struct中的描述符資訊

struct task_struct  
{  
    //...  
    pid_t pid;  
    pid_t tgid;  
    struct task_struct *group_leader;  
    struct pid_link pids[PIDTYPE_MAX];  
    struct nsproxy *nsproxy;  
    //...  
};
欄位 描述
pid 指該程序的程序描述符。在fork函式中對其進行賦值的
tgid 指該程序的執行緒描述符。在linux核心中對執行緒並沒有做特殊的處理,還是由task_struct來管理。所以從核心的角度看, 使用者態的執行緒本質上還是一個程序。對於同一個程序(使用者態角度)中不同的執行緒其tgid是相同的,但是pid各不相同。 主執行緒即group_leader(主執行緒會建立其他所有的子執行緒)。如果是單執行緒程序(使用者態角度),它的pid等於tgid。
group_leader 除了在多執行緒的模式下指向主執行緒,還有一個用處, 當一些程序組成一個群組時(PIDTYPE_PGID), 該域指向該群組的leader
nsproxy 指標指向namespace相關的域,通過nsproxy域可以知道該task_struct屬於哪個pid_namespace

對於使用者態程式來說,呼叫getpid()函式其實返回的是tgid,因此執行緒組中的程序id應該是是一致的,但是他們pid不一致,這也是核心區分他們的標識

  1. 多個task_struct可以共用一個PID

  2. 一個PID可以屬於不同的名稱空間

  3. 當需要分配一個新的pid時候,只需要查詢pidmap點陣圖即可

那麼最終,linux下程序名稱空間和程序的關係結構如下:

程序名稱空間和程序的關係結構

可以看到,多個task_struct指向一個PID,同時PID的hash數組裡安裝不同的型別對task進行雜湊,並且一個PID會屬於多個名稱空間。

核心是如何設計task_struct中程序ID相關資料結構的

Linux 核心在設計管理ID的資料結構時,要充分考慮以下因素:

  1. 如何快速地根據程序的 task_struct、ID型別、名稱空間找到區域性ID

  2. 如何快速地根據區域性ID、名稱空間、ID型別找到對應程序的 task_struct

  3. 如何快速地給新程序在可見的名稱空間內分配一個唯一的 PID

如果將所有因素考慮到一起,將會很複雜,下面將會由簡到繁設計該結構。

一個PID對應一個task時的task_struct設計

一個PID對應一個task_struct如果先不考慮程序之間的關係,不考慮名稱空間,僅僅是一個PID號對應一個task_struct,那麼我們可以設計這樣的資料結構

struct task_struct
{
    //...
    struct pid_link pids;   
    //...
};

struct pid_link
{
    struct hlist_node node;
    struct pid *pid;
};

struct pid
{
    struct hlist_head tasks; //指回 pid_link 的 node
    int nr; //PID
    struct hlist_node pid_chain; //pid hash 散列表結點
};

每個程序的 task_struct 結構體中有一個指向 pid 結構體的指標,pid結構體包含了PID號。

結構示意圖如圖

一個task_struct對應一個PID

如何快速地根據區域性ID、名稱空間、ID型別找到對應程序的 task_struct

圖中還有兩個結構上面未提及:

  • pid_hash[]

這是一個hash表的結構,根據pid的nr值雜湊到其某個表項,若有多個 pid 結構對應到同一個表項,這裡解決衝突使用的是散列表法。

這樣,就能解決開始提出的第2個問題了,根據PID值怎樣快速地找到task_struct結構體:

  1. 首先通過 PID 計算 pid 掛接到雜湊表 pid_hash[] 的表項

  2. 遍歷該表項,找到 pid 結構體中 nr 值與 PID 值相同的那個 pid

  3. 再通過該 pid 結構體的 tasks 指標找到 node

  4. 最後根據核心的 container_of 機制就能找到 task_struct 結構體

如何快速地給新程序在可見的名稱空間內分配一個唯一的 PID

  • pid_map

這是一個位圖,用來唯一分配PID值的結構,圖中灰色表示已經分配過的值,在新建一個程序時,只需在其中找到一個為分配過的值賦給 pid 結構體的 nr,再將pid_map 中該值設為已分配標誌。這也就解決了上面的第3個問題——如何快速地分配一個全域性的PID

如何快速地根據程序的 task_struct、ID型別、名稱空間找到區域性ID

至於上面的*第1個問題就更加簡單,已知 task_struct 結構體,根據其 pid_link 的 pid 指標找到 pid 結構體,取出其 nr 即為 PID 號。

帶程序ID型別的task_struct設計

如果考慮程序之間有複雜的關係,如執行緒組、程序組、會話組,這些組均有組ID,分別為 TGID、PGID、SID,所以原來的 task_struct 中pid_link 指向一個 pid 結構體需要增加幾項,用來指向到其組長的 pid 結構體,相應的 struct pid 原本只需要指回其 PID 所屬程序的task_struct,現在要增加幾項,用來連結那些以該 pid 為組長的所有程序組內程序。資料結構如下:

enum pid_type
{
    PIDTYPE_PID,
    PIDTYPE_PGID,
    PIDTYPE_SID,
    PIDTYPE_MAX
};

struct task_struct
{
    //...
    pid_t pid; //PID
    pid_t tgid; //thread group id
    //..
    struct pid_link pids[PIDTYPE_MAX];    
    struct task_struct *group_leader; // threadgroup leader
    //...
    struct pid_link pids[PIDTYPE_MAX];  
    struct nsproxy *nsproxy;  
};

struct pid_link
{
    struct hlist_node node;
    struct pid *pid;
};

struct pid
{
    struct hlist_head tasks[PIDTYPE_MAX];
    int nr; //PID
    struct hlist_node pid_chain; // pid hash 散列表結點
};

上面 ID 的型別 PIDTYPE_MAX 表示 ID 型別數目。之所以不包括執行緒組ID,是因為核心中已經有指向到執行緒組的 task_struct 指標 group_leader,執行緒組 ID 無非就是 group_leader 的PID。

假如現在有三個程序A、B、C為同一個程序組,程序組長為A,這樣的結構示意圖如圖

增加ID型別的結構

關於上圖有幾點需要說明:

圖中省去了 pid_hash 以及 pid_map 結構,因為第一種情況類似;

程序B和C的程序組組長為A,那麼 pids[PIDTYPE_PGID] 的 pid 指標指向程序A的 pid 結構體;

程序A是程序B和C的組長,程序A的 pid 結構體的 tasks[PIDTYPE_PGID] 是一個散列表的頭,它將所有以該pid 為組長的程序連結起來。

再次回顧本節的三個基本問題,在此結構上也很好去實現。

進一步增加程序PID名稱空間的task_struct設計

若在第二種情形下再增加PID名稱空間

一個程序就可能有多個PID值了,因為在每一個可見的名稱空間內都會分配一個PID,這樣就需要改變 pid 的結構了,如下:

struct pid
{
    unsigned int level;
    /* lists of tasks that use this pid */
    struct hlist_head tasks[PIDTYPE_MAX];
    struct upid numbers[1];
};

struct upid
{
    int nr;
    struct pid_namespace *ns;
    struct hlist_node pid_chain;
};

在 pid 結構體中增加了一個表示該程序所處的名稱空間的層次level,以及一個可擴充套件的 upid 結構體。對於struct upid,表示在該名稱空間所分配的程序的ID,ns指向是該ID所屬的名稱空間,pid_chain 表示在該名稱空間的散列表。

舉例來說,在level 2 的某個名稱空間上新建了一個程序,分配給它的 pid 為45,對映到 level 1 的名稱空間,分配給它的 pid 為 134;再對映到 level 0 的名稱空間,分配給它的 pid 為289,對於這樣的例子,如圖4所示為其表示:

增加PID名稱空間之後的結構圖

圖中關於如果分配唯一的 PID 沒有畫出,但也是比較簡單,與前面兩種情形不同的是,這裡分配唯一的 PID 是有名稱空間的容器的,在PID名稱空間內必須唯一,但各個名稱空間之間不需要唯一。
至此,已經與 Linux 核心中資料結構相差不多了。

程序ID管理函式

有了上面的複雜的資料結構,再加上散列表等資料結構的操作,就可以寫出我們前面所提到的三個問題的函數了:

pid號到struct pid實體

很多時候在寫核心模組的時候,需要通過程序的pid找到對應程序的task_struct,其中首先就需要通過程序的pid找到程序的struct pid,
然後再通過struct pid找到程序的task_struct

我知道的實現函式有三個。

struct pid *find_pid_ns(int nr, struct pid_namespace *ns)
struct pid *find_vpid(int nr)
struct pid *find_get_pid(pid_t nr)

find_pid_ns獲得 pid 實體的實現原理,主要使用雜湊查詢。核心使用雜湊表組織struct pid,每建立一個新程序,給程序的struct pid都會插入到雜湊表中,這時候就需要使用程序
的程序pid和命名ns在雜湊表中將相對應的struct pid索引出來,現在可以看下find_pid_ns的傳入引數,也是通過nr和ns找到struct pid。

根據區域性PID以及名稱空間計算在 pid_hash 陣列中的索引,然後遍歷散列表找到所要的 upid, 再根據核心的 container_of 機制找到 pid 例項。

程式碼如下:

struct pid *find_pid_ns(int nr, struct pid_namespace *ns)
{
        struct hlist_node *elem;
        struct upid *pnr; 

        hlist_for_each_entry_rcu(pnr, elem,
                        &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;
}

而另外兩個函式則是對其進行進一步的封裝,如下

struct pid *find_vpid(int nr)
{
        return find_pid_ns(nr, current->nsproxy->pid_ns);
}
struct pid *find_get_pid(pid_t nr)
{ 
        struct pid *pid; 

        rcu_read_lock();
        pid = get_pid(find_vpid(nr)); 
        rcu_read_unlock();

        return pid; 
}

三者之間的呼叫關係如下

這裡寫圖片描述

由圖可以看出,find_pid_ns是最終的實現,find_vpid是使用find_pid_ns
實現的,find_get_pid又是由find_vpid實現的。

由原始碼可以看出find_vpid和find_pid_ns是一樣的,而find_get_pid和find_vpid有一點差異,就是使用find_get_pid將返回的struct pid中的欄位count加1,而find_vpid沒有加1。

獲得區域性ID

根據程序的 task_struct、ID型別、名稱空間,可以很容易獲得其在名稱空間內的區域性ID

獲得與task_struct 關聯的pid結構體。輔助函式有 task_pid、task_tgid、task_pgrp和task_session,分別用來獲取不同型別的ID的pid 例項,如獲取 PID 的例項:

static inline struct pid *task_pid(struct task_struct *task)
{
    return task->pids[PIDTYPE_PID].pid;
}

獲取執行緒組的ID,前面也說過,TGID不過是執行緒組組長的PID而已,所以:

static inline struct pid *task_tgid(struct task_struct *task)
{
    return task->group_leader->pids[PIDTYPE_PID].pid;
}

而獲得PGID和SID,首先需要找到該執行緒組組長的task_struct,再獲得其相應的 pid:

static inline struct pid *task_pgrp(struct task_struct *task)
{
    return task->group_leader->pids[PIDTYPE_PGID].pid;
}

static inline struct pid *task_session(struct task_struct *task)
{
    return task->group_leader->pids[PIDTYPE_SID].pid;
}

獲得 pid 例項之後,再根據 pid 中的numbers 陣列中 uid 資訊,獲得區域性PID。

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;
}

這裡值得注意的是,由於PID名稱空間的層次性,父名稱空間能看到子名稱空間的內容,反之則不能,因此,函式中需要確保當前名稱空間的level 小於等於產生區域性PID的名稱空間的level。

除了這個函式之外,核心還封裝了其他函式用來從 pid 例項獲得 PID 值,如 pid_nr、pid_vnr 等。在此不介紹了。
結合這兩步,核心提供了更進一步的封裝,提供以下函式:

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);
pid_t task_pigd_nr_ns(struct task_struct *tsk, struct pid_namespace *ns);
pid_t task_session_nr_ns(struct task_struct *tsk, struct pid_namespace *ns);

從函式名上就能推斷函式的功能,其實不外於封裝了上面的兩步。

根據PID查詢程序task_struct

  • 根據PID號(nr值)取得task_struct 結構體

  • 根據PID以及其型別(即為區域性ID和名稱空間)獲取task_struct結構體

如果根據的是程序的ID號,我們可以先通過ID號(nr值)獲取到程序struct pid實體(區域性ID),然後根據區域性ID、以及名稱空間,獲得程序的task_struct結構體

可以使用pid_task根據pid和pid_type獲取到程序的task

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;
}

那麼我們根據pid號查詢程序task的過程就成為

pTask = pid_task(find_vpid(pid), PIDTYPE_PID);  

核心還提供其它函式用來實現上面兩步:

struct task_struct *find_task_by_pid_ns(pid_t nr, struct pid_namespace *ns);
struct task_struct *find_task_by_vpid(pid_t vnr);
struct task_struct *find_task_by_pid(pid_t vnr);

由於linux程序是組織在雙向連結串列和紅黑樹中的,因此我們通過遍歷連結串列或者樹也可以找到當前程序,但是這個並不是我們今天的重點

生成唯一的PID

核心中使用下面兩個函式來實現分配和回收PID的:

static int alloc_pidmap(struct pid_namespace *pid_ns);
static void free_pidmap(struct upid *upid);

在這裡我們不關注這兩個函式的實現,反而應該關注分配的 PID 如何在多個名稱空間中可見,這樣需要在每個名稱空間生成一個區域性ID,函式 alloc_pid 為新建的程序分配PID,簡化版如下:

struct pid *alloc_pid(struct pid_namespace *ns)
{
    struct pid *pid;
    enum pid_type type;
    int i, nr;
    struct pid_namespace *tmp;
    struct upid *upid;
    tmp = ns;
    pid->level = ns->level;
    // 初始化 pid->numbers[] 結構體
    for (i = ns->level; i >= 0; i--)
    {
        nr = alloc_pidmap(tmp); //分配一個區域性ID
        pid->numbers[i].nr = nr;
        pid->numbers[i].ns = tmp;
        tmp = tmp->parent;
    }
    // 初始化 pid->task[] 結構體
    for (type = 0; type < PIDTYPE_MAX; ++type)
        INIT_HLIST_HEAD(&pid->tasks[type]);

    // 將每個名稱空間經過雜湊之後加入到散列表中
    upid = pid->numbers + ns->level;
    for ( ; upid >= pid->numbers; --upid)
    {
        hlist_add_head_rcu(&upid->pid_chain, &pid_hash[pid_hashfn(upid->nr, upid->ns)]);
        upid->ns->nr_hashed++;
    }

    return pid;