歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux基礎 >> 關於Linux >> Linux進程ID號--Linux進程的管理與調度(三)

Linux進程ID號--Linux進程的管理與調度(三)

日期:2017/3/1 11:52:18   编辑:關於Linux

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

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

進程ID概述

進程ID類型

要想了解內核如何來組織和管理進程ID,先要知道進程ID的類型:

內核中進程ID的類型用pid_type來描述。

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的pid和tgid成員:

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

局部ID 對於屬於某個特定的命名空間,它在其命名空間內分配的ID為局部ID,該ID也可以出現在其他的命名空間中。

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的個數

pidmappidmap結構體表示分配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 pid是內核對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不一致,這也是內核區分他們的標識

對於用戶態程序來說,調用getpid()函數其實返回的是tgid,因此線程組中的進程id應該是是一致的,但是他們pid不一致,這也是內核區分他們的標識

多個task_struct可以共用一個PID

一個PID可以屬於不同的命名空間

當需要分配一個新的pid時候,只需要查找pidmap位圖即可

那麼最終,linux下進程命名空間和進程的關系結構如下:

進程命名空間和進程的關系結構

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

內核是如何設計task_struct中進程ID相關數據結構的

本部內容較多的采用了Linux 內核進程管理之進程ID

Linux 內核在設計管理ID的數據結構時,要充分考慮以下因素:

如何快速地根據進程的 task_struct、ID類型、命名空間找到局部ID

如何快速地根據局部ID、命名空間、ID類型找到對應進程的 task_struct

如何快速地給新進程在可見的命名空間內分配一個唯一的 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結構體:

首先通過 PID 計算 pid 掛接到哈希表 pid_hash[] 的表項

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

再通過該 pid 結構體的 tasks 指針找到 node

最後根據內核的 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管理函數

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

獲得局部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);

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

查找進程task_struct

根據局部ID、以及命名空間,怎樣獲得進程的task_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) //pid_hashfn() 獲得hash的索引

if (pnr->nr == nr && pnr->ns == ns) //比較 nr 與 ns 是否都相同

return container_of(pnr, struct pid, //根據container_of機制取得pid 實體

numbers[ns->level]);

return NULL;

}

根據ID類型取得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;

}

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

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

具體函數實現的功能也比較簡單。

生成唯一的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;

}

Copyright © Linux教程網 All Rights Reserved