- 論壇徽章:
- 2
|
linux內(nèi)核進(jìn)程切換最重要的一個(gè)部分就是宏定義switch_to,下面從幾個(gè)方面來詳細(xì)講解一下:
(1)內(nèi)嵌匯編
(2)memory 破壞描述符(編譯器優(yōu)化)
(3)進(jìn)程切換的標(biāo)志是什么?
(4)堆棧切換的標(biāo)志是什么?
(5)為什么switch_to 提供了三個(gè)參數(shù)?
(6)匯編參數(shù)的傳遞?
帶著這幾個(gè)問題,先來大體瀏覽一下代碼
#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表示把變量放入內(nèi)存,即把[prev_sp]存儲(chǔ)的變量放入內(nèi)存,最后再寫入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 放入內(nèi)存中的[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)
以上代碼,主要是內(nèi)嵌匯編,這里先簡單介紹一下:
1 內(nèi)嵌匯編語法
__asm__ __violate__ ("movl %1,%0" : "=r" (result) : "m" (input));
__asm__ __violate__("指令模板" : 輸出部 : 輸入部);
“movl %1,%0”是指令模板;“%0”和“%1”代表指令的操作數(shù),稱為占位符,內(nèi)嵌匯
編靠它們將C 語言表達(dá)式與指令操作數(shù)相對(duì)應(yīng)。指令模板后面用小括號(hào)括起來的是C語言表
達(dá)式,本例中只有兩個(gè):“result”和“input”,他們按照出現(xiàn)的順序分別與指令操作數(shù)“%0”,
“%1”對(duì)應(yīng);注意對(duì)應(yīng)順序:第一個(gè)C 表達(dá)式對(duì)應(yīng)“%0”;第二個(gè)表達(dá)式對(duì)應(yīng)“%1”,依次類
推,操作數(shù)至多有10 個(gè),分別用“%0”,“%1”….“%9”表示。在每個(gè)操作數(shù)前面有一個(gè)
用引號(hào)括起來的字符串,字符串的內(nèi)容是對(duì)該操作數(shù)的限制或者說要求!皉esult”前面的
限制字符串是“=r”,其中“=”表示“result”是輸出操作數(shù),“r”表示需要將“result”
與某個(gè)通用寄存器相關(guān)聯(lián),先將操作數(shù)的值讀入寄存器,然后在指令中使用相應(yīng)寄存器,而
不是“result”本身,當(dāng)然指令執(zhí)行完后需要將寄存器中的值存入變量“result”,從表面
上看好像是指令直接對(duì)“result”進(jìn)行操作,實(shí)際上GCC做了隱式處理,這樣我們可以少寫
一些指令!癷nput”前面的“r”表示該表達(dá)式需要先放入某個(gè)寄存器,然后在指令中使用
該寄存器參加運(yùn)算。
(2)memory 破壞描述符(編譯器優(yōu)化)
內(nèi)存訪問速度遠(yuǎn)不及CPU處理速度,為提高機(jī)器整體性能,在硬件上引入硬件高速緩存
Cache,加速對(duì)內(nèi)存的訪問。另外在現(xiàn)代CPU中指令的執(zhí)行并不一定嚴(yán)格按照順序執(zhí)行,沒
有相關(guān)性的指令可以亂序執(zhí)行,以充分利用CPU的指令流水線,提高執(zhí)行速度。以上是硬件
級(jí)別的優(yōu)化。再看軟件一級(jí)的優(yōu)化:一種是在編寫代碼時(shí)由程序員優(yōu)化,另一種是由編譯器
進(jìn)行優(yōu)化。編譯器優(yōu)化常用的方法有:將內(nèi)存變量緩存到寄存器;調(diào)整指令順序充分利用
CPU指令流水線,常見的是重新排序讀寫指令。
對(duì)常規(guī)內(nèi)存進(jìn)行優(yōu)化的時(shí)候,這些優(yōu)化是透明的,而且效率很好。由編譯器優(yōu)化或者硬
件重新排序引起的問題的解決辦法是在從硬件(或者其他處理器)的角度看必須以特定順序
執(zhí)行的操作之間設(shè)置內(nèi)存屏障(memory barrier),linux 提供了一個(gè)宏解決編譯器的執(zhí)行
順序問題。
void Barrier(void)
這個(gè)函數(shù)通知編譯器插入一個(gè)內(nèi)存屏障,但對(duì)硬件無效,編譯后的代碼會(huì)把當(dāng)前CPU寄存器
中的所有修改過的數(shù)值存入內(nèi)存,需要這些數(shù)據(jù)的時(shí)候再重新從內(nèi)存中讀出。
Memory描述符告知GCC:
l 1)不要將該段內(nèi)嵌匯編指令與前面的指令重新排序;也就是在執(zhí)行內(nèi)嵌匯編代碼
之前,它前面的指令都執(zhí)行完畢
l 2)不要將變量緩存到寄存器,因?yàn)檫@段代碼可能會(huì)用到內(nèi)存變量,而這些內(nèi)存變
量會(huì)以不可預(yù)知的方式發(fā)生改變,因此GCC插入必要的代碼先將緩存到寄存器的變
量值寫回內(nèi)存,如果后面又訪問這些變量,需要重新訪問內(nèi)存。
如果匯編指令修改了內(nèi)存,但是GCC 本身卻察覺不到,因?yàn)樵谳敵霾糠譀]有描述,此時(shí)
就需要在修改描述部分增加“memory”,告訴GCC 內(nèi)存已經(jīng)被修改,GCC 得知這個(gè)信息后,
就會(huì)在這段指令之前,插入必要的指令將前面因?yàn)閮?yōu)化Cache 到寄存器中的變量值先寫回內(nèi)
存,如果以后又要使用這些變量再重新讀取。
(3)進(jìn)程切換的標(biāo)志-----sp指針的切換
因?yàn)檫M(jìn)程切換也就是進(jìn)程描述符的切換,現(xiàn)在讓我們來想一下我們是如何定位某個(gè)進(jìn)程描述符的地址的,看下面的匯編代碼:
mov $0xffffe000,%ecx
andl %esp,%ecx
movl %ecx,p
執(zhí)行上面代碼后,p中即存儲(chǔ)當(dāng)前運(yùn)行進(jìn)程的thread_info結(jié)構(gòu)的地址,但是我們最長用的是進(jìn)程描述符的地址,因此內(nèi)核設(shè)計(jì)了current宏來計(jì)算指向進(jìn)程描述符的指針:
mov $0xfffe000,%ecx
andl %esp,%ecx
movl (%ecx),p
因?yàn)閠ask字段在thread_info中的偏移量為0,所以執(zhí)行上述三條指令后,p即是當(dāng)前運(yùn)行的進(jìn)程的描述符指針。
我們可以看到,只要知道esp,那么,進(jìn)程就唯一確定了,所以說esp是進(jìn)程切換的標(biāo)志
(4)堆棧切換的標(biāo)志 --- ebp (棧低指針)
毋庸置疑,棧底指針肯定是堆棧切換的標(biāo)志
(5)switch_to 三個(gè)參數(shù)
進(jìn)程切換一般都涉及三個(gè)進(jìn)程,如進(jìn)程a切換成進(jìn)程b,b開始執(zhí)行,但是當(dāng)a恢復(fù)執(zhí)行的時(shí)候往往是通過一個(gè)進(jìn)程c,而不是進(jìn)程b。注意switch_to的調(diào)用: switch_to(prev,next,prev), 可以看到last就是prev 調(diào)用方法如下:進(jìn)程A->進(jìn)程B switch_to(A,B,A)主要有三個(gè)參數(shù):
輸入?yún)?shù)兩個(gè):prev:切換前的進(jìn)程,next:切換后的進(jìn)程,輸出參數(shù)一個(gè):last:切換前進(jìn)程。
注意這三個(gè)變量都是局部變量,在系統(tǒng)棧中,所以切換到另一進(jìn)程后變量的值不會(huì)改變。
進(jìn)程a切換b之前,eax的值為prev,也就是A;edx的值為next,也就是B,eax的值為last,也就是A
當(dāng)不考慮第三個(gè)參數(shù)時(shí),從C切換成A,內(nèi)核棧切換成A的棧,這時(shí)A中的prev和nexxt分別指向A和B,進(jìn)程C的引用丟失了。這時(shí)第三個(gè)參數(shù)就派上用場了。C切換進(jìn)程A后,將C存入eax中,切換到A后,由于輸出部"=a" (last)會(huì)將eax的值寫入last中,也就是prev中,所以此時(shí)prev和next的值就是C和B了。
(6)匯編參數(shù)的傳遞?
匯編是通過寄存器傳遞參數(shù)的,這里用了eax和edx,這樣jmp就和call差不多了,但是jmp和call的區(qū)別是,call會(huì)有硬件自動(dòng)壓棧一些寄存器的,比如cs:ip ,這里是通過手工壓棧ip,模擬了call,在__switch_to中,用return 返回。我們又會(huì)想到為什么不用call呢?原因是進(jìn)入__switch_to后,我們是為運(yùn)行別的進(jìn)程做準(zhǔn)備,也就是說當(dāng)返回的時(shí)候應(yīng)該是運(yùn)行next進(jìn)程而不是當(dāng)前進(jìn)程。如果用call的話,壓棧的是當(dāng)前進(jìn)程的ip,那么__switch_to返回后就運(yùn)行當(dāng)前進(jìn)程了,這與我們想運(yùn)行next的進(jìn)程的想法是不一致的,因此,我們手工壓棧的是next進(jìn)程的ip,那么,當(dāng)__switch_to返回后自然出棧的就是next的ip,也就是運(yùn)行next進(jìn)程了。 |
|