在Linux內核中,內核將進程、線程和內核線程一視同仁,即內核使用唯一的數據結構task_struct來分別表示他們;內核使用相同的調度算法對這三者進行調度;並且內核也使用同一個函數do_fork()來分別創建這三種執行線程(thread of execution)。執行線程通常是指任何正在執行的代碼實例,比如一個內核線程,一箇中斷處理程序或一個進入內核的進程。
這樣處理無疑是簡潔方便的,並且內核在統一處理這三者之餘並沒有失去他們本身所具有的特性。本文將結合進程、線程和內核線程的特性淺談進程在內核中的角色扮演問題。
1.進程描述符task_struct的多角色扮演
上述三種執行線程在內核中都使用統一的數據結構task_struct來表示。task_struct結構即所謂的進程描述符,它包含了與一個進程相關的所有信息。進程描述符中不僅包含了許多描述進程屬性的字段,而且還有一系列指向其他數據結構的指針。下面將簡單介紹進程描述符中幾個比較特殊的字段,它們分別指向代表進程所擁有的資源的數據結構。
mm字段:指向mm_struct結構的指針,該類型用來描述進程整個的虛擬地址空間。
fs字段:指向fs_struct結構的指針,該類型用來描述進程所在文件系統的根目錄和當前進程所在的目錄信息。
files字段:指向files_struct結構的指針,該類型用來描述當前進程所打開文件的信息。
signal字段:指向signal_struct結構(信號描述符)的指針,該類型用來描述進程所能處理的信號。
對於普通進程來說,上述字段分別指向具體的數據結構以表示該進程所擁有的資源。
對應每個線程而言,內核通過輕量級進程與其進行關聯。輕量級進程之所輕量,是因爲它與其他進程共享上述所提及的進程資源。比如進程A創建了線程B,則B線程會在內核中對應一個輕量級進程。這個輕量級進程很自然的對應一個進程描述符,只不過B線程的進程描述符中的某些代表資源指針會和A進程中對應的字段指向同一個數據結構,這樣就實現了多線程之間的資源共享。
由於內核線程只運行在內核態,並且只能由其他內核線程創建,所以內核線程並不需要和普通進程那樣的獨立地址空間。因此內核線程的進程描述符中的mm指針即爲NULL。內核線程是否共享父內核線程的某些資源,則通過向內核線程創建函數kernel_thread()傳遞參數來決定。
通過上面的分析可以發現,內核中使用統一的進程描述符來表示進程、線程和內核線程,根據他們不同的特性,其進程描述符中某些代表資源的字段的指向會有所不同,以實現扮演不同角色。
2. do_fork()的多角色扮演
進程、線程以及內核線程都有對應的創建函數,不過這三者所對應的創建函數最終在內核都是由do_fork()進行創建的,具體的調用關係圖如下:
從圖中可以看出,內核中創建進程的核心函數即爲看do_fork(),該函數的原型如下:
1 |
long
do_fork(unsigned long
clone_flags, |
2 |
unsigned long
stack_start, |
3 |
struct pt_regs *regs,
|
4 |
unsigned long
stack_size, |
5 |
int __user *parent_tidptr,
|
6 |
int __user *child_tidptr) |
該函數的參數個數是固定的,每個參數的功能如下:
clone_flags:代表進程各種特性的標誌。低字節指定子進程結束時發送給父進程的信號代碼,一般爲SIGCHLD信號,剩餘三個字節是若干個標誌或運算的結果。
stack_start:子進程用戶態堆棧的指針,該參數會被賦值給子進程的esp寄存器。
regs:指向通用寄存器值的指針,當進程從用戶態切換到內核態時通用寄存器中的值會被保存到內核態堆棧中。
stack_size:未被使用,默認值爲0。
parent_tidptr:該子進程的父進程用戶態變量的地址,僅當CLONE_PARENT_SETTID被設置時有效。
child_tidptr:該子進程用戶態變量的地址,僅當CLONE_CHILD_SETTID被設置時有效。
既然進程、線程和內核線程在內核中都是通過do_fork()完成創建的,那麼do_fork()是如何體現其功能的多樣性?其實,clone_flags參數在這裏起到了關鍵作用,通過選取不同的標誌,從而保證了do_fork()函數實現多角色——創建進程、線程和內核線程——功能的實現。clone_flags參數可取的標誌很多,下面只介紹幾個與本文相關的標誌。
CLONE_VIM:子進程共享父進程內存描述符和所有的頁表。
CLONE_FS:子進程共享父進程所在文件系統的根目錄和當前工作目錄。
CLONE_FILES:子進程共享父進程打開的文件。
CLONE_SIGHAND:子進程共享父進程的信號處理程序、阻塞信號和掛起的信號。使用該標誌必須同時設置CLONE_VM標誌。
如果創建子進程時設置了上述標誌,那麼子進程會共享這些標誌所代表的父進程資源。
2.1 進程的創建
在用戶態程序中,可以通過fork()、vfork()和clone()三個接口函數創建進程,這三個函數在庫中分別對應同名的系統調用。系統調用函數通過128號軟中斷進入內核後,會調用相應的系統調用服務例程。這三個函數對應的服務歷程分別是sys_fork()、sys_vfork()和sys_clone()。
01 |
int
sys_fork( struct
pt_regs *regs) |
02 |
{ |
03 |
return do_fork(SIGCHLD, regs->sp, regs, 0, NULL, NULL);
|
04 |
} |
05 |
int
sys_vfork( struct
pt_regs *regs) |
06 |
{ |
07 |
return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs->sp, regs, 0, |
08 |
NULL, NULL); |
09 |
} |
10 |
long |
11 |
sys_clone(unsigned long clone_flags, unsigned
long newsp,
|
12 |
void __user *parent_tid,
void __user *child_tid,
struct pt_regs *regs)
|
13 |
{ |
14 |
if (!newsp)
|
15 |
newsp = regs->sp; |
16 |
return do_fork(clone_flags, newsp, regs, 0, parent_tid, child_tid); |
17 |
} |
通過上述系統調用服務例程的源碼可以發現,三個服務歷程內部都調用了do_fork(),只不過差別在於第一個參數所傳的值不同。這也正好導致由這三個進程創建函數所創建的進程有不同的特性。下面對每種進程作以簡單說明。
fork():由於do_fork()中clone_flags參數除了子進程結束時返回給父進程的SIGCHLD信號外並無其他特性標誌,因此由fork()創建的進程不會共享父進程的任何資源。子進程會完全複製父進程的資源,也就是說父子進程相對獨立。不過由於寫時複製技術(Copy On Write,COW)的引入,子進程可以只讀父進程的物理頁,只有當兩者之一去寫某個物理頁時,內核此時纔會將這個頁的內容拷貝到一個新的物理頁,並把這個新的物理頁分配給正在寫的進程。
vfork():do_fork()中的clone_flags使用了CLONE_VFORK和CLONE_VM兩個標誌。CLONE_VFORK標誌使得子進程先於父進程執行,父進程會阻塞到子進程結束或執行新的程序。CLONE_VM標誌使得子進程共享父進程的內存地址空間(父進程的頁表項除外)。在COW技術引入之前,vfork()適用子進程形成後立馬執行execv()的情形。因此,vfork()現如今已經沒有特別的使用之處,因爲寫實複製技術完全可以取代它創建進程時所帶來的高效性。
clone():clone通常用於創建輕量級進程。通過傳遞不同的標誌可以對父子進程之間數據的共享和複製作精確的控制,一般flags的取值爲CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND。由上述標誌可以看到,輕量級進程通常共享父進程的內存地址空間、父進程所在文件系統的根目錄以及工作目錄信息、父進程當前打開的文件以及父進程所擁有的信號處理函數。
2.2 線程的創建
每個線程在內核中對應一個輕量級進程,兩者的關聯是通過線程庫完成的。因此通過pthread_create()創建的線程最終在內核中是通過clone()完成創建的,而clone()最終調用do_fork()。
2.3 內核線程的創建
一個新內核線程的創建是通過在現有的內核線程中使用kernel_thread()而創建的,其本質也是向do_fork()提供特定的flags標誌而創建的。
1 |
int
kernel_thread( int
(*fn)( void *), void
*arg, unsigned long
flags) |
2 |
{ |
3 |
/*some register operations*/ |
4 |
return do_fork(flags | CLONE_VM | CLONE_UNTRACED, 0, ®s, 0, NULL, NULL); |
5 |
} |
從上面的組合的flag可以看出,新的內核線程至少會共享父內核線程的內存地址空間。這樣做其實是爲了避免賦值調用線程的頁表,因爲內核線程無論如何都不會訪問用戶地址空間。CLONE_UNTRACED標誌保證內核線程不會被任何進程所跟蹤,
3. 進程的調度
由於進程、線程和內核線程使用統一數據結構來表示,因此內核對這三者並不作區分,也不會爲其中某一個設立單獨的調度算法。內核將這三者一視同仁,進行統一的調度。