linux进程管理 - 核心调度器

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不会乱序执行。






你可能感兴趣的:(linux进程管理 - 核心调度器)