linux 的schedule函數


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也是配套的。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM