這幾天在公司閑暇時間看了一下Robert Love的《linux核心設計與實現》(當然,我看的是中文翻譯版哈。看到英文就頭痛)進程管理、調度、系統調度、中斷這幾章,覺得這本書真的寫的很好,對我這種層級的人剛好,看起來沒那麼卡,不像最開始接觸linux的時候就抱起《linux核心源碼情景分析》來看,看得我“雲裡來霧裡去”,現在想起來真的覺得就是一口吃成大胖子,還沒學會走路就開始跑了,好了,廢話少說,這裡把進程管理這章好好複習一下,希望有興趣的人一起來學習!
1 幾個概念
什麼是進程
進程就是處理執行期的程式。但進程不局限於一段可執行程式代碼,通常還包括其他資源,如存放全域變數的資料區段、開啟的檔案、掛起的訊號等,當然還包括地址空間及一個或幾個執行線程。
什麼是線程
線程就是在進程中活動的對象。(呵呵,好抽象啊)。每個線程都擁有一個獨立的程式計數器、進程棧和一組進程寄存器。核心調度的對象是線程,而不是線程。在現在的系統大都都支援多線程應用程式,稍後會看到,在linux核心中,對線程的實現和進程並無特別區分。
建立進程
要注意的是程式不是進程。進程是處於執行期的程式以及它所包含的資源的總稱。實際上完全可能存在兩個或多個不同的進程實際上執行同一個程式。並且它們還可以共用諸如開啟的檔案、地址空間之類的資源。在linux系統中,調用fork()系統調用(下次專門寫一篇linux系統調用的實現機制),該系統調用複製一個現有進程來建立一個全新的進程。調用fork()的進程稱為父進程,新產生的進程為子進程。在返回點這個相同位置上,父進程恢複執行,子進程開始執行。通常建立新惡進程為了執行新的不同的程式,會接著調用exec()這族函數;
進程退出
程式通過exit()系統調用退出執行。這個函數會終結進程並將佔用的資源釋放掉。父進程可以通過wait4()(核心負責實現
wait4()系統調用。linux系統通過C庫通常要提供wait() waitpid() wait3() wait4()函數,下次介紹了系統調用的實現就明白C庫與系統調用的關係了)系統調用查詢子進程是否終結,這其實使得進程有了等待特定進程執行完畢的能力。進程退出執行後被設定為僵死狀態,直到它的父進程調用wait()或waitpid()為止。
任務
經常我們能聽到任務(task)這個概念。其實,linux核心通常把進程也叫做任務,經常,我們一般把在核心中啟動並執行程式叫任務,在使用者空間啟動並執行程式叫進程。
2 進程描述符和任務隊列
核心把進程放在叫做任務隊列(task list)的雙向迴圈鏈表中。鏈表中的每一項都是task_struct,稱為進程描述符(process decriptor)的結構,該結構定義在include/linux/sched.h檔案中。進程描述符包含一個具體進程的所有資訊。
task_struct
task_struct相對較大,在32位機器上,大約為1.7K位元組。但它包含了核心管理一個進程的所有資訊,它包含的資料能完整描述一個正在執行的程式:它開啟的檔案、進程的地址空間、掛起的訊號、進程的狀態等。
struct task_struct
{
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
void *stack;
atomic_t usage;
unsigned int flags; /* per process flags, defined below */
unsigned int ptrace;
int lock_depth; /* BKL lock depth */
....
}
Linux通過slab分配器分配task_struct結構,這樣能達到對象複用和緩衝的目的(可以避免動態分配和釋放所帶來的資源消耗)。在2.6核心中,各個進程的task_struct存放在核心棧的尾端。這樣,通過棧指標就能計算它們的位置。在linux中,在棧底或棧頂建立了一個新的結構struct thread_info:(<asm/thread_info.h>中定義)
struct thread_info
{
struct task_struct *task; /* main task structure */
struct exec_domain *exec_domain; /* execution domain */
__u32 flags; /* low level flags */
__u32 status; /* thread synchronous flags */
__u32 cpu; /* current CPU */
int preempt_count; /* 0 => preemptable, <0 => BUG */
mm_segment_t addr_limit;
struct restart_block restart_block;
void __user *sysenter_return;
#ifdef CONFIG_X86_32
unsigned long previous_esp; /* ESP of the previous stack in case of nested (IRQ) stacks */
__u8 supervisor_stack[0];
#endif
int uaccess_err;
}
進程核心棧 (說明一下:每個進程都有自己的核心棧。當進程從使用者態進入核心態時,CPU就自動地設定該進程的核心棧)
------------------------------------------最高記憶體位址
| |
| 棧首 |
| |
| |
| |
| |
| |
| |
| |
|-----------------------------------------|棧指標
| |
| |
| |
| |
|-----------------------------------------|
| |
| |
| struct thread_struct |
|-----------------------------------------|最低記憶體位址 current_thread_info()
thread info 有一個指向進程描述符的指標
每個任務的thread_info結構在它的核心棧的尾端分配。結構中task存放的是指向該任務實際task_struct指標。
這個地方我有點納悶,直接用task_struct不就行了嗎?為什麼還要thread_info,不可以把thread_info其他一些資訊放到task_struct裡面???
可以通過current_thread_info()->task返回當前進程資訊結構
進程狀態
進程描述符中state描述了進程當前的狀態,有下面五種狀態:
TASK_RUNNING 運行------進程可執行;它或者正在執行,或者在運行隊列中等待執行(我們知道如果cpu單核的話,一次只能執行一個任務,其他的任務在隊列中等待執行)
TASK_INTERRUPTIBLE 可中斷----進程正在睡眠,等待某些條件達成。一旦這些條件達成,核心就會把進程狀態設定為運行。此狀態也會因為接收到訊號而提前被喚醒並投入運行
TASK_UNINTERRUPTIBLE 不可中斷----除了不會因為接收到訊號而投入運行,其狀態與可中斷狀態相同。使用得較少
TASK_ZOMBIE 僵死-------該進程結束了,但是父進程還沒有調用wait4()系統調用。為了父進程能夠獲知它的訊息,子進程的進程描述符仍然保留著
TASK_STOPPED 停止-------進程停止執行;進程沒有投入運行也不能投入運行。通常發生在接收到SIGSTOP SIGSTP SIGTTIN SIGTTOU等訊號的時候
設定當前進程的狀態
採用set_current_state(state)或set_task_state(curent, state)函數。上面我們說了current_thread_info()->task很容易就擷取了當前進程描述符,為什麼不直接擷取到了task->state = state設定呢?
那是因為如果在SMP(多核)系統中,需要保護,防止其他處理器做重新排序(進程運行隊列);
進程上下文
這是一個很重要的概念。當一個程式執行系統調用或觸發了某個異常時,它就陷入了核心空間,這時,我們稱核心“代碼進程執行”並處理進程上下文中。在此上下文中current宏是有效地。除非有更進階的進程搶佔了當前進程的執行,否則在核心退出的時候,程式恢複在使用者空間繼續執行。
系統調用和例外處理常式是對核心明確定義的介面。進程只有通過這些介面才能陷入核心。
LINUX進程之間存在一個明顯的繼承關係。所有進程都是PID為1的init進程的後代。核心在系統啟動的最後階段啟動init進程,該進程讀取系統的初始化指令碼並執行相關程式,最終完成系統的啟動全過程。
每個進程都有一個父進程,每個進程可以擁有一個或多個子進程。擁有同一個父進程的所有進程為兄弟。進程間的關係存放在進程描述符中。每個task_struct都包含一個父進程指標,還包含一個children的子進程鏈表。
struct task_struct *task = current->parent; /* 擷取當前進程的父進程 */
struct list_head *list;
可以通過以下方式依次訪問子進程:
struct task_struct *task;
struct list_head *list;
list_for_each(list, ¤t->children)
{
task = list_entry(list, struct task_struct, sibling) /*sibling也是進程描述符的成員。核心是這樣解釋的:linkage in my parent's children list,父進程的子進程鏈表,也就是當前進程的兄弟進程鏈表*/
}
init進程的進程描述符是作為init_task靜態分配的。(其實就是一個靜態變數吧)
下面代碼可得到所有進程之間的關係
struct task_struct *task;
for (task = current; task != &init_task; task = task->parent)
{
;
}
上面可以看到,可以從系統任何一個進程出發,找到任意指定的其他進程。但是我們不需要這麼麻煩,只要簡單的遍曆系統中的所有進程,因為任務隊列本來就是一個雙向迴圈鏈表。如下:
對於給定的進程,擷取鏈表下一個進程:
list_entry(task->tasks.next, struct task_struct, tasks)
(解釋一下這個什麼意思,回想一下前面的進程描述符結構,結構裡是不是有一個成員list_head tasks 。 list_entry是一個宏,由它可以由其成員tasks的指標task->tasks.next得到struct task_struct的地址
不知道有沒有說清楚、。。)
同理,擷取前一個進程:
list_entry(task->tasks.prev, struct task_struct, tasks)
同樣,for_each_process(task)宏提供了一個依次訪問這個任務的能力:
struct task_struct *task;
for_each_process(task)
{
}
但是,在一個擁有大量進程的系統中,通過這樣的方法來遍曆所有進程是非常耗時的,所以沒有充足理由別這麼幹
3 進程建立
寫時拷貝(copy-on-write)
一開始我們介紹了進程的建立,傳統的fork()系統調用直接把所有資源複製給新建立的進程。這種實現過於簡單且效率低下。linux的fork()使用寫時拷貝(copy-on-write)頁實現
寫時拷貝是一種可以延遲甚至免除拷貝資料的技術。核心並不複製整個進程地址空間,而是讓父進程和子進程共用同一個拷貝,只有在需要寫入的時候,資料才會被複製,從而使各個進程擁有各自的拷貝。
fork()實際開銷就是複製父進程的頁表以及子進程建立唯一的進程描述符。
fork()
Linux通過clone()系統調用實現fork()。這個調用通過一系列的參數標誌來指明父、子進程需要共用的資源。fork()、vfork()、 __clone()庫函數都是根據各自需要的標誌去調用clone()。
然後由clone()去調用do_fork()。
do_fork完成了建立中的大部分工作,它定義在kernel/fork.c,該函數調用copy_process函數,然後讓進程開始運行。
copy_process()主要工作:
(1)調用dup_task_struct()為新進程建立一個核心棧、thread_info和task_struct,這些值與當前進程的值相同,此時父子進程描述符完成相同;
(2)檢查新建立子進程是否超過進程數目限制;
(3)子進程描述符很多成員清0或者設為初始值,區別父進程;
(4)子進程的狀態設定為TASK_UNINTERRUPTIBLE以保證它不會投入運行;
(5)調用copy_flags()以更新task_struct的flags成員。表明進程是否擁有超級使用者權限的PF_SUPERPRIV標誌清0。表明進程還沒調用exec()函數的PF_FORKNOEXEC標誌被設定;
(6)調用get_pid()為新進程擷取一個有效PID;
(7)根據傳遞給clone()的參數標誌,拷貝資源;
(8)讓父子進程平分剩餘的時間片
(9)返回一個指向子進程的指標
再回到do_fork()函數,如果copy_process()函數成功返回,新建立的子進程被喚醒並讓其投入運行。核心有意選擇子進程首先執行。因為一般子進程都會馬上調用exec()函數,這樣就可以避免寫時拷貝的額外開銷,
vfork()
vfork是fork還未實現寫時拷貝頁表現的一個最佳化,後來fork實現了就徹底沒用了,這裡也不介紹了;