基於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.從當前進程切換到新進程

相關文章
相關標籤/搜索