casualet + 原創作品轉載請注明出處 + 《Linux內核分析》MOOC課程http://mooc.study.163.com/course/USTC-1000029000
linux中的schedule函數負責完成進程調度,本文將分析schedule相關的機制,並通過調試運行的方式來補充說明。
我們考慮linux系統中的fork系統調用。fork會創建一個新的進程,加載文件並進行執行。在這個過程中,涉及到了兩個進程之間的切換。我們依然使用前一篇文章的環境,對fork系統調用進行調試,來完成這個分析。當我們調用fork函數的時候,產生了軟中斷,通過int 0x80陷入內核,進入sys_call函數,並且通過eax傳遞系統調用號參數。然后,通過sys_call_table來尋找相應的系統調用函數進行執行。在這里是sys_clone。這個是我們的地一個斷點。
從這里開始,我們進入sys_clone中的do_fork,進行了之前講過的fork執行流程,包括copy_process,copy_thread等,主要的工作是創建PCB,復制內核棧等相關內容,並且對子進程的特殊的地方進行定制。我們設置下一個斷點是schedule。在完成do_fork以后,會調用schedule進行進程調度。schedule的部分代碼如下:
static void __sched __schedule(void){
struct task_struct *prev, *next; unsigned long *switch_count; struct rq *rq; int cpu;
.......
next = pick_next_task(rq,prev);//進程調度的算法實現
........
context_switch(rq,prev,next);//進程上下文切換
對於上下文切換,有如下的代碼:
static inline void context_switch(struct rq *rq, struct task_struct *prev, struct task_struct *next){ struct mm_struct *mm, *oldmm; prepare_task_switch(rq, prev, next); mm = next->mm;
switch_to(prev,next,prev);//切換寄存器的狀態和堆棧的切換
我們主要來看switch_to函數:
#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 */ \//保存到了output里面,棧頂里面. "movl %[next_sp],%%esp\n\t" /* restore ESP */ \//完成了內核堆棧的切換!!接下來就是在另外一個進程的棧空間了 "movl $1f,%[prev_ip]\n\t" /* save EIP */ \//當前進程的eip保持 "pushl %[next_ip]\n\t" /* restore EIP */\ 把ip放到了內核堆棧中,后面其實直接itrt就可以開始執行了,但這里使用的是__switch_to __switch_canary \ "jmp __switch_to\n" /* regparm call */ \//jmp的函數完成以后,需要iret,把ip彈出來了,這樣就到了下一行代碼執行.這里jmp 到switch_to做了什么工作? "1:\t" \//新設置的IP是從這里開始的,也就是movl $1f,從這里開始就說明是另外一個進程了。所以內核堆棧先切換好,執行了兩句,用的是新的進程的內核堆棧,但是確是在原來的進程的ip繼續執行。 "popl %%ebp\n\t" /* restore EBP */ \ "popfl\n" /* restore flags */ \ //原來的進程切換的時候,曾經設置過save ebp和save flags,所以這里就需要pop來恢復 \ /* output parameters */ \ : [prev_sp] "=m" (prev->thread.sp), \ //分別表示內核堆棧以及當前進程的eip [prev_ip] "=m" (prev->thread.ip), \ "=a" (last), \ \ /* clobbered output registers: */ \ "=b" (ebx), "=c" (ecx), "=d" (edx), \ "=S" (esi), "=D" (edi) \ \ __switch_canary_oparam \ \ /* input parameters: */ \ : [next_sp] "m" (next->thread.sp), \ [next_ip] "m" (next->thread.ip), \ //下一個進程的執行起點以及內核堆棧 \ /* regparm parameters for __switch_to(): */ \ [prev] "a" (prev), \ [next] "d" (next) \ //用a和d來傳遞參數... \ __switch_canary_iparam \ \ : /* reloaded segment registers */ \ "memory"); \ } while (0)
由於switch_to gdb不支持單步調試,我們僅對其源碼進行觀察分析。首先是pushfl \n\t等語句,用來在當前的棧中保持flags,以及當前的ebp,准備進行進程的切換。然后是當前的esp,會保存在當前進程的thread結構體中。其中的movl $1f,%[prev_ip]則是保存當前進程的ip為代碼中標號1的位置。然后是resotre ebp和flags的語句,用於恢復ebp和flags到寄存器中,這些值是保持在內核棧中的。這樣,對於新的進程,我們使用c繼續執行,就可以走到ret_from_fork中了。
總結:
對於進程的切換,主要有兩部分需要理解,一個是進程切換的時機,一個是schedule函數的調用過程。對於進程切換的時機,中斷處理以后是其中一個時機,內核線程也可以進程schedule函數的調用,來完成進程的切換。對於schedule函數。大致的過程是: 首先,進程x在執行,假設執行的ip是 ip_prev. 進入中斷處理,以及進程切換的時機以后,先保持自己的內核棧信息,包括flags以及ebp,自己的ip會保存在thread結構體中。這里的ip設置成了標號1的位置。也就是,如果進程切換了,下次回到這個進程的時候,會執行標號1的位置開始執行,回復flags以及ebp。所以這里的保持flags/ebp 和恢復是對應。對於新的進程開始執行的位置,如果是像fork這樣的創建的新進程,從thread.ip中取出來的就是ret_from_fork,如果是之前運行過的進程,就如上面說的,進入標號1的位置開始執行。這里的的保持ip和恢復ip也是配套的。