深入分析任務切換與堆棧

深入分析任務切換與堆棧 ————沒細看,只是感覺寫的很屌的樣子。。。。MARK

深入分析任務切換與堆棧 by Liu Wanli 

關鍵字:時間中斷、任務切換、堆棧、LINUX0.01 

引言: 

任務切換與堆棧的關係怎樣?很多朋友可能不知道她們之間有什麼關係,還有一些朋友可能認爲他們之間不會有太大的關係(文獻4)。而我認爲:任務切換跟堆棧有着密切的關係!下面是我對它們之間關係進行的探討,這裏的任務切換我指的是發生時間中斷時進行強制調度發生的任務切換,所以下面考慮堆棧時我是從中斷開始探討的。當然,我在進行這方面分析的時候,也愈感它們的複雜性,錯誤之處在所難免,望各位朋友多多指正。建議讀者水平:* * * 


一、時間中斷。 

假設一個進程在用戶空間執行時(這時CPL=3),發生了時間中斷。這時的中斷處理過程爲(文獻1:P438): 
1、根據中斷向量號找到中斷門描述符; 
2、從描述符中分解出選擇子、偏移量、屬性字段並進行相應的特權檢查; 
3、根據描述符類型轉入相應中斷處理程序中去執行。 
好象太膚淺了一些?再看看(文獻1:P439圖10.20): 
1、選擇子爲空?no繼續; 
2、取得對應描述符;(描述符中DPL屬性應該爲0,文獻3中斷向量初始化部分) 
3、存儲段描述符?yes繼續; 
4、非一致代碼段且DPL<CPL且段存在?yes繼續;根據假設CPL=3,DPL=0,所以到5! 
5、切換成內層堆棧! 
如何切換??因爲一個進程有用戶空間堆棧和系統空間(也叫內核空間)堆棧,用戶空間堆棧在哪兒我不管,它應該是由該進程的任務狀態段TSS中SS2指定,SS0指定系統空間堆棧,它和該進程任務結構task_struct共佔一頁空間(見文獻3:sched.c)。所以這裏的切換成內層堆棧應該是將該進程的TSS中SS0的值賦給SS寄存器。 
6、使RPL=0; 
7、把描述符裝入CS; 
8、入口偏移越界?no繼續; 
9、EFLAG、CS、EIP入棧;呵,開始棧的改變了喲! 
10、TF=0、NT=0、IF=0;這裏考慮的是中斷門。 
11、轉入處理程序。 

別急,先看看現在的堆棧情況: 

| 外層EIP | 
| 外層CS | 
| EFLAG | 
| 外層ESP | 
| 外層SS | 
----------- 
這個棧在什麼地方呢?這相當重要!這是在當初切換至內層堆棧時進行的,即已經到了當前進程的系統空間堆棧,也就是跟task_struct共佔一頁的那個堆棧。而這裏保存的就是該進程在用戶空間的堆棧和代碼信息,以便中斷完成後恢復進程執行。 

二、中斷處理程序。 
這裏指的是時間中斷。(文獻3:system_call.c: timer_interrupt:) 

timer_interrupt: 
1. push %ds 
2. push %es 
3. push %fs 
4. pushl %edx 
5. pushl %ecx 
6. pushl %ebx 
7. pushl %eax 
8. movl $0x10,%eax 
9. mov %ax,%ds 
10. mov %ax,%es 
11. movl $0x17,%eax 
12. mov %ax,%fs 
13. incl jiffies 
14. movb $0x20,%al 
15. outb %al,$0x20 
16. movl CS(%esp),%eax 
17. andl $3,%eax 
18. pushl %eax 
19. call do_timer 
20. andl $4,%esp 
21. jmp ret_from_sys_call 

1-7行爲壓棧操作,這是我們所關心的!16-18即是將CPL(CPL=CS&3)壓棧,目的是用於do_tiemr(long cpl)函數。那麼在執行到do_timer裏面時的堆棧怎麼樣呢?看看: 


