1.Linux系统中进程调度的时机,schedule()函数
进程调度的时机主要有以下几种情况:
1).进程状态的转换,如运行态进入终止,睡眠态,进程通过调用函数exit(),sleep()等函数,再通过进程调度程序schedule()完成进程状态的转换。
2).当前进程的时间片结束(current->counter=0)
3).设备驱动程序,当设备驱动程序执行长而重复的任务时,直接调用调度程序。在每次反复循环中,驱动程序都检查need_resched的值,如果必要,则调用调度程序schedule()主动放弃CPU。
4).进程从中断、异常及系统调用返回到用户态时,不管是从中断、异常还是系统调用返回,最终都调用ret_from_sys_call(),由这个函数进行调度标志的检测。
schedule():进程调度函数,由它来完成进程的调度。通过检查检测 need_resched 标志,如果此标志为非0,那么就转到reschedule处调用调度程序schedule()进行进程的选择。调度程序schedule()会根据具体的标准在运行队列中选择下一个应该运行的进程。当从调度程序返回时,如果发现又有调度标志被设置,则又调用调度程序,直到调度标志为0,这时,从调度程序返回时由RESTORE_ALL恢复被选定进程的环境,返回到被选定进程的用户空间,使之得到运行。schedule()函数被定义在usr/include/linux/sched.h中,它的实现代码如下,
5asmlinkage __visible void __sched schedule(void)
{
struct task_struct *tsk = current; //获取当前进程的状态
sched_submit_work(tsk); //用以提交记录当前进程的状态。
__schedule(); //开始调度
}
EXPORT_SYMBOL(schedule);
调度算法在__shedule()函数中,通过检查need_resched标志位,判断是否继续执行。此时会进行大量的工作,比如获取进程的优先级等。其中
调用context_switch进行上下文的切换是关键步骤,context_switch通过调用宏switch_to进行上下文切换工作。
进程调度算法封装函数如下:
pick_next_task(struct rq *rq, struct task_struct *prev)
{
const struct sched_class *class = &fair_sched_class;
struct task_struct *p;
/*
* Optimization: we know that if all tasks are in
* the fair class we can call that function directly:
*/
if (likely(prev->sched_class == class &&
rq->nr_running == rq->cfs.h_nr_running)) { //检查进程运行队列
p = fair_sched_class.pick_next_task(rq, prev); //队列中下一个进程
if (unlikely(p == RETRY_TASK))
goto again;
/* assumes fair_sched_class->next == idle_sched_class */
if (unlikely(!p))
p = idle_sched_class.pick_next_task(rq, prev); //空闲进程
return p;
}
again:
for_each_class(class) {
p = class->pick_next_task(rq, prev);
if (p) {
if (unlikely(p == RETRY_TASK))
goto again;
return p;
}
}
BUG(); /* the idle class will always have a runnable task */
}
处理流程next = pick_next_task(rq, prev);->context_switch(rq, prev, next)->switch_to
2.分析switch_to中的汇编代码,进程上下文的切换机制进程上下文切换:context_switch(rq, prev, next),它通过switch_to调用了prev和next两个参数:prev指向当前进程,next指向被调度的进程。
#define switch_to(prev, next, last) \
32do { \
33 /* \
34 * Context-switching clobbers all registers, so we clobber \
35 * them explicitly, via unused output variables. \
36 * (EAX and EBP is not listed because EBP is saved/restored \
37 * explicitly for wchan access and EAX is the return value of \
38 * __switch_to()) \
39 */ \
40 unsigned long ebx, ecx, edx, esi, edi; \
41 \
42 asm volatile("pushfl\n\t" /* save flags */ \ //保护标志位
43 "pushl %%ebp\n\t" /* save EBP */ \ //栈基址
44 "movl %%esp,%[prev_sp]\n\t" /* save ESP */ \
45 "movl %[next_sp],%%esp\n\t" /* restore ESP */ \
46 "movl $1f,%[prev_ip]\n\t" /* save EIP */ \
47 "pushl %[next_ip]\n\t" /* restore EIP */ \
48 __switch_canary \
49 "jmp __switch_to\n" /* regparm call */ \
50 "1:\t" \
51 "popl %%ebp\n\t" /* restore EBP */ \
52 "popfl\n" /* restore flags */ \
53 \
54 /* output parameters */ \
55 : [prev_sp] "=m" (prev->thread.sp), \
56 [prev_ip] "=m" (prev->thread.ip), \
57 "=a" (last), \
58 \
59 /* clobbered output registers: */ \ //各个寄存器的值
60 "=b" (ebx), "=c" (ecx), "=d" (edx), \
61 "=S" (esi), "=D" (edi) \
62 \
63 __switch_canary_oparam \
64 \
65 /* input parameters: */ \
66 : [next_sp] "m" (next->thread.sp), \
67 [next_ip] "m" (next->thread.ip), \
68 \
69 /* regparm parameters for __switch_to(): */ \
70 [prev] "a" (prev), \ //当前进程
71 [next] "d" (next) \ //下一条进程
72 \
73 __switch_canary_iparam \
74 \
75 : /* reloaded segment registers */ \
76 "memory"); \
77} while (0)
78
79#else /* CONFIG_X86_32 */
宏switch_to()内部做了很多压栈的工作,目的是保护现场,以待后续进程恢复现场restore_all。
context_switch()调用switch_to()执行prev和next之间的进程切换了:switch_to(prev,next,prev);
return prev;
进程切换后schedule()所执行的操作
schedule( ) 函数中在switch_to 宏之后紧接着的指令并不由next 进程立即执行,而是稍后当调度程序选择prev 又执行时由prev 执行。
最一般的情况:正在运行的用户态进程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
3.使用gdb跟踪分析一个schedule()函数
1).重新克隆一份menu文件,打开调试窗口,如下图:
2)..打开gdb调试窗口,在schedule处设置断点:
3.)在pick_next_task处设置断点
由于switch_to是宏而不是函数,因此在gdb中不能追踪到,但是它的执行过程是清晰地。
张伟+《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000