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