純使用者空間的搶佔式多線程庫其實是很麻煩的一件事,在設計之前首先必須明白搶佔式多線程的意義,其本質就是古老的unix多道程式設計,策略可以是分時的,也可以是其它任何的調度策略,不管什麼策略,機制要素都是底層的OS核心和機器硬體提供的,對於x86上的linux來說,這些要素包括:分頁機制--提供進程間相同虛擬位址不衝突的棧,線程間不同虛擬位址不衝突的棧;時鐘中斷以及任意中斷機制--可以在不通知使用者進程的情況下中斷之,然後進行調度抉擇,該機制是調度策略的前提;fork機制--啟動新線程。只有完全類比出以上等機制,多線程才是搶佔式的。網上有篇文章用setjmp和longjmp實現了一個協作多線程,由於何時調度必須由線程自己決定,因此那不能算是搶佔式的。由於x86上的linux核心的分層設計並沒有提供下層對上層的調用,因此實現一個純使用者空間的搶佔式多線程真的很麻煩。
純使用者空間搶佔式多線程的外部環境有兩個要點:要點之一是抽象一台機器,該抽象的機器必須可以在進程外部將進程中斷,有一種辦法是向進程發訊號;要點之二是必須能夠得到進程當前的環境,比如所有寄存器,並且能儲存這個環境。內部環境也有兩個要點:其一是每個線程必須有一個屬於自己的棧,由於這是純使用者空間的線程,因此最好自己用諸如malloc的方式動態分配;其二是每個線程必須可以自己啟動。下面是MultiThread的部分代碼:
//jmp_buf env[2];
//int idx[2];
void interrput_func (int sig)
{
static int flag = 1;
//idx[1] = setjmp(env[1]); //可惜setjmp只能儲存當前棧的context,因此在此無法獲得被訊號中斷之前的context,故而必須通過ptrace介面協助。
if (1 == flag) {
flag ++;
...//建立一個新的堆棧,也就是重新設定esp寄存器,在新的堆棧上啟動thread_func2
thread_func2()
}
}
void thread_func1 ()
{
while (1) {
printf("f1---/n");
}
}
void thread_func2()
{
while (1) {
printf ("f2---/n");
}
}
省略建立堆棧的代碼,thread_func2和thread_func1必須在不同的堆棧上方可無錯誤地執行。
在一個父進程中fork-exec上述的程式MultiThread,然後用ptrace介面跟蹤之,在發送SIGUSR訊號給MultiThread並被父進程得知後,父進程交替使用PTRACE_GETREGS/PTRACE_SETREGS儲存並設定上述程式的寄存器環境,如此就可以交替執行thread_func1和thread_func2了。
上述代碼中注釋調用setjmp的語句,本來用setjmp/longjmp+signal可以很好的類比作業系統的多線程,可是jmp_buf儲存的context在調用函數返回後就會失效,而signal函數是在當前棧或者另分配的棧(使用sigaltstack)上執行的,無論哪種情況,最後都要調用sigreturn,因此在訊號處理函數中的setjmp是無效的,setjmp只針對當前棧幀有效,這裡的要點是,要想實現搶佔式多線程,棧的切換是必然的,棧的切換不能影響寄存器環境的儲存,因此必須使用ptrace等機制顯式的設定進程的寄存器上下文,我們之所以還是使用了訊號機制,那是因為訊號機制可以中斷進程並且通知ptrace進程,從而給ptrace進程修改MultiThread進程寄存器上下文從而類比多線程的機會。另外,線程的啟動也是一個要點,在一個執行緒的情況下,你幾乎不可能在當前的棧幀中啟動使用另一個棧的另一個線程,所有的基於馮諾依曼體系的機器本身都是單執行緒的,所謂的x86機器的多線程只是在進程這個層面的下層保留了一系列的上下文環境,然後不斷切換它們從而類比了多個線程,正如OS核心線程的建立及啟動需要底層系統調用一樣,使用者空間的多線程建立及啟動需要訊號機制(使用訊號僅僅是一個例子,也可以用別的),同樣的理由,在馮氏機器上實現使用者空間多線程必須藉助別的執行緒,比如ptrace的協助。
如果setjmp可以得到被中斷前的上下文,並且longjmp可以設定被中斷後的上下文,並且不影響全域變數的話,正如kernel的context_switch一樣,那麼MultiThread的interrput_func就會成為:
static int flag = 0; //flag標識執行緒是訊號處理進入的還是longjmp進入的。
void interrput_func (int sig)
{
flag = 1;
idx[1] = setjmp(env[1]); //注釋*
if (1 == flag) { //如果是正規的訊號處理則切換線程;
flag = 0; //設定全域變數,因為下面的longjmp之後,執行緒將從注釋*下面開始,由於已經切換了上下文,故到時將不再切換
longjmp(env[0], idx[0]);
}
//否則訊號返回,這個執行緒不是訊號處理進入的,而是longjmp進入的。
}