天天看點

Linux switch_to()深入分析

深入分析任務切換與堆棧 by Liu Wanli    下面可以直接連結文章出處:

http://www.linuxforum.net/forum/showflat.php?Cat=&Board=linuxK&Number=301272&fpart=all

關鍵字:時間中斷、任務切換、堆棧、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。記得不,這是不是已經恢複到了最初的時間中斷時程序被中斷的那一刻?恭喜!你終于可以繼續做你需要做的事情了!小心,還有下一個時間中斷,哦,你不怕?因為它不會影響你的連貫性。

繼續閱讀