2017-2018-1 20179215《Linux内核原理与分析》第九周作业
实验:理解进程调度时机跟踪分析进程调度与进程切换的过程
一、实验要求
(1)理解Linux系统中进程调度的时机,可以在内核代码中搜索schedule()函数,看都是哪里调用了schedule(),判断我们课程内容中的总结是否准确。
(2)使用gdb跟踪分析一个schedule()函数 ,验证您对Linux系统进程调度与进程切换过程的理解。
(3)特别关注并仔细分析switch_to中的汇编代码,理解进程上下文的切换机制,以及与中断上下文切换的关系。
二、实验过程
1、理解进程上下文的切换机制,以及与中断上下文切换的关系
(1)用户态进程它在用户的时候,它没法直接调用schedule(),因为schedule是个内核函数,而且它也不是一个系统调用,没法直接调用它,只能间接的调用它,间接的调用schedule()的时机就是中断处理过程.对于用户态进程,它要从当前运行中的进程切换出去的话,那么它就必须要进入中断,这个中断是一般中断,进入中断后才会有一个可能会发生进程调度的时机,所以一般的用户态进程只能被动调度。eg正在运行的用户态进程X切换到运行用户态进程Y的过程:
正在运行的用户态进程X
①发生中断——save cs:eip/esp/eflags(current) to kernel stack,then load cs:eip(entry of a specific ISR) and ss:esp(point to kernel stack).
②SAVE_ALL //保存现场
③中断处理过程中或中断返回前调用了schedule(),其中的switch_to做了关键的进程上下文切换
④标号1之后开始运行用户态进程Y(这里Y曾经通过以上步骤被切换出去过因此可以从标号1继续执行)
⑤restore_all //恢复现场
⑥iret - pop cs:eip/ss:esp/eflags from kernel stack
继续运行用户态进程Y
(2)[内核线程]可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度;用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。
2、进程调度的时机
- 中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule();
- 内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度;
- 用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。
3、进程的切换
-
为了控制进程的执行,内核必须有能力挂起正在CPU上执行的进程,并恢复以前挂起的某个进程的执行,这叫做进程切换、任务切换、上下文切换;
-
挂起正在CPU上执行的进程,与中断时保存现场是不同的,中断前后是在同一个进程上下文中,只是由用户态转向内核态执行;
-
进程上下文包含了进程执行需要的所有信息
-
用户地址空间:包括程序代码,数据,用户堆栈等
-
控制信息:进程描述符,内核堆栈等
-
硬件上下文(注意中断也要保存硬件上下文只是保存的方法不同)
-
schedule()函数选择一个新的进程来运行,并调用context_switch进行上下文的切换,这个宏调用switch_to来进行关键上下文切换
-
switch_to利用了prev和next两个参数:prev指向当前进程,next指向被调度的进程
-
4.关键代码分析
(1)schedule
asmlinkage __visible
void __sched schedule(void)
{
struct task_struct *tsk = current;//来获取当前进程
sched_submit_work(tsk);//避免死锁
__schedule();//处理切换过程
}
static void __sched __schedule(void)
{
...
next = pick_next_task(rq, prev);
if (likely(prev != next)) {
...
context_switch(rq, prev, next); /* unlocks the rq */
...
} else {
...
raw_spin_unlock_irq(&rq->lock);
...
}
...
post_schedule(rq);
...
}
我们根据调度策略在运行队列rq中拿出prev进程的下一个进程,如果next进程和prev不是同一个进程,则进行进程的切换并释放自旋锁,否则直接释放自旋锁。
(2)switch_to
asm volatile("pushfl\n\t" // 保存当前进程的标志位
"pushl %%ebp\n\t" // 当前进程的基址压栈保存EBP
"movl %%esp,%[prev_sp]\n\t" // 保存ESP 把当前的内核堆栈栈顶保存下
"movl %[next_sp],%%esp\n\t" // 恢复 ESP
//整体这两步是完成内核堆栈的切换
"movl $1f,%[prev_ip]\n\t" // 保存 EIP
"pushl %[next_ip]\n\t" //恢复 EIP
__switch_canary
"jmp __switch_to\n" //跳转l
"1:\t"
"popl %%ebp\n\t" //恢复 EBP
"popfl\n" //恢复标志位
/* 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");
先看当前进程(prev):首先保存当前进程的flags,push ebp,然后把EIP置为标号1,等到当前进程(prev)下一次再开始执行时(被__ switch_to切出来),内核堆栈被恢复了以后,刚好会从pop ebp开始执行(和前面的push ebp相对应),即恢复原来的堆栈状态。
再看下一个进程(next): 这个进程即将上CPU,是被jmp __ switch_ to 切换出来的进程,由于这里使用的是jmp指令而不是call指令,之前又手工压栈了EIP,所以__ switch_ to会返回到next_ ip的地方开始执行,这样就完成了进程的切换过程。 如果只有两个进程的话,那么下一次prev就变成了next,next变成prev。
课本笔记