Linux 2.6內核筆記【Process-1】

終於掙脫了《Understanding the Linux Kernel》的Process一章。中文版的翻譯低級錯誤太多,所以只好繼續看影印版。

 

簡介部分,除了通常我們對Process的認識,Linux中值得一提的是:笨重的不分青紅皁白把父進程整個地址空間都複製過來的fork()採用了傳說中的Copy-on-Write技術;還有就是2.6啓用了lightweight process來支持native的thread。從前是模擬pthread實現,現在的native thread有了LinuxThreads, Native POSIX Thread Library(NPTL)和IBM's Next Generation Posix Threading Package(NGPT)這些庫支持。而這又引入了thread group的概念,因爲屬於同一進程的多個線程(lightweight process)雖然是process,卻要以某種一致的方式響應getpid()之類的系統調用,因此被放在同一個thread group中。

 

也因爲這個原因,本文中的process都直接寫英文,偶爾出現進程,那是在傳統的語境下討論進程與線程之間的關係。

 

Process Descriptor,也就是struct task_struct,又名task_t,是一個長達306行,集合了衆多設計智慧的結構。它非常複雜,不僅有很多字段來表徵process的屬性,還有很多指向其他結構的指針,比如thread_info這個非常重要的結構。

process的狀態

 

字段state

 

運行着的

TASK_RUNNING   其實是 可運行的。schedule()會按照時間片輪流讓所有狀態爲TASK_RUNNING的process運行。

 

睡眠着、等待着的
TASK_INTERRUPTIBLE   在等待hardware interrupt, system resource,或是signal。
TASK_UNINTERRUPTIBLE   同上,但signal叫不醒。

停下來了的
TASK_STOPPED  退出了。
TASK_TRACED 被Debugger停下來。

 

字段exit_state或state:


EXIT_ZOMBIE   非正常死亡。其parent process還沒有用wait4()或waitpid()獲取他的遺物,所以內核不敢焚燒屍體。
EXIT_DEAD  遺物獲取完畢了,可以焚燒屍體了。如果是非正常死亡,由於init會接過來做養父,所以init會獲取他的遺物。

 

process之間的組織

 

有時候面向對象的思想會阻礙我們對現實世界的表達,尤其是可能阻礙性能上的優化。

STL這種利用泛型實現的不侵入的,一般化的途徑固然好。但 2.6內核中task_t的結構說明,使用侵入式的embeded數據結構,可以更好地在實體間織出多種關係,滿足性能和各方面的要求。

只使用task_t一個結構,利用embeded的雙向鏈表(struct list_head)和單向鏈表(struct hlist_head),process之間就織出了process list、runqueue、waitqueue、pidhash table、chained list(thread group)等多個關係,並由外在的array統領,實現了高效率的查找與多個字段間的映射。

 

此筆記不具體複述書中的討論,只勾勒基本圖景。

 

process list包含了所有的task_t, 用的是雙向鏈表,內嵌字段名是tasks。

 

runqueue包含了所有state爲TASK_RUNNING的task_t,由140個(一個優先級一個)雙向鏈表組成,內嵌字段名是run_list。這140個雙向鏈表的頭放在struct prio_array_t裏的一個array中。

 

我們知道,PID可以唯一identify一個process。其實PID有4種,一種是process自身create時候內核 sequentially分配的ID(pid),一種是thread group中leader的PID(tgid),這個ID其實是進程的主線程的ID,一種是process group中eader的PID(pgrp)[補充介紹:process group的一個常見例子就是:在Bash中執行ls|grep sth|more這樣的命令,這裏3個process就應該被組織在一個process group中],還有一種是一個session中leader的PID。

 

因此pidhash table是一個有4項的array,每個array分別是一個對該類PID的hash。這個hash對collision的解決辦法是chaining。以tgid爲例,collide的tgid的進程被一個單向鏈表chain着,而同一tgid的進程則只有leader掛在chian上,其他則以雙向鏈表的形式掛在leader上。

 

注意,根據我在LXR中的查證,2.6.11中的對pidhash table、chained list很重要的struct pid,在最新的2.6.29中已經被包裹在struct pid_link中,而且內部的字段也脫胎換骨,其中用於表達thread group的內嵌雙向鏈表字段被拆出來直接放在task_t裏。這樣對thread group的表達就更爲清晰直接。因此書中的討論已不完全適用。

 

waitqueues,則是所有TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE狀態的process。它們按所等待的事件分別排在不同的隊(雙向鏈表)中。

 

這裏涉及的結構是wait_queue_t。它除了process的指針,還包含了flag和類型爲wait_queue_func_t的喚醒處理函數。

 

flag爲0說明等待的事件是nonexclusive的,所以事件發生時,喚醒所有等它的process,爲1說明等待的事件是exclusive的,比如只有一個的資源,就只喚醒一個。

 

在隊列中nonexclusive的process永遠從前面加進去(不必分先來後到,大家一起醒),exclusive的process永遠從後面加進去(要分先來後到)。這是由add_wait_queue()和add_wait_queue_exclusive()完成的。這樣排隊,使得wake_up宏中的循環可以在成功喚醒第一個exclusive的process就終止。

 

睡眠和喚醒process的函數或宏有:sleep_on族、2.6引入的wait族函數、wait_event族宏、wake_up族宏。這裏只講一下sleep_on()。

 

sleep_on()的本質就是把進程從runqueue拿出來放進wait_queue,然後重新調用schedule(),面對新的runqueue,按照算法,繼續調度。schedule()返回之後(說明又讓自己執行了),就把自己再從從wait_queue拿出來放進runqueue,然後接着執行自己接下來的代碼。

 

內核是如何獲取當前process的

用current這個宏可以獲得當前process的task_t結構的指針。

 

低版本Linux的current是一個邪惡的全局變量。高版本則利用了內存佈局,智能地推斷出當前process。

 

Linux用一個union把當前process的thread_info和(倒着增長的)kernel棧放在一個兩page長(8kb)的內存區域。

 

  union thread_union {
        struct thread_info thread_info;
        unsigned long stack[2048]; /* 1024 for 4KB stacks */
    };

 

利用這樣的內存佈局,三行彙編就可以獲得當前process:

 

movl $0xffffe000,%ecx /* or 0xfffff000 for 4KB stacks */

andl %esp,%ecx

movl (%ecx),p
 

第一二行mask掉esp的一部分,到達了thread_info所對齊的地方。

然後利用指向相應task_t的task字段在thread_info的offset 0的位置的事實,直接**ecx賦值給p,這時p就是當前process的task_t結構的指針。

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