赵连讯 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
schedule
进程调度的关键函数是schedule函数,函数原型如下:
asmlinkage __visible void __sched schedule(void)
{
struct task_struct *tsk = current;
sched_submit_work(tsk);
__schedule();
}
当进程需要上下文切换的时候,此函数就会被执行。真正的函数实现是__schedule此函数。下面将依据老师的课程讲解,分析这个函数中的重点。
在schedule函数的实现前文,内核实现者给这个函数列出了这个函数被调用的几个场景。
/*
* __schedule() is the main scheduler function.
*
* The main means of driving the scheduler and thus entering this function are:
*
* 1. Explicit blocking: mutex, semaphore, waitqueue, etc.
*
* 2. TIF_NEED_RESCHED flag is checked on interrupt and userspace return
* paths. For example, see arch/x86/entry_64.S.
*
* To drive preemption between tasks, the scheduler sets the flag in timer
* interrupt handler scheduler_tick().
*
* 3. Wakeups don’t really cause entry into schedule(). They add a
* task to the run-queue and that’s it.
*
* Now, if the new task added to the run-queue preempts the current
* task, then the wakeup sets TIF_NEED_RESCHED and schedule() gets
* called on the nearest possible occasion:
*
* - If the kernel is preemptible (CONFIG_PREEMPT=y):
*
* - in syscall or exception context, at the next outmost
* preempt_enable(). (this might be as soon as the wake_up()’s
* spin_unlock()!)
*
* - in IRQ context, return from interrupt-handler to
* preemptible context
*
* - If the kernel is not preemptible (CONFIG_PREEMPT is not set)
* then at the next:
*
* - cond_resched() call
* - explicit schedule() call
* - return from syscall or exception to user-space
* - return from interrupt-handler to user-space
*/
函数体实现,选择下一个任务,这里面将会包含了使用何种策略,最终从进程的运行队列中选择出最应当执行一个进程出来。
next = pick_next_task(rq, prev);
进程上下文的切换函数如下:
context_switch(rq, prev, next); /* unlocks the rq */
context_switch中的关键语句是:
/* Here we just switch the register state and the stack. */切换寄存器的状态和堆栈
switch_to(prev, next, prev);
进程上下文切换
而swtich_to函数的实现则对应不同的架构有不同的定义。针对x86的平台,在文档arch/x86/include/asm/switch_to.h中定义。
在这个文档中,针对x86_32平台定义了switch_to的实现,不定义x86_32给出另外一个定义。
*/
#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 */ \
"movl %[next_sp],%%esp\n\t" /* restore ESP */ \
"movl $1f,%[prev_ip]\n\t" /* save EIP */ \
"pushl %[next_ip]\n\t" /* restore EIP */ \
__switch_canary \
"jmp __switch_to\n" /* regparm call */ \
"1:\t" \
"popl %%ebp\n\t" /* restore EBP */ \
"popfl\n" /* restore flags */ \
\
/* output parameters */ \
: [prev_sp] "=m" (prev->thread.sp), \
[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) \
\
__switch_canary_iparam \
\
: /* reloaded segment registers */ \
"memory"); \
} while (0)
关键语句解析:
/* output parameters */
: [prev_sp] "=m" (prev->thread.sp),
[prev_ip] "=m" (prev->thread.ip),
"=a" (last),
输出参数,第一个是当前进程的sp,是内核栈的栈顶位置将会被保存;当前进程的eip寄存器的值,将会保存到内存变量ip值中。
需要破坏的寄存器的值:
/* clobbered output registers: */
"=b" (ebx),
"=c" (ecx),
"=d" (edx),
"=S" (esi),
"=D" (edi)
上述的寄存器是我们在系统调用时传递参数的寄存器,也是通用的寄存器,进程切换完成之后他们的值将会被修改。
输出参数如下:
/* input parameters: */
: [next_sp] "m" (next->thread.sp),
[next_ip] "m" (next->thread.ip),
next->thread.sp是下一个进程的栈顶指针作为输入参数将会修改我们的栈顶寄存器的值。
next->thread.ip是下一个进程的ip值,将会被更新到eip寄存器中,靠ret命令将会被执行。
汇编代码的实现流程分析:
"pushfl\n\t" /* save flags */
"pushl %%ebp\n\t" /* save EBP */
"movl %%esp,%[prev_sp]\n\t" /* save ESP */
"movl %[next_sp],%%esp\n\t" /* restore ESP */
"movl $1f,%[prev_ip]\n\t" /* save EIP */
"pushl %[next_ip]\n\t" /* restore EIP */
__switch_canary
"jmp __switch_to\n" /* regparm call */
"1:\t"
"popl %%ebp\n\t" /* restore EBP */
"popfl\n" /* restore flags */
pushfl保存当前进程的flag
pushl %%ebp保存当前进程的ebp
movl %%esp,%[prev_sp]:把当前的栈顶保存到内存变量中。
movl %[next_sp],%%esp:把新的栈的栈顶更新入栈顶寄存器中。
movl $1f,%[prev_ip]:把1标号位置的语句保存到当前的内存变量ip中,表示我将来被切回的时候将会执行这一条语句。
pushl %[next_ip]:把新的ip值压栈,压入到新的栈的栈顶位置。虽然这里是将next_ip的保存了,但是接下来并不是和我们前面学得mykernel中的设置是不一样的,不是依靠ret将接下来要调用的语句弹出到eip寄存器中的。接下来而是直接jmp跳转_switch_to函数中执行。这个函数靠的是寄存器传递参数。
jmp __switch_to:跳转到switch_to中。对于fork新创建的子进程,我们调用的call ret_from_fork,这里是switch_to中。
当__swtich_to执行结束后,接下来执行1F处的指令。
“popl %%ebp\n\t” 弹出栈基地址的时候,此时弹出的栈基地址已经是新的栈基地址了
“popfl\n”弹出标志值,此时弹出的值也是新的进程的标志。
扩展思考
为什么ebp没有单独保存呢?ebp也保存了,只不过是保存在栈空间中,而不是保存在内存的变量中。为什么不保存在内存变量中呢?我认为可以保存内存变量中,只是这样的话,内存需要记录的信息会太多。但是为什么sp不保存在栈中。如果保存的话应该怎么保存呢?普通的理解可能代码如下:
push %esp
可是这句话是否真正的保存了esp呢?没有,因为push是先减去4,然后在将esp中的值存储esp中,那么存入的就不在是esp的原来的值而是被修改后的值。因为仅仅靠push,无法做到保存esp的值,所以使用内存变量来单独的存储esp。
在老师讲解的mykernel中内容,有新的进程创建也有进程的切换。他的实现将会和我们的fork和switch相互对应起来,既有相同的地方也有区别。