要想了解核心如何來組織和管理程序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命名空間
在上圖有四個命名空間,一個父命名空間衍生了兩個子命名空間,其中的一個子命名空間又衍生了一個子命名空間。以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
指向父命名空間的指針
實際上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下程序命名空間和程序的關系結構如下:
可以看到,多個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号。
結構示意圖如圖
圖中還有兩個結構上面未提及:
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,這樣的結構示意圖如圖
關于上圖有幾點需要說明:
圖中省去了 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所示為其表示:
圖中關于如果配置設定唯一的 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 執行個體。
代碼如下:
而另外兩個函數則是對其進行進一步的封裝,如下
三者之間的調用關系如下
由圖可以看出,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