程式控制塊PCB結構 task_struct 描述

s1mba發表於2013-09-16

注:本分類下文章大多整理自《深入分析linux核心原始碼》一書,另有參考其他一些資料如《linux核心完全剖析》、《linux c 程式設計一站式學習》等,只是為了更好地理清系統程式設計和網路程式設計中的一些概念性問題,並沒有深入地閱讀分析原始碼,我也是草草翻過這本書,請有興趣的朋友自己參考相關資料。此書出版較早,分析的版本為2.4.16,故出現的一些概念可能跟最新版本核心不同。

此書已經開源,閱讀地址 http://www.kerneltravel.net


一、task_struct 結構描述

1.程式狀態(State)

程式執行時,它會根據具體情況改變狀態。程式狀態是排程和對換的依據。Linux 中的程式主要有如下狀態,如表4.1 所示。

(1)可執行狀態
處於這種狀態的程式,要麼正在執行、要麼正準備執行。正在執行的程式就是當前程式(由current 巨集 所指向的程式),而準備執行的程式只要得到CPU 就可以立即投入執行,CPU 是這些程式唯一等待的系統資源。系統中有一個執行佇列(run_queue),用來容納所有處於可執行狀態的程式,排程程式執行時,從中選擇一個程式投入執行。當前執行程式一直處於該佇列中,也就是說,current總是指向執行佇列中的某個元素,只是具體指向誰由排程程式決定。

(2)等待狀態
處於該狀態的程式正在等待某個事件(Event)或某個資源,它肯定位於系統中的某個等待佇列(wait_queue)中。Linux 中處於等待狀態的程式分為兩種:可中斷的等待狀態和不可中斷的等待狀態。處於可中斷等待態的程式可以被訊號喚醒,如果收到訊號,該程式就從等待狀態進入可執行狀態,並且加入到執行佇列中,等待被排程;而處於不可中斷等待態的程式是因為硬體環境不能滿足而等待,例如等待特定的系統資源,它任何情況下都不能被打斷,只能用特定的方式來喚醒它,例如喚醒函式wake_up()等。

(3)暫停狀態
此時的程式暫時停止執行來接受某種特殊處理。通常當程式接收到SIGSTOP、SIGTSTP、SIGTTIN 或 SIGTTOU 訊號後就處於這種狀態。例如,正接受除錯的程式就處於這種狀態。

(4)僵死狀態
程式雖然已經終止,但由於某種原因,父程式還沒有執行wait()系統呼叫,終止程式的資訊也還沒有回收。顧名思義,處於該狀態的程式就是死程式,這種程式實際上是系統中的垃圾,必須進行相應處理以釋放其佔用的資源。

A child that terminates, but has not been waited for becomes a "zombie".  The kernel maintains a 

minimal set of information  about the  zombie  process (PID, termination status, resource usage 

information) in order to allow the parent to later perform a wait to obtain information about the 

child.  As long as a zombie is not removed from the system via a wait, it will consume a slot in  

the kernel  process  table,  and if this table fills, it will not be possible to create further 

processes.  If a parent process terminates, then its "zombie" children (if any) are adopted by 

init(8), which automatically performs a wait to remove the zombies.


2.程式排程資訊

排程程式利用這部分資訊決定系統中哪個程式最應該執行,並結合程式的狀態資訊保證系統運轉的公平和高效。這一部分資訊通常包括程式的類別(普通程式還是實時程式)、進程的優先順序等,如表4.2 所示。

當need_resched 被設定時,在“下一次的排程機會”就呼叫排程程式schedule();counter 代表程式剩餘的時間片,是程式排程的主要依據,也可以說是程式的動態優先順序,因為這個值在不斷地減少;nice 是程式的靜態優先順序,同時也代表程式的時間片,用於對counter 賦值,可以用nice()系統呼叫改變這個值;policy是適用於該程式的排程策略,實時程式和普通程式的排程策略是不同的;rt_priority 只對實時程式有意義,它是實時程式排程的依據。

