1966 /* Here we just switch the register state and the stack. */
1967 switch_to(prev, next, prev);
1968
1969 barrier();
linux内核进程调度器基于两个函数:周期性调度器函数和主调度器函数.
周期性调度器
所谓周期性调度器就是scheduler_tick中实现。如果系统正在活动中,内核会按照HZ自动调用这个函数。实际上在每个滴答的handler中会调用这个函数。如果在没有进程等待调度,那么在计算机电力供应不足的情况下,也可以关闭该调度器以减少电能消耗。该函数会激活负责当前进程的调度类的周期性调度方法。
if (curr != rq->idle) /* FIXME: needed? */
curr->sched_class->task_tick(rq, curr);
由于调度器的模块化结构,调度器本身实现比较简单,因为主要的工作完全可以委托给特定调度器类的方法。
task_tick的实现完全依赖于底层的调度器类。例如,CFS调度器类会在方法中检测进程是否已经运行太长时间,以避免过长的延迟。如果需要调度,那么会调用set_tsk_need_resched函数来设置TIF_NEED_RESCHED标志,以表示该请求。
主调度器
在内核的许多地方,如果需要将CPU分配给与当前活动进程不同的另一个进程,都会直接调用主调度器函数schedule。在从系统调用返回后,内核会检查当前进程是否设置了重新调度标志TIF_NEED_RESCHED标志(例如上面提到的 scheduler_tick就可能会设置这个标志),如果设置了,则调用schedule函数。
我们现在看一下schedule的实现
3616 /*
3617 * schedule() is the main scheduler function.
3618 */
3619 asmlinkage void __sched schedule(void)
3620 {
3621 struct task_struct *prev, *next;
3622 long *switch_count;
3623 struct rq *rq;
3624 int cpu;
3625
3626 need_resched:
3627 preempt_disable();
3628 cpu = smp_processor_id();
3629 rq = cpu_rq(cpu);
3630 rcu_qsctr_inc(cpu);
3631 prev = rq->curr;
3632 switch_count = &prev->nivcsw;
3630 先获得当前正在运行的进程,保存在prev中。
3639 /*
3640 * Do the rq-clock update outside the rq lock:
3641 */
3642 local_irq_disable();
3643 __update_rq_clock(rq);
3644 spin_lock(&rq->lock);
3645 clear_tsk_need_resched(prev);
3643 更新rq->prev_clock_raw和rq->clock
3645 清除TIF_NEED_SCHED标志
3647 if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
3648 if (unlikely((prev->state & TASK_INTERRUPTIBLE) &&
3649 unlikely(signal_pending(prev)))) {
3650 prev->state = TASK_RUNNING;
3651 } else {
3652 deactivate_task(rq, prev, 1);
3653 }
3654 switch_count = &prev->nvcsw;
3655 }
如果当前进程处在可中断睡眠状态,那么必须再次提升为运行进程。否则调用deactivate_task使得进程停止活动
3660 prev->sched_class->put_prev_task(rq, prev);
3661 next = pick_next_task(rq, prev);
调用调度器类的put_prev_task通知调度器类当前的进程要被另外一个进程取代。这个操作并不意味着prev从就绪队列移除,而是提供了一个时机,执行一些记账工作。
pick_next_task选择下一个应该执行的进程。注意新选择的进程有可能就是原来的进程,比如就绪队列中仅有一个进程的情况。
3665 if (likely(prev != next)) {
3666 rq->nr_switches++;
3667 rq->curr = next;
3668 ++*switch_count;
3669
3670 context_switch(rq, prev, next); /* unlocks the rq */
3671 } else
3672 spin_unlock_irq(&rq->lock);
如果新进程不等于旧的进程,那么我们调用context_switch进行进程上下文的切换。
上下文切换
1929 static inline void
1930 context_switch(struct rq *rq, struct task_struct *prev,
1931 struct task_struct *next)
1932 {
...............
1945 if (unlikely(!mm)) {
1946 next->active_mm = oldmm;
1947 atomic_inc(&oldmm->mm_count);
1948 enter_lazy_tlb(oldmm, next);
1949 } else
1950 switch_mm(oldmm, mm, next);
switch_mm是一个体系结构特定的函数,前换页全局目录以安装一个新的地址空间。对于arm平台来说,就是设置CP15协处理器TTB寄存器为新的pgd;对于X86来说,则是设置CR3寄存器为新的pgd。有人会问,这里切换了新的pgd,那么代码执行会不会不连续了? 没关系,因为现在是内核空间,内核地址空间的页面映射不会随着pgd而改变。
1966 /* Here we just switch the register state and the stack. */
1967 switch_to(prev, next, prev);
1968
1969 barrier();
1970 /*
1971 * this_rq must be evaluated again because prev may have moved
1972 * CPUs since it called schedule(), thus the 'rq' on its stack
1973 * frame will be invalid.
1974 */
1975 finish_task_switch(this_rq(), prev);
1976 }
进程切换的硬件上下文是共享的CPU 寄存器,进程在切换时,硬件上下文保存在task_struct->thread字段中,注意thread是体系结构特定的,所以不是每个体系结构都需要保存寄存器到这个结构中的。swtich之后的代码,只有在当前进程下一次被选择执行时才会执行。
barrier确保switch_to和后面的finish_task_switch不会乱序执行。