1966 /* Here we just switch the register state and the stack. */ 1967 switch_to(prev, next, prev); 1968 1969 barrier();
周期性调度器
所谓周期性调度器就是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不会乱序执行。