天天看點

Linux程序ID号--Linux程序的管理與排程(三) 程序ID概述 pid結構描述 核心是如何設計task_struct中程序ID相關資料結構的 程序ID管理函數

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

1

2

3

4

5

6

7

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成員中

命名空間是為作業系統層面的虛拟化機制提供支撐,目前實作的有六種不同的命名空間,分别為mount命名空間、uts命名空間、ipc命名空間、使用者命名空間、pid命名空間、網絡命名空間。命名空間簡單來說提供的是對全局資源的一種抽象,将資源放到不同的容器中(不同的命名空間),各容器彼此隔離。

關于命名空間的詳細資訊,請參見

命名空間有的還有層次關系,如pid命名空間

Linux程式ID号--Linux程式的管理與排程(三) 程式ID概述 pid結構描述 核心是如何設計task_struct中程式ID相關資料結構的 程式ID管理函數
在上圖有四個命名空間,一個父命名空間衍生了兩個子命名空間,其中的一個子命名空間又衍生了一個子命名空間。以pid命名空間為例,由于各個命名空間彼此隔離,是以每個命名空間都可以有 pid 号為 1 的程序;但又由于命名空間的層次性,父命名空間是知道子命名空間的存在,是以子命名空間要映射到父命名空間中去,是以上圖中 level 1 中兩個子命名空間的六個程序分别映射到其父命名空間的pid 号5~10。

命名空間增加了pid管理的複雜性。

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

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

局部id 對于屬于某個特定的命名空間,它在其命名空間内配置設定的id為局部id,該id也可以出現在其他的命名空間中。

8

兩項都是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的核心表示形式和使用者空間可見的數值進行轉換的功能。

在介紹表示id本身所需的資料結構之前,我需要讨論pid命名空間的表示方式。我們所需檢視的代碼如下所示:

命名空間的結構如下

9

10

11

我們這裡隻關心其中的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

指向父命名空間的指針

Linux程式ID号--Linux程式的管理與排程(三) 程式ID概述 pid結構描述 核心是如何設計task_struct中程式ID相關資料結構的 程式ID管理函數

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

每個pid命名空間都具有一個程序,其發揮的作用相當于全局的init程序。init的一個目的是對孤兒程序調用wait4,命名空間局部的init變體也必須完成該工作。

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

nr

表示id具體的值

ns

指向命名空間的指針

pid_chain

指向pid哈希清單的指針,用于關聯對于的pid

所有的upid執行個體都儲存在一個散清單中,稍後我們會看到該結構。

count

是指使用該pid的task的數目;

表示可以看到該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的哈希表存儲結構

nr_free

表示還能配置設定的pid的數量

page

指向的是存放pid的實體頁

pidmap[pidmap_entries]域表示該pid_namespace下pid已配置設定情況

pids[pidtype_max]指向了和該task_struct相關的pid結構體。

pid_link的定義如下

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不一緻,這也是核心區分他們的辨別

多個task_struct可以共用一個pid

一個pid可以屬于不同的命名空間

當需要配置設定一個新的pid時候,隻需要查找pidmap位圖即可

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

Linux程式ID号--Linux程式的管理與排程(三) 程式ID概述 pid結構描述 核心是如何設計task_struct中程式ID相關資料結構的 程式ID管理函數

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

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

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

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

如何快速地給新程序在可見的命名空間内配置設定一個唯一的 pid

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

一個pid對應一個<code>task_struct</code>如果先不考慮程序之間的關系,不考慮命名空間,僅僅是一個pid号對應一個<code>task_struct</code>,那麼我們可以設計這樣的資料結構

12

13

14

15

16

17

18

19

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

結構示意圖如圖

Linux程式ID号--Linux程式的管理與排程(三) 程式ID概述 pid結構描述 核心是如何設計task_struct中程式ID相關資料結構的 程式ID管理函數

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

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_map 這是一個位圖,用來唯一配置設定pid值的結構,圖中灰色表示已經配置設定過的值,在建立一個程序時,隻需在其中找到一個為配置設定過的值賦給 pid 結構體的 nr,再将pid_map 中該值設為已配置設定标志。這也就解決了上面的第3個問題——如何快速地配置設定一個全局的pid

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

