Linux 中的行程 (Process) 被稱為任務 (Task),其資料結構是一個稱為 task_struct 的 C 語言結構,該結構所記錄的欄位相當多,在 Linux 2.6.29.4 版當中光是該結構的宣告就佔了 306 行的程式碼,範例 1 顯示了該結構的開頭與結束部分,由此可見要實作一個作業系統是相當不容易的工程。
範例 1 Linux 中的 Task 之結構
行號 Linux 2.6.29.4 版核心原始碼 include/linux/sched.h 檔案
… …
1115 struct task_struct {
1116 volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
1117 void *stack;
1118 atomic_t usage;
1119 unsigned int flags; /* per process flags, defined below */
1120 unsigned int ptrace;
… …
1263 /* CPU-specific state of this task */
1264 struct thread_struct thread;
1265 /* filesystem information */
1266 struct fs_struct *fs;
1267 /* open file information */
1268 struct files_struct *files;
1269 /* namespaces */
1270 struct nsproxy *nsproxy;
1271 /* signal handlers */
1272 struct signal_struct *signal;
1273 struct sighand_struct *sighand;
… …
1417 #ifdef CONFIG_TRACING
1418 /* state flags for use by tracers */
1419 unsigned long trace;
1420 #endif
1421 };
… …
Linux 的行程結構中包含了一個 thread 欄位 (1264 行),該欄位用來儲存與 CPU 相關的暫存器、分段表等資訊,由於暫存器資訊是與處理器密切相關的,所以每一種 CPU 都會擁有不同的執行緒結構,IA32 (x86) 的thread_struct之程式片段如範例 10.3所示。
範例 2. Linux 中的IA32 (x86) 處理器的Thread 之結構
行號 Linux 2.6.29.4 版核心原始碼 arch/x86/include/asm/processor.h 檔案
… …
391 struct thread_struct {
392 /* Cached TLS descriptors: */
393 struct desc_struct tls_array[GDT_ENTRY_TLS_ENTRIES];
394 unsigned long sp0;
395 unsigned long sp;
… …
##行程狀態
Linux 行程的狀態有 Running, Interruptible, Uninterruptible, Zombie, Stopped 等五種,但後來又增加了 Traced, EXIT_DEAD, TASK_DEAD, TASK_WAKEKILL 等四種,形成了九種狀態 (2.6.29.4 版),如範例 10.4所示。
範例 3. Linux 中的行程狀態
行號 Linux 2.6.29.4 版核心原始碼 include/linux/sched.h 檔案
… …
174 #define TASK_RUNNING 0
175 #define TASK_INTERRUPTIBLE 1
176 #define TASK_UNINTERRUPTIBLE 2
177 #define __TASK_STOPPED 4
178 #define __TASK_TRACED 8
179 /* in tsk->exit_state */
180 #define EXIT_ZOMBIE 16
181 #define EXIT_DEAD 32
182 /* in tsk->state again */
183 #define TASK_DEAD 64
184 #define TASK_WAKEKILL 128
… …
##行程切換
行程切換 (內文切換) 是與處理器密切相關的程式碼,每個處理器的實作方式均有相當大的差異,但基本原理都是將上一個行程 (prev, 以下稱為舊行程) 的暫存器保存後,再將程式計數器設定給下一個行程 (next, 以下稱為新行程)。在 IA32 (x86) 的處理器中,Linux 的行程切換程式碼 如範例 4 所示,該行程切換函數 switch_to(prev, next, last) 是一個內嵌於 C 語言的組合語言巨集,採用 GNU 的內嵌組合語言語法 。
首先,switch_to 最外層是一個 do { … } while (0) 的迴圈,這個語法很奇怪,因為該迴圈根本永遠都只會執行一次,那又為何要用迴圈呢?這純粹只是為了要把中間的組合語言用區塊結構 {…} 包起來而已,但卻很容易誤導讀者,以為那是一個無窮迴圈 。
範例 4. Linux在 IA32 (x86) 處理器上的行程切換程式碼
行號 Linux 2.6.29.4 檔案 arch/x86/include/asm/system.h
… …
30 #define switch_to(prev, next, last) \
31 do {\
32 /* \
33 * Context-switching clobbers all registers, so we clobber\
34 * them explicitly, via unused output variables.\
35 * (EAX and EBP is not listed because EBP is saved/restored \
36 * explicitly for wchan access and EAX is the return value of \
37 * __switch_to()) \
38 */ \
39 unsigned long ebx, ecx, edx, esi, edi; \
40 \
41 asm volatile("pushfl\n\t" /* save flags */ \
42 "pushl %%ebp\n\t" /* save EBP */ \
43 "movl %%esp,%[prev_sp]\n\t"/* save ESP */ \
44 "movl %[next_sp],%%esp\n\t"/* restore ESP */ \
45 "movl $1f,%[prev_ip]\n\t" /* save EIP */ \
46 "pushl %[next_ip]\n\t" /* restore EIP */ \
47 "jmp __switch_to\n" /* regparm call */ \
48 "1:\t" \
49 "popl %%ebp\n\t" /* restore EBP */ \
50 "popfl\n" /* restore flags */ \
51 \
52 /* output parameters */ \
53 [prev_sp] "=m" (prev->thread.sp), \
54 [prev_ip] "=m" (prev->thread.ip), \
55 "=a" (last), \
56 \
57 /* clobbered output registers: */ \
58 "=b" (ebx), "=c" (ecx), "=d" (edx), \
59 "=S" (esi), "=D" (edi) \
60 \
61 /* input parameters: */ \
62 : [next_sp] "m" (next->thread.sp), \
63 [next_ip] "m" (next->thread.ip), \
64 \
65 /* regparm parameters for __switch_to(): */ \
66 [prev] "a" (prev), \
67 [next] "d" (next) \
68 \
69 : /* reloaded segment registers */ \
70 "memory"); \
71 } while (0)
… …
第41行開始才是行程切換的動作,指令pushfl 用來儲存旗標暫存器到堆疊中,pushl %%ebp 用來儲存框架暫存器 (ebp) 到堆疊中,movl %%esp, %[prev_sp] 則用來儲存舊行程的堆疊暫存器 (esp) 到 (prev->thread.sp) 欄位中,而 pushl %[next_sp], %%esp 則是將新行程的堆疊暫存器 (next->thread.sp) 取出後,放到CPU的esp暫存器中,於是建構好新行程的堆疊環境。接著,第45行的movl $1f, %[prev_ip] 將標記 1 的位址放入舊行程的 prev->thread.ip 欄位中。接著46行用指令pushl %[next_ip] 將新行程的程式計數器 next->thread.ip 推入堆疊中,然後利用指令jmp __switch_to跳入C語言的 switch_to() 函數 中,當該函數的返回指令被執行時,將會發生一個奇妙的結果。
由於 switch_to() 函數是一個C語言函數,原本應該被其他C語言函數呼叫的,呼叫前原本上層函數會先將下一個指令的位址存入堆疊中,然後才進行呼叫。C語言函數在返回前會從堆疊中取出返回點,以返回上一層函數繼續執行 。雖然我們是利用組合語言指令 jmp __switch_to 跳入該函數的,但C語言的編譯器仍然會以同樣的方式編譯,於是返回時仍然會從堆疊中取出 pushl %[next_ip] 指令所推入的位址,因而在 switch_to() 函數返回時,就會將程式計數器設為 next->thread.ip,於是透過函數返回的過程,間接的完成了行程切換的動作。
既然新行程已經在 switch_to() 函數返回時就開始執行了,那麼內文切換的動作不就已經完成了嗎?既然如此為何又需要第 49-50 兩行的程式呢?我們必須進一步回答這個問題。
第45行之所以將標記1放入prev->thread.ip中,是為了讓舊行程在下次被喚醒時,可以回到標記 1 的位置。當下次舊行程被喚醒後,就會從標記 1 的位址開始執行,舊行程可以利用第49-50行的popl %%ebp; popfl 兩個指令,恢復其 ebp (框架指標) 與旗標暫存器,然後再度透過 switch_to(),切換回舊行程 (只不過這次舊行程變成了函數switch_to(prev, next, last) 中的 next 角色,不再是『舊行程』了。
或許我們可以說 switch_to() 函數其實並不負責切換行程,因為該函數會將處理器中各種需要保存的值存入舊行程 prev 的 task_struct 結構中,以便下次 prev 行程被喚醒前可以回存這些暫存器值,其實並沒有切換或執行新行程的功能,但因為 jmp __switch_to 指令前的 pushl %[next_ip] 指令,導致該函數在返回時順便做了行程切換的動作,這種隱含性是作業系統設計時一種相當弔詭的技巧,也是學習 Linux 時對程式人員最大的挑戰之一。
##排程
解說完行程切換的原理後,我們就可以來看看排程系統了。Linux 採用的排程機制較為複雜,該排程式建構在一個稱為 goodness 的行程 (或執行緒) 評估值上,goodness 是一個從 0 到 1999 之間的整數。一般執行緒的 goodness 小於 1000,但即時執行緒的 goodness 則會從 1000 開始,因此保證了任何即時執行緒的 goodness 都會高於一般執行緒。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int spawn(char *prog, char **arg_list) { // Spawn為生育的意思
pid_t child;
child = fork(); // 用fork()函數分枝出子行程
if (child != 0) { // 如果不成功
return child; // 傳回失敗的行程代碼
} else { // 否則
execvp(prog, arg_list); // 將 prog 參數所指定的
fprintf(stderr, "spawn error\n"); // 程式載入到子行程中
return -1;
}
}
int main() { // 主程式開始
char *arg_list[] = { "ls", "-l", "/etc", NULL }; // ls -l /etc
spawn("ls", arg_list); // 開始分支
printf("The end of program.\n"); // 印出主程式結束訊息
return 0;
}
執行過程與結果
$ gcc fork.c -o fork
$ ./fork
The end of program.
$ total 94
-rwxr-x--- 1 ccc Users 2810 Jun 13 2008 DIR_COLORS
drwxrwx---+ 2 ccc Users 0 Oct 7 2008 alternatives
-rwxr-x--- 1 ccc Users 28 Jun 13 2008 bash.bashrc
drwxrwx---+ 4 ccc Users 0 Oct 7 2008 defaults
-rw-rw-rw- 1 ccc Users 716 Oct 7 2008 group
….
##執行緒
POSIX標準中支援的執行緒函式庫稱為 pthread,我們可以透過 pthread 結構與 pthread_create() 函數執行某個函數指標,以建立新的執行緒。範例 6 顯示了一個利用 pthread 建立兩個執行緒,分別不斷印出 George 與 Mary 的程式。該程式中總共有三個執行主體,第一個是 print_george()、第二個是 print_mary()、第三個是主程式本身。由於每隔 1 秒印一次 George ,但每隔 0.5 秒就印一次 Mary,因此執行結果會以 George, Mary, George, George,Mary 的形式印出。
範例 6 利用pthread 函式庫建立執行緒的範例
#include <pthread.h> // 引用 pthread 函式庫
#include <stdio.h>
void *print_george(void *argu) { // 每隔一秒鐘印出一次 George 的函數
while (1) {
printf("George\n");
sleep(1);
}
return NULL;
}
void *print_mary(void *argu) { // 每隔一秒鐘印出一次 Mary 的函數
while (1) {
printf("Mary\n");
sleep(2);
}
return NULL;
}
int main() { // 主程式開始
pthread_t thread1, thread2; // 宣告兩個執行緒
pthread_create(&thread1, NULL, &print_george, NULL); // 執行緒 print_george
pthread_create(&thread2, NULL, &print_mary, NULL); // 執行緒 print_mary
while (1) { // 主程式每隔一秒鐘
printf("----------------\n"); // 就印出分隔行
sleep(1); // 停止一秒鐘
}
return 0;
}
執行過程與結果
$ gcc thread.c -o thread
$ ./thread
George
Mary
-------------------------
George
-------------------------
George
Mary
-------------------------
George
-------------------------
…