linux核心進程切換最重要的一個部分就是宏定義switch_to,下面從幾個方面來詳細講解一下:
(1)內嵌彙編
(2)memory 破壞描述符(編譯器最佳化)
(3)進程切換的標誌是什嗎?
(4)堆棧切換的標誌是什嗎?
(5)為什麼switch_to 提供了三個參數?
(6)彙編參數的傳遞?
帶著這幾個問題,先來大體瀏覽一下代碼
#define switch_to(prev, next, last) /
do { /
/* /
* Context-switching clobbers(徹底擊敗) all registers, so we clobber /
* them explicitly, via unused output variables. /
* (EAX and EBP is not listed because EBP is saved/restored /
* explicitly for wchan access and EAX is the return value of /
* __switch_to()) /
*/ /
unsigned long ebx, ecx, edx, esi, edi; /
/
asm volatile("pushfl/n/t" /* save flags */ /
"pushl %%ebp/n/t" /* save EBP */ /
"movl %%esp,%[prev_sp]/n/t" /* save ESP */ /
"movl %[next_sp],%%esp/n/t" /* restore ESP */ /
"movl $1f,%[prev_ip]/n/t" /* save EIP */ /
"pushl %[next_ip]/n/t" /* restore EIP */ /
"jmp __switch_to/n" /* regparm call */ /
"1:/t" /
"popl %%ebp/n/t" /* restore EBP */ /
"popfl/n" /* restore flags */ /
/
/* output parameters */ /
: [prev_sp] "=m" (prev->thread.sp), /
/*m表示把變數放入記憶體,即把[prev_sp]儲存的變數放入記憶體,最後再寫入prev->thread.sp*//
[prev_ip] "=m" (prev->thread.ip), /
"=a" (last), /
/*=表示輸出,a表示把變數last放入ax,eax = last*/ /
/
/* clobbered output registers: */ /
"=b" (ebx), "=c" (ecx), "=d" (edx), /
/*b 變數放入ebx,c表示放入ecx,d放入edx,S放入si,D放入edi*//
"=S" (esi), "=D" (edi) /
/
/* input parameters: */ /
: [next_sp] "m" (next->thread.sp), /
/*next->thread.sp 放入記憶體中的[next_sp]*//
[next_ip] "m" (next->thread.ip), /
/
/* regparm parameters for __switch_to(): */ /
[prev] "a" (prev), /
/*eax = prev edx = next*//
[next] "d" (next) /
/
: /* reloaded segment registers */ /
"memory"); /
} while (0)
以上代碼,主要是內嵌彙編,這裡先簡單介紹一下:
1 內嵌彙編文法
__asm__ __violate__ ("movl %1,%0" : "=r" (result) : "m" (input));
__asm__ __violate__("指令模板" : 輸出部 : 輸入部);
“movl %1,%0”是指令模板;“%0”和“%1”代表指令的運算元,稱為預留位置,內嵌匯
編靠它們將C 語言運算式與指令運算元相對應。指令模板後面用小括弧括起來的是C語言表
達式,本例中只有兩個:“result”和“input”,他們按照出現的順序分別與指令運算元“%0”,
“%1”對應;注意對應順序:第一個C 運算式對應“%0”;第二個運算式對應“%1”,依次類
推,運算元至多有10 個,分別用“%0”,“%1”….“%9”表示。在每個運算元前面有一個
用引號括起來的字串,字串的內容是對該運算元的限制或者說要求。“result”前面的
限制字串是“=r”,其中“=”表示“result”是輸出運算元,“r”表示需要將“result”
與某個通用寄存器相關聯,先將運算元的值讀入寄存器,然後在指令中使用相應寄存器,而
不是“result”本身,當然指令執行完後需要將寄存器中的值存入變數“result”,從表面
上看好像是指令直接對“result”進行操作,實際上GCC做了隱式處理,這樣我們可以少寫
一些指令。“input”前面的“r”表示該運算式需要先放入某個寄存器,然後在指令中使用
該寄存器參加運算。
(2)memory 破壞描述符(編譯器最佳化)
記憶體訪問速度遠不及CPU處理速度,為提高機器整體效能,在硬體上引入硬體快取
Cache,加速對記憶體的訪問。另外在現代CPU中指令的執行並不一定嚴格按照順序執行,沒
有相關性的指令可以亂序執行,以充分利用CPU的指令流水線,提高執行速度。以上是硬體
層級的最佳化。再看軟體一級的最佳化:一種是在編寫代碼時由程式員最佳化,另一種是由編譯器
進行最佳化。編譯器最佳化常用的方法有:將記憶體變數緩衝到寄存器;調整指令順序充分利用
CPU指令流水線,常見的是重新排序讀寫指令。
對常規記憶體進行最佳化的時候,這些最佳化是透明的,而且效率很好。由編譯器最佳化或者硬
件重新排序引起的問題的解決辦法是在從硬體(或者其他處理器)的角度看必須以特定順序
執行的操作之間設定記憶體屏障(memory barrier),linux 提供了一個宏解決編譯器的執行
順序問題。
void Barrier(void)
這個函數通知編譯器插入一個記憶體屏障,但對硬體無效,編譯後的代碼會把當前CPU寄存器
中的所有修改過的數值存入記憶體,需要這些資料的時候再重新從記憶體中讀出。
Memory描述符告知GCC:
l 1)不要將該段內嵌彙編指令與前面的指令重新排序;也就是在執行內嵌彙編代碼
之前,它前面的指令都執行完畢
l 2)不要將變數緩衝到寄存器,因為這段代碼可能會用到記憶體變數,而這些記憶體變
量會以不可預知的方式發生改變,因此GCC插入必要的代碼先將緩衝到寄存器的變
量值寫回記憶體,如果後面又訪問這些變數,需要重新訪問記憶體。
如果彙編指令修改了記憶體,但是GCC 本身卻察覺不到,因為在輸出部分沒有描述,此時
就需要在修改描述部分增加“memory”,告訴GCC 記憶體已經被修改,GCC 得知這個資訊後,
就會在這段指令之前,插入必要的指令將前面因為最佳化Cache 到寄存器中的變數值先寫回內
存,如果以後又要使用這些變數再重新讀取。
(3)進程切換的標誌-----sp指標的切換
因為進程切換也就是進程描述符的切換,現在讓我們來想一下我們是如何定位某個進程描述符的地址的,看下面的彙編代碼:
mov $0xffffe000,%ecx
andl %esp,%ecx
movl %ecx,p
執行上面代碼後,p中即儲存當前運行進程的thread_info結構的地址,但是我們最長用的是進程描述符的地址,因此核心設計了current宏來計算指向進程描述符的指標:
mov $0xfffe000,%ecx
andl %esp,%ecx
movl (%ecx),p
因為task欄位在thread_info中的位移量為0,所以執行上述三條指令後,p即是當前啟動並執行進程的描述符指標。
我們可以看到,只要知道esp,那麼,進程就唯一確定了,所以說esp是進程切換的標誌
(4)堆棧切換的標誌 --- ebp (棧低指標)
毋庸置疑,棧底指標肯定是堆棧切換的標誌
(5)switch_to 三個參數
進程切換一般都涉及三個進程,如進程a切換成進程b,b開始執行,但是當a恢複執行的時候往往是通過一個進程c,而不是進程b。注意switch_to的調用: switch_to(prev,next,prev), 可以看到last就是prev 調用方法如下:進程A->進程B switch_to(A,B,A)主要有三個參數:
輸入參數兩個:prev:切換前的進程,next:切換後的進程,輸出參數一個:last:切換前進程。
注意這三個變數都是局部變數,在系統棧中,所以切換到另一進程後變數的值不會改變。
進程a切換b之前,eax的值為prev,也就是A;edx的值為next,也就是B,eax的值為last,也就是A
當不考慮第三個參數時,從C切換成A,核心棧切換成A的棧,這時A中的prev和nexxt分別指向A和B,進程C的引用丟失了。這時第三個參數就派上用場了。C切換進程A後,將C存入eax中,切換到A後,由於輸出部"=a" (last)會將eax的值寫入last中,也就是prev中,所以此時prev和next的值就是C和B了。
(6)彙編參數的傳遞?
彙編是通過寄存器傳遞參數的,這裡用了eax和edx,這樣jmp就和call差不多了,但是jmp和call的區別是,call會有硬體自動壓棧一些寄存器的,比如cs:ip ,這裡是通過手工壓棧ip,類比了call,在__switch_to中,用return 返回。我們又會想到為什麼不用call呢?原因是進入__switch_to後,我們是為運行別的進程做準備,也就是說當返回的時候應該是運行next進程而不是當前進程。如果用call的話,壓棧的是當前進程的ip,那麼__switch_to返回後就運行當前進程了,這與我們想運行next的進程的想法是不一致的,因此,我們手工壓棧的是next進程的ip,那麼,當__switch_to返回後自然出棧的就是next的ip,也就是運行next進程了。