程的排程策略有3 種,如表4.3 所示。


只有root 使用者能通過sched_setscheduler()系統呼叫來改變排程策略。

3.識別符號(Identifiers)

每個程式有程式識別符號、使用者識別符號、組識別符號,如表4.4 所示。不管對核心還是普通使用者來說,怎麼用一種簡單的方式識別不同的程式呢?這就引入了程式識別符號(PID,process identifier),每個程式都有一個唯一的識別符號,核心通過這個識別符號來識別不同的程式,同時,程式識別符號PID 也是核心提供給使用者程式的介面,使用者程序通過PID 對程式發號施令。PID 是32 位的無符號整數,它被順序編號:新建立程式的PID通常是前一個程式的PID 加1。然而,為了與16 位硬體平臺的傳統Linux 系統保持相容,在Linux 上允許的最大PID 號是32767,當核心在系統中建立第32768 個程式時,就必須重新開始使用已閒置的PID 號。

4.程式通訊有關資訊(IPC,Inter_Process Communication)

為了使程式能在同一項任務上協調工作,程式之間必須能進行通訊即交流資料。Linux 支援多種不同形式的通訊機制。它支援典型的UNIX 通訊機制(IPC Mechanisms):訊號(Signals)、管道(Pipes),也支援System V / Posix 通訊機制:共享記憶體(Shared Memory)、訊號量和訊息佇列(Message Queues),如表4.5 所示。


5.程式連結資訊(Links)

程式建立的程式具有父/子關係。因為一個程式能建立幾個子程式,而子程式之間有兄弟關係,在task_struct 結構中有幾個域來表示這種關系。在Linux 系統中,除了初始化程式init,其他程式都有一個父程式(Parent Process)。可以通過fork()或clone()系統呼叫來建立子程式,除了程式識別符號(PID)等必要的資訊外,子程式的task_struct 結構中的絕大部分的資訊都是從父程式中拷貝。系統有必要記錄這種“親屬”關係,使程式之間的協作更加方便,例如父程式給子程式傳送殺死(kill)訊號、父子程式通訊等。每個程式的task_struct 結構有許多指標,通過這些指標,系統中所有程式的task_struct結構就構成了一棵程式樹,這棵程式樹的根就是初始化程式init的task_struct結構(init 程式是Linux 核心建立起來後人為建立的一個程式,是所有程式的祖先程式)。
表4.6 是程式所有的連結資訊。


6.時間和定時器資訊(Times and Timers)

一個程式從建立到終止叫做該程式的生存期(lifetime)。程式在其生存期內使用CPU的時間,核心都要進行記錄,以便進行統計、計費等有關操作。程式耗費CPU 的時間由兩部分組成:一是在使用者模式(或稱為使用者態)下耗費的時間、一是在系統模式(或稱為系統態)下耗費的時間。每個時鐘滴答,也就是每個時鐘中斷,核心都要更新當前程式耗費CPU 的時間資訊。


7.檔案系統資訊(File System)

程式可以開啟或關閉檔案,檔案屬於系統資源,Linux 核心要對程式使用檔案的情況進行記錄。task_struct 結構中有兩個資料結構用於描述程式與檔案相關的資訊。其中,fs_struct 中描述了兩個VFS 索引節點(VFS inode),這兩個索引節點叫做root 和pwd,分別指向程式的可執行映像所對應的根目錄(Home Directory)和當前目錄或工作目錄。file_struct 結構用來記錄了程式開啟的檔案的描述符(Descriptor)。如表4.9 所示。


在檔案系統中,每個VFS 索引節點唯一描述一個檔案或目錄,同時該節點也是向更低層的檔案系統提供的統一的介面。

8.虛擬記憶體資訊(Virtual Memory)

