基于CFS算法的schedule()源码分析

内核中的调度算法在不断变化,2.4内核中的调度器是在全部的进程中选择优先级最高的进程,2.6内核前期的调度器是基于O(1)算法的,而 2.6.23版本以后的内核采用CFS调度算法,并同时对调度器进行了比较大的改善。内核主要是引入了调度器类来增长调度器的可扩展性。调度器类将各类调 度策略模块化,封装了对不一样调度策略的具体实现。算法

内核中对进程调度的方法有两种,其一为周期性调度器(generic scheduler),它对进行进行周期性的调度,以固定的频率运行;其二为主调度器(main scheduler),若是进程要进行睡眠或由于其余缘由主动放弃CPU,那么就直接调用主调度器。模块化

内核的主调度器是经过schedule()实现的,该函数的主要工做就是挑选下一个应该被调度的进程next。
该函数首先禁止内核抢占,而且依次获取当前CPU编号cpu、当前CPU对应的运行队列rq、当前进程的切换次数switch_count以及当前进程的描述符prev。函数

1 asmlinkage void __sched schedule(void)
2 {
3     struct task_struct *prev, *next;
4     unsigned long *switch_count;
5     struct rq *rq;
6     int cpu;
7  
8 need_resched:
9     preempt_disable();
10     cpu = smp_processor_id();
11     rq = cpu_rq(cpu);
12     rcu_sched_qs(cpu);
13     prev = rq->curr;
14     switch_count = &prev->nivcsw;
15  
16     release_kernel_lock(prev);
17 need_resched_nonpreemptible:
18  
19     schedule_debug(prev);
20  
21     if (sched_feat(HRTICK))
22         hrtick_clear(rq);

接下来经过update_rq_clock()更新就绪队列上的时钟,接着经过clear_tsk_need_resched()清除当前进程prev的从新调度标志TIF_NEED_RESCHED。post

1 raw_spin_lock_irq(&rq->lock);
2 update_rq_clock(rq);
3 clear_tsk_need_resched(prev);

若是当前进程是可中断睡眠状态(可运性状态TASK_RUNNING宏的值为0),但它却收到了某个唤醒它的信号,那么当前进程的标志被更新为TASK_RUNNING,等待再次被调度。不然,经过deactivate_task()将当前进程prev从就绪队列中删除。ui

这里的deactivate_task()根据调度类的不一样实现也有所不一样,但这些差别对主调度器是透明的,由于调度器类在各类调度实例和调度器之间起到了链接做用。该函数的核心语句即为:debug

1 p->sched_class->dequeue_task(rq, p, sleep);

sched_class是进程描述符中描述当前进程所属调度类的字段,经过这个字段回调钩子函数dequeue_task()。code

1 if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
2     if (unlikely(signal_pending_state(prev->state, prev)))
3         prev->state = TASK_RUNNING;
4     else
5         deactivate_task(rq, prev, 1);
6     switch_count = &prev->nvcsw;
7 }
8  
9 pre_schedule(rq, prev);
10  
11 if (unlikely(!rq->nr_running))
12     idle_balance(cpu, rq);

经过put_prev_task()将prev进程从新插入到就绪队列合适的位置中。再经过pick_next_task()在当前的就绪队列中挑选下一个应该被执行的进程next。这两个函数都属于调度器类中的钩子函数,它们的具体实现根据调度实例的不一样而不一样。队列

1 put_prev_task(rq, prev);
2 next = pick_next_task(rq);

有时候,调度器所选的下一个被执行的进程刚好就是当前进程,那么调度器就没必要耗费精力去执行上下文切换,但这种状况不是常常发生的。若是prev和 next不是同一个进程,那么先经过sched_info_switch()更新两个进程描述符的相关字段,而且更新可运行队列的相关字段。进程

接下来调用context_switch()进行prev和next两个进程的上下文切换,该函数由一段汇编代码组成。ip

1 if (likely(prev != next)) {
2     sched_info_switch(prev, next);
3     perf_event_task_sched_out(prev, next);
4  
5     rq->nr_switches++;
6     rq->curr = next;
7     ++*switch_count;
8  
9     context_switch(rq, prev, next); /* unlocks the rq */
10     /*
11      * the context switch might have flipped the stack from under
12      * us, hence refresh the local variables.
13      */
14     cpu = smp_processor_id();
15     rq = cpu_rq(cpu);
16 } else
17     raw_spin_unlock_irq(&rq->lock);

切换完毕后,当前的进程就是新选择的进程,它会开始执行。而被切换出去的进程从新运行时会从切换函数的下一条语句开始执行。

1     post_schedule(rq);
2  
3     if (unlikely(reacquire_kernel_lock(current) < 0)) {      prev = rq->curr;
4         switch_count = &prev->nivcsw;
5         goto need_resched_nonpreemptible;
6     }
7  
8     preempt_enable_no_resched();
9     if (need_resched())
10         goto need_resched;
11 }

根据上述对主调度器函数源码的分析,能够总结出主调度器的主要功能以下:

1.获取当前进程的描述符以及本地CPU的运行队列

2.将当前进程prev放入可运行队列中,等待下一次被从新调度

3.在当前的可运行队列中选取下一个被调度的新进程next

4.从当前进程切换到新进程

相关文章
相关标签/搜索