如果考慮程序之間有複雜的關系,如線程組、程序組、會話組,這些組均有組id,分别為 tgid、pgid、sid,是以原來的 task_struct 中pid_link 指向一個 pid 結構體需要增加幾項,用來指向到其組長的 pid 結構體,相應的 struct pid 原本隻需要指回其 pid 所屬程序的task_struct,現在要增加幾項,用來連結那些以該 pid 為組長的所有程序組内程序。資料結構如下:

20

21

22

23

24

25

26

27

28

29

30

31

32

33

上面 id 的類型 pidtype_max 表示 id 類型數目。之是以不包括線程組id,是因為核心中已經有指向到線程組的 task_struct 指針 group_leader,線程組 id 無非就是 group_leader 的pid。

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

Linux程式ID号--Linux程式的管理與排程(三) 程式ID概述 pid結構描述 核心是如何設計task_struct中程式ID相關資料結構的 程式ID管理函數

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

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

程序b和c的程序組組長為a,那麼 pids[pidtype_pgid] 的 pid 指針指向程序a的 pid 結構體;

程序a是程序b和c的組長,程序a的 pid 結構體的 tasks[pidtype_pgid] 是一個散清單的頭,它将所有以該pid 為組長的程序連結起來。

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

若在第二種情形下再增加pid命名空間

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

在 pid 結構體中增加了一個表示該程序所處的命名空間的層次level,以及一個可擴充的 upid 結構體。對于struct upid,表示在該命名空間所配置設定的程序的id,ns指向是該id所屬的命名空間,pid_chain 表示在該命名空間的散清單。

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

Linux程式ID号--Linux程式的管理與排程(三) 程式ID概述 pid結構描述 核心是如何設計task_struct中程式ID相關資料結構的 程式ID管理函數

圖中關于如果配置設定唯一的 pid 沒有畫出,但也是比較簡單,與前面兩種情形不同的是,這裡配置設定唯一的 pid 是有命名空間的容器的,在pid命名空間内必須唯一,但各個命名空間之間不需要唯一。

至此,已經與 linux 核心中資料結構相差不多了。

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

很多時候在寫核心子產品的時候,需要通過程序的pid找到對應程序的task_struct,其中首先就需要通過程序的pid找到程序的struct pid,

然後再通過struct pid找到程序的task_struct

我知道的實作函數有三個。

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 執行個體。

代碼如下:

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

三者之間的調用關系如下

Linux程式ID号--Linux程式的管理與排程(三) 程式ID概述 pid結構描述 核心是如何設計task_struct中程式ID相關資料結構的 程式ID管理函數

由圖可以看出,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。

根據程序的 task_struct、id類型、命名空間,可以很容易獲得其在命名空間内的局部id

獲得與task_struct 關聯的pid結構體。輔助函數有 task_pid、task_tgid、task_pgrp和task_session,分别用來擷取不同類型的id的pid 執行個體,如擷取 pid 的執行個體:

擷取線程組的id,前面也說過,tgid不過是線程組組長的pid而已,是以:

而獲得pgid和sid,首先需要找到該線程組組長的task_struct,再獲得其相應的 pid:

獲得 pid 執行個體之後,再根據 pid 中的numbers 數組中 uid 資訊,獲得局部pid。

這裡值得注意的是,由于pid命名空間的層次性,父命名空間能看到子命名空間的内容,反之則不能,是以,函數中需要確定目前命名空間的level 小于等于産生局部pid的命名空間的level。

除了這個函數之外,核心還封裝了其他函數用來從 pid 執行個體獲得 pid 值,如 pid_nr、pid_vnr 等。在此不介紹了。

結合這兩步,核心提供了更進一步的封裝,提供以下函數:

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

根據pid号(nr值)取得task_struct 結構體

根據pid以及其類型(即為局部id和命名空間)擷取task_struct結構體

如果根據的是程序的id号,我們可以先通過id号(nr值)擷取到程序struct pid實體(局部id),然後根據局部id、以及命名空間,獲得程序的task_struct結構體

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

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

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

由于linux程序是組織在雙向連結清單和紅黑樹中的,是以我們通過周遊連結清單或者樹也可以找到目前程序,但是這個并不是我們今天的重點

核心中使用下面兩個函數來實作配置設定和回收pid的:

在這裡我們不關注這兩個函數的實作,反而應該關注配置設定的 pid 如何在多個命名空間中可見,這樣需要在每個命名空間生成一個局部id,函數 alloc_pid 為建立的程序配置設定pid,簡化版如下:

轉載:http://blog.csdn.net/gatieme/article/details/51383377

繼續閱讀