除了核心執行緒(Kernel Thread),每個程式都擁有自己的地址空間(也叫虛擬空間),mm_struct 來描述。另外Linux 2.4 還引入了另外一個域active_mm,這是為核心執行緒而引入的。因為核心執行緒沒有自己的地址空間,為了讓核心執行緒與普通程式具有統一的上下文切換方式,當核心執行緒進行上下文切換時,讓切換進來的執行緒的active_mm 指向剛被排程出去的程式的mm_struct。記憶體資訊如表4.10 所示。


9.頁面管理資訊

當實體記憶體不足時,Linux 記憶體管理子系統需要把記憶體中的部分頁面交換到外存,其交換是以頁為單位的。有關頁面的描述資訊如表4.11。


10.對稱多處理機(SMP)資訊

Linux 2.4 對SMP 進行了全面的支援,表4.12 是與多處理機相關的幾個域。


11.和處理器相關的環境(上下文)資訊(Processor Specific Context)

這裡要特別注意標題:和“處理器”相關的環境資訊。程式作為一個執行環境的綜合,當系統排程某個程式執行,即為該程式建立完整的環境時,處理器(Processor)的暫存器、堆疊等是必不可少的。因為不同的處理器對內部暫存器和堆疊的定義不盡相同,所以叫做“和處理器相關的環境”,也叫做“處理機狀態”。當程式暫時停止執行時,處理機狀態必須保存在程式的thread_struct 結構中,當程式被排程重新執行時再從中恢復這些環境,也就是恢復這些暫存器和堆疊的值。處理機資訊如表4.13 所示。

12.其他

(1)struct wait_queue *wait_chldexit
在程式結束時,或發出系統呼叫wait 時,為了等待子程式的結束,而將自己(父程式)睡眠在該等待佇列上,設定狀態標誌為TASK_INTERRUPTIBLE,並且把控制權轉給排程程式。

(2)Struct rlimit rlim[RLIM_NLIMITS]
每一個程式可以通過系統呼叫setrlimit 和getrlimit 來限制它資源的使用。

(3)Int exit_code exit_signal
程式的返回程式碼以及程式異常終止產生的訊號,這些資料由父程式(子程式完成後)輪流查詢。

(4)Char comm[16]
這個域儲存程式執行的程式的名字,這個名字用在除錯中。

(5)Unsigned long personality
Linux 可以執行X86 平臺上其他UNIX 作業系統生成的符合iBCS2 標準的程式,personality 進一步描述程式執行的程式屬於何種UNIX 平臺的“個性”資訊。通常有PER_Linux,PER_Linux_32BIT,PER_Linux_EM86,PER_SVR4,PER_SVR3,PER_SCOSVR3,
PER_WYSEV386,PER_ISCR4,PER_BSD,PER_XENIX 和PER_MASK 等,參見include/Linux/personality.h>。

(6) int did_exec:1
按POSIX 要求設計的布林量,區分程式正在執行老程式程式碼,還是用系統呼叫execve()裝入一個新的程式。

(7)struct linux_binfmt *binfmt
指向程式所屬的全域性執行檔案格式結構,共有a.out、script、elf、java 等4 種。


二、程式組織方式


1、核心棧

每個程式都有自己的核心棧,當程式從使用者態進入核心態時,CPU 就自動地設定該程式的核心棧,也就是說,CPU 從任務狀態段TSS 中裝入核心棧指標esp,在/include/linux/sched.h 中定義瞭如下一個聯合結構:

 C++ Code 
1
2
3
4
5
6
 
union task_union
{
    struct task_struct task;
    unsigned long stack[2048];
};


從這個結構可以看出,核心棧佔8KB 的記憶體區。實際上,程式的task_struct 結構所佔的記憶體是由核心動態分配的,更確切地說,核心根本不給task_struct 分配記憶體,而僅僅給核心棧分配8KB 的記憶體,並把其中的一部分給task_struct 使用。task_struct 結構大約佔1K 位元組左右,其具體數字與核心版本有關,因為不同的版本其域稍有不同。因此,核心棧的大小不能超過7KB,否則,核心棧會覆蓋task_struct 結構,從而導致核心崩潰。不過,7KB 大小對核心棧已足夠。

