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

 

posted @ 2016-04-14 11:56  Casualet  阅读(10287)  评论(0编辑  收藏  举报