Linux CFS 進程調度算法

Linux主要實現了兩大類調度算法,CFS(完全公平調度算法)和實時調度算法。宏SCHED_NOMAL和SCHED_BATCH主要用於CFS調度,而SCHED_FIFO和SCHED_RR主要用於實時調度。這幾個宏的定義可以在include/linux/sched.h中找到。文件kernel/sched.c包含了內核調度器及相關係統調用的實現。調度的核心函數爲sched.c中的schedule(),schedule函數封裝了內核調度的框架。細節實現上調用具體的調度算法類中的函數實現,如kernel/sched_fair.c或kernel/sched_rt.c中的實現。
    1、時鐘tick中斷的處理

    在CFS中,當產生時鐘tick中斷時,sched.c中scheduler_tick()函數會被時鐘中斷(定時器timer的代碼)直接調用,我們調用它則是在禁用中斷時。注意在fork的代碼中,當修改父進程的時間片時,也會導致sched_tick的調用。sched_tick函數首先更新調度信息,然後調整當前進程在紅黑樹中的位置。調整完成後如果發現當前進程不再是最左邊的葉子,就標記need_resched標誌,中斷返回時就會調用scheduler()完成進程切換,否則當前進程繼續佔用CPU。注意這與以前的調度器不同,以前是tick中斷導致時間片遞減,當時間片被用完時才觸發優先級調整並重新調度。sched_tick函數的代碼如下:

  1. void scheduler_tick(void)  
  2. {  
  3.     int cpu = smp_processor_id();  
  4.     struct rq *rq = cpu_rq(cpu);  
  5.     struct task_struct *curr = rq->curr;  
  6.   
  7.     sched_clock_tick();  
  8.   
  9.     spin_lock(&rq->lock);  
  10.     update_rq_clock(rq);  
  11.     update_cpu_load(rq);  
  12.     curr->sched_class->task_tick(rq, curr, 0);  
  13.     spin_unlock(&rq->lock);  
  14.   
  15.     perf_event_task_tick(curr, cpu);  
  16.   
  17. #ifdef CONFIG_SMP  
  18.     rq->idle_at_tick = idle_cpu(cpu);  
  19.     trigger_load_balance(rq, cpu);  
  20. #endif  
  21. }  
    它先獲取目前CPU上的運行隊列中的當前運行進程,更新runqueue級變量clock,然後通過sched_class中的接口名task_tick,調用CFS的tick處理函數task_tick_fair(),以處理時鐘中斷。我們看kernel/sched_fair.c中的CFS算法實現。具體的調度類如下:

  1. static const struct sched_class fair_sched_class = {  
  2.     .next           = &idle_sched_class,  
  3.     .enqueue_task       = enqueue_task_fair,  
  4.     .dequeue_task       = dequeue_task_fair,  
  5.     .yield_task     = yield_task_fair,  
  6.   
  7.     .check_preempt_curr = check_preempt_wakeup,  
  8.   
  9.     .pick_next_task     = pick_next_task_fair,  
  10.     .put_prev_task      = put_prev_task_fair,  
  11.   
  12. #ifdef CONFIG_SMP  
  13.     .select_task_rq     = select_task_rq_fair,  
  14.   
  15.     .load_balance       = load_balance_fair,  
  16.     .move_one_task      = move_one_task_fair,  
  17.     .rq_online      = rq_online_fair,  
  18.     .rq_offline     = rq_offline_fair,  
  19.   
  20.     .task_waking        = task_waking_fair,  
  21. #endif  
  22.   
  23.     .set_curr_task          = set_curr_task_fair,  
  24.     .task_tick      = task_tick_fair,  
  25.     .task_fork      = task_fork_fair,  
  26.   
  27.     .prio_changed       = prio_changed_fair,  
  28.     .switched_to        = switched_to_fair,  
  29.   
  30.     .get_rr_interval    = get_rr_interval_fair,  
  31.   
  32. #ifdef CONFIG_FAIR_GROUP_SCHED  
  33.     .task_move_group    = task_move_group_fair,  
  34. #endif  
  35. };  
    task_tick_fair函數用於輪詢調度類的中一個進程。實現如下:

  1. static void task_tick_fair(struct rq *rq, struct task_struct *curr, int queued)  
  2. {  
  3.     struct cfs_rq *cfs_rq;  
  4.     struct sched_entity *se = &curr->se;  
  5.   
  6.     for_each_sched_entity(se) {  /* 考慮了組調度 */  
  7.         cfs_rq = cfs_rq_of(se);  
  8.         entity_tick(cfs_rq, se, queued);  
  9.     }  
  10. }  
    該函數獲取各層的調度實體,對每個調度實體獲取CFS運行隊列,調用entity_tick進程進行處理。kernel/sched_fair.c中的函數entity_tick源代碼如下:

  1. static void  
  2. entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr, int queued)  
  3. {  
  4.     /* 
  5.      * Update run-time statistics of the 'current'. 
  6.      */  
  7.     update_curr(cfs_rq);  
  8.   
  9. #ifdef CONFIG_SCHED_HRTICK  
  10.     /* 
  11.      * queued ticks are scheduled to match the slice, so don't bother 
  12.      * validating it and just reschedule. 
  13.      */  
  14.     if (queued) {  
  15.         resched_task(rq_of(cfs_rq)->curr);  
  16.         return;  
  17.     }  
  18.     /* 
  19.      * don't let the period tick interfere with the hrtick preemption 
  20.      */  
  21.     if (!sched_feat(DOUBLE_TICK) &&  
  22.             hrtimer_active(&rq_of(cfs_rq)->hrtick_timer))  
  23.         return;  
  24. #endif  
  25.   
  26.     if (cfs_rq->nr_running > 1 || !sched_feat(WAKEUP_PREEMPT))  
  27.         check_preempt_tick(cfs_rq, curr);  
  28. }  
    該函數用kernel/sched_fair.c:update_curr()更新當前進程的運行時統計信息,然後調用kernel/sched_fair.c:check_preempt_tick(),檢測是否需要重新調度,用下一個進程來搶佔當前進程。update_curr()實現記賬功能,由系統定時器週期調用,實現如下:
  1. static inline void  
  2. __update_curr(struct cfs_rq *cfs_rq, struct sched_entity *curr,  
  3.           unsigned long delta_exec)  
  4. {  
  5.     unsigned long delta_exec_weighted;  
  6.   
  7.     schedstat_set(curr->exec_max, max((u64)delta_exec, curr->exec_max));  
  8.   
  9.     curr->sum_exec_runtime += delta_exec; /* 總運行時間更新 */  
  10.     schedstat_add(cfs_rq, exec_clock, delta_exec); /* 更新cfs_rq的exec_clock */  
  11.     /* 用優先級和delta_exec來計算weighted,以用於更新vruntime */  
  12.     delta_exec_weighted = calc_delta_fair(delta_exec, curr);  
  13.   
  14.     curr->vruntime += delta_exec_weighted; /* 更新當前進程的vruntime */  
  15.     update_min_vruntime(cfs_rq);  
  16. }  
  17.   
  18. static void update_curr(struct cfs_rq *cfs_rq)  
  19. {  
  20.     struct sched_entity *curr = cfs_rq->curr;  
  21.     u64 now = rq_of(cfs_rq)->clock_task;  /* now計時器 */  
  22.     unsigned long delta_exec;  
  23.   
  24.     if (unlikely(!curr))  
  25.         return;  
  26.   
  27.     /* 
  28.      * 獲取從最後一次修改負載後當前進程所佔用的運行總時間, 
  29.      * 即計算當前進程的執行時間 
  30.      */  
  31.     delta_exec = (unsigned long)(now - curr->exec_start);  
  32.     if (!delta_exec)  /* 如果本次沒有執行過,不用重新更新了 */  
  33.         return;  
  34.     /* 根據當前可運行進程總數對運行時間進行加權計算 */  
  35.     __update_curr(cfs_rq, curr, delta_exec);  
  36.     curr->exec_start = now;  /* 將exec_start屬性置爲now */  
  37.   
  38.     if (entity_is_task(curr)) {  /* 下面爲關於組調度的 */  
  39.         struct task_struct *curtask = task_of(curr);  
  40.   
  41.         trace_sched_stat_runtime(curtask, delta_exec, curr->vruntime);  
  42.         cpuacct_charge(curtask, delta_exec);  
  43.         account_group_exec_runtime(curtask, delta_exec);  
  44.     }  
  45. }  
    這裏delta_exec獲得從最後一次修改負載後當前進程所佔用的運行總時間,即計算當前進程的執行時間。然後調用__update_curr()更新進程的vruntime。更新前需要計算weighted,這由sched_fair.c:calc_delta_fair()實現,如下:
  1. static inline unsigned long  
  2. calc_delta_fair(unsigned long delta, struct sched_entity *se)  
  3. {  
  4.     if (unlikely(se->load.weight != NICE_0_LOAD))  
  5.         delta = calc_delta_mine(delta, NICE_0_LOAD, &se->load);  
  6.   
  7.     return delta;  
  8. }  
    在calc_delta_fair中,如果進程的優先級爲0,那麼就是返回delta,如果不爲0,就要調用kernel/sched.c中的calc_delta_mine對delta值進行修正,如下:
  1. #if BITS_PER_LONG == 32  
  2. # define WMULT_CONST    (~0UL)  
  3. #else  
  4. # define WMULT_CONST    (1UL << 32)  
  5. #endif  
  6.   
  7. #define WMULT_SHIFT 32  
  8.   
  9. /* 
  10.  * Shift right and round: 
  11.  */  
  12. #define SRR(x, y) (((x) + (1UL << ((y) - 1))) >> (y))  
  13.   
  14. /* 
  15.  * delta *= weight / lw 
  16.  */  
  17. static unsigned long  
  18. calc_delta_mine(unsigned long delta_exec, unsigned long weight,  
  19.         struct load_weight *lw)  
  20. {  
  21.     u64 tmp;  
  22.   
  23.     if (!lw->inv_weight) {  
  24.         if (BITS_PER_LONG > 32 && unlikely(lw->weight >= WMULT_CONST))  
  25.             lw->inv_weight = 1;  
  26.         else  
  27.             lw->inv_weight = 1 + (WMULT_CONST-lw->weight/2)  
  28.                 / (lw->weight+1);  
  29.     }  
  30.   
  31.     tmp = (u64)delta_exec * weight;  
  32.     /* 
  33.      * Check whether we'd overflow the 64-bit multiplication: 
  34.      */  
  35.     if (unlikely(tmp > WMULT_CONST))  
  36.         tmp = SRR(SRR(tmp, WMULT_SHIFT/2) * lw->inv_weight,  
  37.             WMULT_SHIFT/2);  
  38.     else  
  39.         tmp = SRR(tmp * lw->inv_weight, WMULT_SHIFT);  
  40.   
  41.     return (unsigned long)min(tmp, (u64)(unsigned long)LONG_MAX);  
  42. }  
    CFS允許每個進程運行一段時間、循環輪轉、選擇運行最少的進程作爲下一個運行進程,而不再採用分配給每個進程時間片的做法了,CFS在所有可運行進程總數基礎上計算出一個進程應該運行多久,而不是依靠nice值來計算時間片。nice值在CFS中被作爲進程獲得的處理器運行比的權重,越高的nice值(越低的優先級)進程獲得更低的處理器使用權重,這是相對默認nice值進程的進程而言的;相反,更低的nice值(越高的優先級)的進程獲得更高的處理器使用權重。
    這裏delta的計算有如下關係: delta=delta* NICE_0_LOAD/se->load。se->load值是怎麼來的呢?可以跟蹤sys_nice(),就可以發現se->load其實就是表示nice對應的load值,nice越低,值越大。據此就可以得到一個結論,在執行相同時間的條件下(delta相同),高優先的進程計算出來的delta值會比低優先級的進程計算出來的低。應此高優先的進程就會位於rb_tree的左邊,在下次調度的時候就會優先調度。
    回到entity_tick,我們看check_preempt_tick()的實現,它用來檢測是否需要重新調度下一個進程。如下:

  1. static void  
  2. check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)  
  3. {  
  4.     unsigned long ideal_runtime, delta_exec;  
  5.   
  6.     ideal_runtime = sched_slice(cfs_rq, curr);  
  7.     delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;  
  8.     if (delta_exec > ideal_runtime) {  
  9.         resched_task(rq_of(cfs_rq)->curr);  
  10.         /* 
  11.          * The current task ran long enough, ensure it doesn't get 
  12.          * re-elected due to buddy favours. 
  13.          */  
  14.         clear_buddies(cfs_rq, curr);  
  15.         return;  
  16.     }  
  17.   
  18.     /* 
  19.      * Ensure that a task that missed wakeup preemption by a 
  20.      * narrow margin doesn't have to wait for a full slice. 
  21.      * This also mitigates buddy induced latencies under load. 
  22.      */  
  23.     if (!sched_feat(WAKEUP_PREEMPT))  
  24.         return;  
  25.   
  26.     if (delta_exec < sysctl_sched_min_granularity)  
  27.         return;  
  28.   
  29.     if (cfs_rq->nr_running > 1) { /* 用於組調度 */  
  30.         struct sched_entity *se = __pick_next_entity(cfs_rq);  
  31.         s64 delta = curr->vruntime - se->vruntime;  
  32.   
  33.         if (delta > ideal_runtime)  
  34.             resched_task(rq_of(cfs_rq)->curr);  
  35.     }  
  36. }  
    該函數先獲取當前進程的理想運行時間,如果當前執行時間超過理想時間,調用kernel/sched.c:resched_task()設置need_resched標誌,完成設置的函數爲resched_task()--->set_tsk_need_resched(p),表示需要重新調度進程。
    從上面分析可以看出,通過調用鏈sched_tick()--->task_tick_fair()--->entity_tick()--->[update_curr()--->__update_curr()--->calc_delta_fair()--->calc_delta_mine()] 和 [check_preempt_tick()--->resched_task()],最終會更新調度信息,設置need_resched調度標誌。當中斷返回時,就會調用schedule()進行搶佔式調度。
    2、CFS調度操作
    在sched_fair.c中,CFS實現了用紅黑樹對運行隊列進行管理的相關操作。

    (1)進程插入enqueue_task_fair:更新調度信息,調用enqueue_entity()--->__enqueue_entity()將調度實體插入到紅黑樹中。它會在nr_running遞增之前被調用。插入時,會找到右邊的空間並進行插入,然後緩存最左邊的節點。對於組調度,會對組中的所有進程進行操作。如下:

  1. static void __enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)  
  2. {  
  3.     struct rb_node **link = &cfs_rq->tasks_timeline.rb_node;  
  4.     struct rb_node *parent = NULL;  
  5.     struct sched_entity *entry;  
  6.     s64 key = entity_key(cfs_rq, se); /* key爲被插入進程的vruntime */  
  7.     int leftmost = 1;  
  8.   
  9.     /* 
  10.      * Find the right place in the rbtree: 
  11.      */  
  12.     while (*link) {  
  13.         parent = *link;  
  14.         entry = rb_entry(parent, struct sched_entity, run_node);  
  15.         /* 
  16.          * We dont care about collisions. Nodes with 
  17.          * the same key stay together. 
  18.          */  
  19.         if (key < entity_key(cfs_rq, entry)) {  
  20.             link = &parent->rb_left;  
  21.         } else {  
  22.             link = &parent->rb_right;  
  23.             leftmost = 0;  
  24.         }  
  25.     }  
  26.   
  27.     /* 
  28.      * Maintain a cache of leftmost tree entries (it is frequently 
  29.      * used): 
  30.      */  
  31.     if (leftmost)  
  32.         cfs_rq->rb_leftmost = &se->run_node;  
  33.   
  34.     rb_link_node(&se->run_node, parent, link);  
  35.     rb_insert_color(&se->run_node, &cfs_rq->tasks_timeline);  
  36. }  
    可見CFS的運行隊列布局是放在紅黑樹裏面的,而這顆紅黑樹的排序方式是按照運行實體的vruntime來的。vruntime的計算方式在上面已經做了分析。在前面“Linux進程管理”的幾節介紹中,我們可以看到fork()在創建子進程時最後就會調用enqueue_task_fair(),將新創建的進程插入到紅黑樹中。
    (2)進程選擇pick_next_task_fair:CFS調度算法的核心是選擇具有最小vruntine的任務。運行隊列採用紅黑樹方式存放,其中節點的鍵值便是可運行進程的虛擬運行時間。CFS調度器選取待運行的下一個進程,是所有進程中vruntime最小的那個,他對應的便是在樹中最左側的葉子節點。實現選擇的函數爲 pick_next_task_fair。如下:

  1. static struct task_struct *pick_next_task_fair(struct rq *rq)  
  2. {  
  3.     struct task_struct *p;  
  4.     struct cfs_rq *cfs_rq = &rq->cfs;  
  5.     struct sched_entity *se;  
  6.   
  7.     if (unlikely(!cfs_rq->nr_running))  
  8.         return NULL;  
  9.   
  10.     do {   /* 此循環爲了考慮組調度 */  
  11.         se = pick_next_entity(cfs_rq);  
  12.         set_next_entity(cfs_rq, se);  /* 設置爲當前運行進程 */  
  13.         cfs_rq = group_cfs_rq(se);  
  14.     } while (cfs_rq);  
  15.   
  16.     p = task_of(se);  
  17.     hrtick_start_fair(rq, p);  
  18.   
  19.     return p;  
  20. }  

    該函數調用pick_next_entity()--->__pick_next_entity()完成獲取下一個進程的工作,這個函數如下:

  1. static struct sched_entity *__pick_next_entity(struct cfs_rq *cfs_rq)  
  2. {  
  3.     struct rb_node *left = cfs_rq->rb_leftmost;  
  4.   
  5.     if (!left)  
  6.         return NULL;  
  7.   
  8.     return rb_entry(left, struct sched_entity, run_node);  
  9. }  
    該函數並不會遍歷紅黑樹來找到最左葉子節點(是所有進程中vruntime最小的那個),因爲該值已經緩存在rb_leftmost字段中。它通過rb_entry函數返回這個緩存的節點進程。完成實質工作的調用爲include/linux/rbtree.h:rb_entry()--->include/linux/kernel.h:container_of(),這是一個宏定義。
    (3)進程刪除dequeue_task_fair:從紅黑樹中刪除進程,並更新調度信息。它會在nr_running遞減之前被調用。完成實質工作的函數爲dequeue_entity()--->__dequeue_entity()。如下:

  1. static void __dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)  
  2. {  
  3.     if (cfs_rq->rb_leftmost == &se->run_node) {  
  4.         struct rb_node *next_node;  
  5.   
  6.         next_node = rb_next(&se->run_node);  
  7.         cfs_rq->rb_leftmost = next_node;  
  8.     }  
  9.   
  10.     rb_erase(&se->run_node, &cfs_rq->tasks_timeline);  
  11. }  
    該函數會刪除當前進程,並從紅黑樹中選出下一個具體最小vruntime值的節點,作爲新的最左邊節點緩存起來。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章