2、current 巨集

當一個程式在某個CPU 上正在執行時,核心如何獲得指向它的task_struct 的指標?在linux/include/i386/current.h 中
定義了current 巨集,這是一段與體系結構相關的程式碼:

 C++ Code 
1
2
3
4
5
6
7
8
 
static inline struct task_struct *get_current(void)
{
    struct task_struct *current;
    __asm__("andl %%esp,%0; ":"=r" (current) : "0" (~8191UL));
    return current;
}


3、雜湊表

Linux 在程式中引入的雜湊表叫做pidhash,在include/linux/sched.h 中定義如下:

 C++ Code 
1
2
3
4
 
#define PIDHASH_SZ (4096 >> 2)
extern struct task_struct *pidhash[PIDHASH_SZ];
#define pid_hashfn(x) ((((x) >> 8) ^ (x)) & (PIDHASH_SZ - 1))

其中,PIDHASH_SZ 為表中元素的個數,表中的元素是指向task_struct 結構的指標。pid_hashfn 為雜湊函式,把程式的PID 轉換為表的索引。通過這個函式,可以把程式的PID均勻地雜湊在它們的域(0 到 PID_MAX-1)中。

Linux 利用鏈地址法來處理衝突的PID:也就是說,每一表項是由衝突的PID 組成的雙向連結串列,這種連結串列是由task_struct 結構中的pidhash_next 和 pidhash_pprev 域實現的,同一連結串列中pid 的大小由小到大排列。

4、雙向迴圈連結串列

雜湊表的主要作用是根據程式的pid 可以快速地找到對應的程式,但它沒有反映程式創建的順序,也無法反映程式之間的親屬關係,因此引入雙向迴圈連結串列。每個程式task_struct結構中的prev_task 和next_task 域用來實現這種連結串列。

連結串列的頭和尾都為init_task,它對應的是程式0(pid 為0),也就是所謂的空程式,它是所有程式的祖先。

5、執行佇列

當核心要尋找一個新的程式在CPU 上執行時,必須只考慮處於可執行狀態的程式(即在TASK_RUNNING 狀態的程式),因為掃描整個程式連結串列是相當低效的,所以引入了可執行狀態程式的雙向迴圈連結串列,也叫執行佇列(run queue)。

該佇列通過task_struct 結構中的兩個指標run_list 連結串列來維持。佇列的標誌有兩個:一個是“空程式”idle_task;一個是佇列的長度,,也就是系統中處於可執行狀態(TASK_RUNNING)的程式數目,用全域性整型變數nr_running 表示。

6、等待佇列

程式必須經常等待某些事件的發生,例如,等待一個磁碟操作的終止,等待釋放系統資源或等待時間走過固定的間隔。等待佇列實現在
事件上的條件等待,也就是說,希望等待特定事件的程式把自己放進合適的等待佇列,並放棄控制權。因此,等待佇列表示一組睡眠的程式,當某一條件變為真時,由核心喚醒它們。等待佇列由迴圈連結串列實現。


7、核心執行緒

核心執行緒(kernel thread)

這類執行緒週期性被核心喚醒和排程,主要用於實現系統後臺操作,如頁面對換,重新整理磁碟快取,網路連線等系統工作。

 核心執行緒執行的是核心中的函式,而普通程式只有通過系統呼叫才能執行核心中的函數。
 核心執行緒只執行在核心態,而普通程式既可以執行在使用者態,也可以執行在核心態。
 因為核心執行緒指只執行在核心態,因此,它只能使用大於PAGE_OFFSET(3G)的地址空間。另一方面,不管在使用者態還是核心態,普通程式可以使用4GB 的地址空間。


相關文章