|返回地址 | 
----------- 
| CPL | 
| eax | 
| ebx | 
| ecx | 
| edx | 
| fs | 
| es | 
| ds | 
----------- 
| 外層EIP | 
| 外層CS | 
| EFLAG | 
| 外層ESP | 
| 外層SS | 
----------- 

上面的返回地址當然就是調用do_timer後的那條語句,即20行的andl $4,%esp語句。那麼是不是do_timer函數執行完就返回到這兒呢,也是,當然要複雜得多,因爲在do_timer()函數中調用了schedule()並且發生了任務切換!哎,好麻煩,也不知道什麼時候才能返回到這兒來呢,還是一步一步來看吧。 

三、do_timer()(文獻3:sched.c: do_timer()) 

void do_timer(long cpl) 

... 
if ((--current->counter)>0) return; 
current->counter=0; 
if(!cpl)return; 
schedule(); 


省略號爲無關緊要的兩條語句,進行進程的計時。如果時間片沒有用完(counter>0)或CPL爲0,不發生調度直接返回,當然這裏也不是就直接返回到以前執行的進程空間,而是返回到do_timer()中,注意開始的返回地址,然後再通過iret指令從中斷處理返回到進程中去。當然,根據我們的假設,這兒CPL應該爲3,因爲是在用戶空間發生中斷的。我們要從最複雜的情況來討論這個問題。好了,就讓我們進入到中心點吧,請進schedule()。 

四、schedule()。 (文獻3:sched.c: schedule()) 

void schedule(void ) 

int next; 
... 
switch_to(next); 


呵,這裏我又省略了幾句代碼,它執行的是調度算法,即從所有狀態爲‘運行’的進程中找出下一個要執行的進程,然後將編號賦給next。進行切換! 

switch_to()是一個宏,它在(文獻3: sched.h)中定義: 

#define switch_to(n) { \ 
struct (long a,b;} __tmp; \ 
__asm__("cmpl %%ecx,current \n\t" \ 
"je 1f\n\t" \ 
"xchgl %%ecx, current\n\t" \ 
"movw %%dx, %1\n\t" \ 
"ljmp *%0\n\t" \ 
"cmpl %%ecx, %2\n\t" \ 
"jne 1f\n\t" \ 
"clts\n" \ 
"1:" \ 
::"m" (*&__tmp.a), "m" (*&__tmp.b), \ 
"m" (last_task_used_math),"d" _TSS(n), "c" ((long) task[n])); \ 


這是任務切換的關鍵代碼,原理是直接通過TSS來進行任務的切換(文獻1:P420)。那我就將這段關鍵代碼逐行解說一下吧。cmpl %%ecx, current,比較任務n是不是當前進程,如果是當然就不用切換了,直接結束schedule()。xchgl %%ecx,current,current指針指向任務n的任務結構,ecx寄存器保存當前進程的任務結構指針。movw %%dx, %1, 使__tmp.b=‘GDT中第n個任務的TSS選擇子’,注意_TSS(n)是求選擇子的宏!ljmp *%0,這句代碼就是真正的任務切換羅, AT&T語法的ljmp相當於INTEL的jmp far SECTION:OFFSET指令格式,它的絕對地址前加*號。這裏引用(文獻1:P420)一段話:當段間轉移指令JMP所含指針的選擇子指示一個可用任務狀態段TSS描述符時,正常情況下就發生從當前任務到由該可用任務的切換。目標任務的入口點由目標任務TSS內的CS和EIP字段所規定的指針確定,這樣的JMP指令內的偏移被丟棄。再具體的任務切換你也許得翻翻(文獻1:P421),這裏我只講有關堆棧的處理,那就是把寄存器現場保存到當前任務的TSS。把通用寄存器、段寄存器、EIP及EFLAGS的當前值保存到當前的TSS中。保存的EIP的值是返回地址,指向引起任務切換指令的下一條指令;恢復目標任務的寄存器現場,根據保存在TSS中的內容恢復各通用寄存器、段寄存器、EFLAG、EIP。好了,基本概念就引用這麼多,那麼,剛纔提到的進程馬上要被切換出去了,它保存TSS中EIP是什麼呢?顯然,根據剛纔的分析應該是cmpl %%ecx, %2這條指令。這意味着什麼呢?這就是說,如果下次這個任務要被切換成運行狀態時,它將從cmpl %%ecx, %2這條指令開始執行!那麼,由彼任務推到此任務,也就是說我們切換至任務next時,它也是從這條指令開始執行的!於是我們進入到任務next的堆棧空間,並開始執行,但由於任務next和當前的任務有着相同的堆棧路徑(這和LINUX中的內核控制路徑是不是一回事呢?),所以我們還是引用當前的堆棧來繼續分析。 
哦,有點糊塗了,好象是。休息一下,再參考一下(文獻2:上冊P373)。專家也是這樣說的;) 
要不,我們這麼理解,剛纔被中斷的進程發生了強制調度,且也發生了任務切換,只不過是切換到它自己,實際上不是喲。好吧,JMP成功,開始執行。 

