進程描述符(Process Descriptor),顧名思義,就是進程的描述,即用來描述進程的資料結構,可以理解為進程的屬性。比如進程的狀態、進程的標識(PID)等,都被封裝在了進程描述符這個資料結構中,該資料結構被定義為task_struct。
進程狀態
Linux中的進程有7種狀態,進程的task_struct結構的state欄位指明了該進程的狀態。
可運行狀態(TASK_RUNNING)
可中斷的等待(TASK_INTERRUPTIBLE)
不可中斷的等待(TASK_UNINTERRUPTIBLE)
暫停狀態(TASK_STOPPED)
跟蹤狀態(TASK_TRACED):進程被調試器暫停或監視。
僵死狀態(EXIT_ZOMBIE):進程被終止,但父進程未調用wait類系統調用。
僵死撤銷狀態(TASK_DEAD):父進程發起wait類系統調用,進程由系統刪除。
標識一個進程
標識進程的兩種方法:進程描述符地址、PID。PID的值儲存在task_struct結構的pid欄位中。
能夠被獨立調度的執行內容都有自己的進程描述符,因此,輕量級進程(LWP)也有自己的task_struct結構。
Linux把不同的PID分配給每個進程和LWP(類似地,Windows中也是將PID和TID分配給每個進程和線程,且PID和TID不會相同,注,這裡Linux中的LWP類似於Windows中的線程)。
Linux中還有線程組的概念,一個線程組的所有線程使用該線程組領頭線程的PID,即該組中第一個LWP的PID。這個線程組的PID儲存在task_struct結構的tpid欄位中,線程組領頭線程的tpid和pid的值相同。
得到進程描述符地址
Linux中,有2個資料結構被緊湊地放在了一起:進程的核心堆棧,thread_info(線程描述符)。一般地,這兩個資料結構大小為8192個位元組,放在兩個連續的頁面中,首地址為213的倍數。8KB對於核心堆棧和thread_info來說已經足夠了(也可以在編譯核心時設定,讓這兩個資料結構佔用一個頁面)。這個8KB的起始存放thread_info結構,核心堆棧從末端向下增長。在thread_info結構中,有一個指向進程描述符的指標task,利用該指標可以找到task_struct結構地址。在task_struct結構中,也有一個thread_info指標,指向thread_info結構。
因為thread_info和核心堆棧被緊湊地存放在一起,因此,可以從核心堆棧找到thread_info結構地址,繼而通過thread_info結構的task指標找到task_struct結構指標。對於8KB而言,得到esp中的值,然後將該值與上0xffffe000,即將低13位清零,就得到了thread_info的地址,然後就可以得到task_struct的地址。
進程鏈表
Linux中將多個進程組織成迴圈雙鏈表的結構,進程鏈表頭是init_task描述符,即0進程或swapper進程的描述符。通過task_struct結構中tasks欄位,將多個進程串連成鏈表的結構。
早期的Linux版本中,把所有TASK_RUNNING狀態的進程放在一個運行隊列中,這樣,按照優先順序排序該鏈表的開銷比較大,早期的發送器不得不遍曆整個鏈表來選擇最佳的進程。
Linux 2.6中的運行隊列不同,系統中建立了多個可運行進程鏈表,即運行隊列中包含多個可運行進程鏈表。每個可運行進程鏈表對應一個優先順序,優先順序取值為0~139。假定某個進程優先順序為k,那麼該進程的task_struct結構中run_list欄位就將其串連到優先順序為k的可運行進程鏈表中。另外,在多處理器系統中,每個CPU都有它自己的運行隊列。這麼多可運行進程鏈表由prio_array_t資料結構來管理。
進程間關係
進程之間有父子關係,如果一個進程建立多個子進程,那這些子進程之間就有了兄弟關係。Linux中,進程0和進程1由核心建立,進程1(init)是其他所有進程的祖先。
在進程描述符表task_struct結構中,以下欄位表示進程間的關係:
real_parent:指向建立進程P的進程的描述符,如果P的父進程不存在,就指向進程1的描述符。
parent:指向P的當前父進程,往往與real_parent一致。當出現Q進程向P發出跟蹤調試ptrace()系統調用時,該欄位指向Q進程描述符。
children:一個鏈表頭,鏈表中所有元素都是進程P建立的子進程。
sibling:指向兄弟進程鏈表的下一個元素或前一個元素的指標。
另外,進程間還存在其他關係:登入工作階段關係、進程組關係、線程組關係、跟蹤調試關係。
在task_struct結構中,以下欄位表示這些關係(假設當前進程為P):
group_leader:P所在進程組的領頭進程的描述符指標
signal->pgrp:P所在進程組的領頭進程的PID
tgid:P所線上程組的領頭進程的PID
signal->session:P所在登入工作階段領頭進程的PID
ptrace_children:一個鏈表頭,鏈表中的所有元素是被調試器程式跟蹤的P的子進程
ptrace_list:當P被調試跟蹤時,指向調試跟蹤進程的父進程鏈表的前一個和下一個元素
PID匯出進程描述符
有些情況需要從PID得到響應的進程描述符指標,比如kill()系統調用。由於順序掃描進程鏈表並檢查進程描述符的pid欄位是比較低效的,因此引入了4個雜湊表:
PIDTYPE_PID
PIDTYPE_TGID
PIDTYPE_PGID
PIDTYPE_SID
這四個雜湊表在核心初始化時動態地分配空間,它們的地址被存入pid_hash數組,其長度依賴於RAM容量。利用pid_hashfn可以將PID轉化為表索引。
為了防止出現雜湊運算帶來的衝突,Linux採用拉鏈法來解決,即引入具有鏈表的雜湊表來處理。
進程組織
運行隊列的鏈表把TASK_RUNNING狀態的所有進程組織在一起。對於其他狀態的進程,Linux做如下處理:
- TASK_STOPPED、EXIT_ZOMBIE、EXIT_DEAD狀態的進程,Linux並沒有為它們建立專門的鏈表,因為訪問簡單。
- TASK_INTERRUPTIBLE、TASK_UNINTERRUPTIBLE狀態的進程被分為很多類,每一類對應一個特定的事件。在這種狀態下,進程狀態無法提供足夠的資訊來快速的得到進程,因此引入額外的進程鏈表是必要的。這些鏈表稱為“等待隊列”。
等待隊列的用途很多,比如中斷處理、進程同步、定時等。
等待隊列由雙鏈表實現,每個等待隊列都有一個隊頭,這是一個wait_queue_head_t的資料結構。該資料結構中有一個spinlock_t類型的lock變數,這是一個自旋鎖,用來保證等待隊列被互斥的訪問和操作。
等待隊列中元素的類型是wait_queue_t,該資料結構中有一個task欄位,是一個進程描述符的指標;有一個func欄位,是一個函數指標,表示進程的如何喚醒(即喚醒時調用該函數);還有一個flags欄位,決定了相關進程是互斥進程(flags = 1)還是非互斥進程(flags = 0)。
這裡解釋下互斥進程與非互斥進程。非互斥進程總是由核心在事件發生時喚醒;互斥進程則是由核心在事件發生時有選擇地喚醒,比如訪問臨界區的進程。
進程資源限制
每個進程都有一組相關的資源限制,指明了進程能夠使用的系統資源數量。避免進程過度使用系統資源(CPU、磁碟空間等)。
進程資源的限制存放在進程描述符的signal->rlim欄位中,該欄位是一個類型為rlimit結構的數組,數組中每個元素對應一種資源。
用getrlimit()和setrlimit()系統調用,使用者能夠增加當前資源限制的上限。
如果資源限制值為RLIMIT_INFINITY(0xffffffff),就意味著沒有對應的資源限制。
總結
進程描述符(task_struct)某些欄位含義,假設進程為P。
- state:P進程狀態,用set_task_state和set_current_state宏更改之,或直接賦值。
- thread_info:指向thread_info結構的指標。
- run_list:假設P狀態為TASK_RUNNING,優先順序為k,run_list將P串連到優先順序為k的可運行進程鏈表中。
- tasks:將P串連到進程鏈表中。
- ptrace_children:鏈表頭,鏈表中的所有元素是被調試器程式跟蹤的P的子進程。
- ptrace_list:P被調試時,鏈表中的所有元素是被調試器程式跟蹤的P的子進程。
- pid:P進程標識(PID)。
- tgid:P所在的線程組的領頭進程的PID。
- real_parent:P的真實的父進程的進程描述符指標。
- parent:P的父進程的進程描述符指標,當被調試時就是調試器進程的描述符指標。
- children:P的子進程鏈表。
- sibling:將P串連到P的兄弟進程鏈表。
- group_leader:P所在的線程組的領頭進程的描述符指標。