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