五、轉折點,從schedule()返回。 

cmpl %%ecx, %2;jne 1f; clts;1: 這幾句是與協處理器有關,還有TS標誌,我們就直接到1:吧,開始從schedule()返回,注意switch_to()是宏,它在schedule()末端。返回到哪兒去了呢?跟蹤一下,看看上面的堆棧示意圖,返回地址就是調用do_timer後的那條語句, 
addl $4, %esp 
jmp ret_from_sys_call 
這兒esp加4就是把堆棧中的CPL去掉,因爲我們不用了,跳轉到ret_from_sys_call。哦,剩下的處理與系統調用返回共用代碼。 

六、ret_from_sys_call (文獻3,kernel/system_call.s) 
先看看我們的焦點,堆棧怎麼樣了呢? 

| eax | 
| ebx | 
| ecx | 
| edx | 
| fs | 
| es | 
| ds | 
----------- 
| 外層EIP | 
| 外層CS | 
| EFLAG | 
| 外層ESP | 
| 外層SS | 
----------- 

ret_from_sys_call: 
movel current, %eax 
cmpl task, %eax 
je 3f 
movl CS(%esp), %ebx 
testl $3, %ebx 
je 3f 
cmpw $0x17, OLDSS(%esp) 
jne 3f 
2: 
.... 
3: 
popl %eax 
popl %ebx 
popl %ecx 
popl %edx 
pop %fs 
pop %es 
pop %ds 
iret 

2標號處我省略了一些有關信號及其它一些處理。讓我們分析一下,如果當前任務是0號進程,或是任務先前的CPL爲3(即用戶態),或是任務先前的堆棧段爲LDT中指定的堆棧,JMP到3標號處。由先前的假設可知,此任務的CPL爲3,那就跳吧。把eax, ebc, ecx, edx, fs, es, ds寄存器從堆棧中恢復出來。 
現在堆棧如下: 

| 外層EIP | 
| 外層CS | 
| EFLAG | 
| 外層ESP | 
| 外層SS | 
----------- 
記得我們還有最後一條語句喲,iret。這條指令大家想必已經很熟悉了,它恢復EIP、CS、EFLAG、ESP、SS。記得不,這是不是已經恢復到了最初的時間中斷時進程被中斷的那一刻?恭喜!你終於可以繼續做你需要做的事情了!小心,還有下一個時間中斷,哦,你不怕?因爲它不會影響你的連貫性。 

結束語: 
我們走過了一段艱辛的歷程,但我們走的是一段近乎直線的路徑,並沒有分析到其它各個方面的情況。不過我相信,通過這段路程,會讓我們對於任務切換機制有一個更深入的認識。

發佈了17 篇原創文章 · 獲贊 5 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章