進程控制塊PCB結構 task_struct 描述

注:本分類下文章大多整理自《深入分析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[2408];
};



從這個結構可以看出,內核棧佔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、內核線程


內核線程(thread)(也稱爲daemon)


 內核線程執行的是內核中的函數,而普通進程只有通過系統調用才能執行內核中的函數。

 內核線程只運行在內核態,而普通進程既可以運行在用戶態,也可以運行在內核態。

 因爲內核線程指只運行在內核態,因此,它只能使用大於PAGE_OFFSET(3G)的地址空間。另一方面,不管在用戶態還是內核態,普通進程可以使用4GB 的地址空間。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章