進程控制塊PCB結構 task_struct 描述

注:本分類下文章大多整理自《深刻分析linux內核源代碼》一書,另有參考其餘一些資料如 《linux內核徹底剖析》、《linux c 編程一站式學習》等,只是爲了更好地理清系統編程和網絡編程中的一些概念性問題,並無深刻地閱讀分析源碼,我也是草草翻過這本書,請有興趣的朋友本身參 考相關資料。此書出版較早,分析的版本爲2.4.16,故出現的一些概念可能跟最新版本內核不一樣。html

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

1、task_struct 結構描述node


1.進程狀態(State)linux


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


(1)可運行狀態網絡

處於這種狀態的進程,要麼正在運行、要麼正準備運行。正在運行的進程就是當前進程(由current 宏 所指向的進程),而準備運行的進程只要獲得CPU 就能夠當即投入運行,CPU 是這些進程惟一等待的系統資源。系統中有一個運行隊列(run_queue),用來容納全部處於可運行狀態的進程,調度程序執行時,從中選擇一個進程投入運行。當前運行進程一直處於該隊列中,也就是說,current老是指向運行隊列中的某個元素,只是具體指向誰由調度程序決定。數據結構


(2)等待狀態多線程

處於該狀態的進程正在等待某個事件(Event)或某個資源,它確定位於系統中的某個等待隊列(wait_queue)中。Linux 中處於等待狀態的進程分爲兩種:可中斷的等待狀態和不可中斷的等待狀態。處於可中斷等待態的進程能夠被信號喚醒,若是收到信號,該進程就從等待狀態進入可運行狀態,而且加入到運行隊列中,等待被調度;而處於不可中斷等待態的進程是由於硬件環境不能知足而等待,例如等待特定的系統資源,它任何狀況下都不能被打斷,只能用特定的方式來喚醒它,例如喚醒函數wake_up()等。ide


(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 種。


2、進程組織方式


一、內核棧


每一個進程都有本身的內核棧,當進程從用戶態進入內核態時,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 大小對內核棧已足夠。


二、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;
}


三、哈希表


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 的大小由小到大排列。


四、雙向循環鏈表


哈希表的主要做用是根據進程的pid 能夠快速地找到對應的進程,但它沒有反映進程創建的順序,也沒法反映進程之間的親屬關係,所以引入雙向循環鏈表。每一個進程task_struct結構中的prev_task 和next_task 域用來實現這種鏈表。


鏈表的頭和尾都爲init_task,它對應的是進程0(pid 爲0),也就是所謂的空進程,它是全部進程的祖先。


五、運行隊列


當內核要尋找一個新的進程在CPU 上運行時,必須只考慮處於可運行狀態的進程(即在TASK_RUNNING 狀態的進程),由於掃描整個進程鏈表是至關低效的,因此引入了可運行狀態進程的雙向循環鏈表,也叫運行隊列(run queue)。


該隊列經過task_struct 結構中的兩個指針run_list 鏈表來維持。隊列的標誌有兩個:一個是「空進程」idle_task;一個是隊列的長度,,也就是系統中處於可運行狀態(TASK_RUNNING)的進程數目,用全局整型變量nr_running 表示。


六、等待隊列


進程必須常常等待某些事件的發生,例如,等待一個磁盤操做的終止,等待釋放系統資源或等待時間走過固定的間隔。等待隊列實如今

事件上的條件等待,也就是說,但願等待特定事件的進程把本身放進合適的等待隊列,並放棄控制權。所以,等待隊列表示一組睡眠的進程,當某一條件變爲真時,由內核喚醒它們。等待隊列由循環鏈表實現。


七、內核線程


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


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

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

 由於內核線程指只運行在內核態,所以,它只能使用大於PAGE_OFFSET(3G)的地址空間。另外一方面,無論在用戶態仍是內核態,普通進程可使用4GB 的地址空間。

相關文章
相關標籤/搜索