Linux CFS調度器之喚醒搶佔--Linux進程的管理與調度(三十)

我們也講解了CFS的很多進程操作

table th:nth-of-type(1){ width: 20%; } table th:nth-of-type(2){ width: 20% ; }

信息

函數

描述

進程入隊/出隊

enqueue_task_fair/dequeue_task_fair

向CFS的就讀隊列中添加刪除進程

選擇最優進程(主調度器)

pick_next_task_fair

主調度器會按照如下順序調度 schedule -> __schedule -> 全局pick_next_task全局的pick_next_task函數會從按照優先級遍歷所有調度器類的pick_next_task函數, 去查找最優的那個進程, 當然因爲大多數情況下, 系統中全是CFS調度的非實時進程, 因而linux內核也有一些優化的策略 一般情況下選擇紅黑樹中的最左進程left作爲最優進程完成調度, 如果選出的進程正好是cfs_rq->skip需要跳過調度的那個進程, 則可能需要再檢查紅黑樹的次左進程second, 同時由於curr進程不在紅黑樹中, 它可能比較飢渴, 將選擇出進程的與curr進程進行擇優選取, 同樣last進程和next進程由於剛被喚醒, 可能比較飢餓, 優先調度他們能提高系統緩存的命中率

週期性調度

task_tick_fair

週期性調度器的工作由scheduler_tick函數完成, 在scheduler_tick中週期性調度器通過調用curr進程所屬調度器類sched_class的task_tick函數完成週期性調度的工作 而entity_tick中則通過check_preempt_tick函數檢查是否需要搶佔當前進程curr, 如果發現curr進程已經運行了足夠長的時間, 其他進程已經開始飢餓, 那麼我們就需要通過resched_curr函數來設置重調度標識TIF_NEED_RESCHED, 此標誌會提示系統在合適的時間進行調度

下面我們到了最後一道工序, 完全公平調度器如何處理一個新創建的進程, 該工作由task_fork_fair函數來完成

1. 處理新進程

我們對完全公平調度器需要考慮的最後一個操作, 創建新進程時的處理函數:task_fork_fair(早期的內核中對應是task_new_fair, 參見LKML-sched: Sanitize fork() handling

1.1 place_entity設置新進程的虛擬運行時間

該函數先用update_curr進行通常的統計量更新, 然後調用此前討論過的place_entity設置調度實體se的虛擬運行時間

 /*  更新統計量  */
    update_curr(cfs_rq);

    if (curr)
        se->vruntime = curr->vruntime;
    /*  調整調度實體se的虛擬運行時間  */
    place_entity(cfs_rq, se, 1);

我們可以看到, 此時調用place_entity時的initial參數設置爲1, 以便用sched_vslice_add計算初始的虛擬運行時間vruntime, 內核以這種方式確定了進程在延遲週期中所佔的時間份額, 並轉換成虛擬運行時間. 這個是調度器最初向進程欠下的債務.

關於place_entity函數, 我們之前在講解CFS隊列操作的時候已經講的很詳細了 參見linux進程管理與調度之CFS入隊出隊操作 設想一下子如果休眠進程的vruntime保持不變, 而其他運行進程的 vruntime一直在推進, 那麼等到休眠進程終於喚醒的時候, 它的vruntime比別人小很多, 會使它獲得長時間搶佔CPU的優勢, 其他進程就要餓死了. 這顯然是另一種形式的不公平,因此CFS是這樣做的:在休眠進程被喚醒時重新設置vruntime值,以min_vruntime值爲基礎,給予一定的補償,但不能補償太多. 這個重新設置其虛擬運行時間的工作就是就是通過place_entity來完成的, 另外新進程創建完成後, 也是通過place_entity完成其虛擬運行時間vruntime的設置的. 其中place_entity函數通過第三個參數initial參數來標識新進程創建和進程睡眠後甦醒兩種情況的 在進程入隊時enqueue_entity設置的initial參數爲0, 參見kernel/sched/fair.c, line 3207 在task_fork_fair時設置的initial參數爲1, 參見kernel/sched/fair.c, line 8167

1.3 sysctl_sched_child_runs_first控制子進程運行時機

接下來可使用參數sysctl_sched_child_runs_first控制新建子進程是否應該在父進程之前運行. 這通常是有益的, 特別在子進程隨後會執行exec系統調用的情況下. 該參數的默認設置是1, 但可以通過/proc/sys/kernel/sched_child_first修改, 代碼如下所示

 /*  如果設置了sysctl_sched_child_runs_first期望se進程先運行
     *  但是se進行的虛擬運行時間卻大於當前進程curr
     *  此時我們需要保證se的entity_key小於curr, 才能保證se先運行
     *  內核此處是通過swap(curr, se)的虛擬運行時間來完成的  */
    if (sysctl_sched_child_runs_first && curr && entity_before(curr, se))
    {
        /*
         * Upon rescheduling, sched_class::put_prev_task() will place
         * 'current' within the tree based on its new key value.
         */
        /*  由於curr的vruntime較小, 爲了使se先運行, 交換兩者的vruntime  */
        swap(curr->vruntime, se->vruntime);
        /*  設置重調度標識, 通知內核在合適的時間進行進程調度  */
        resched_curr(rq);
    }

如果entity_before(curr, se), 則父進程curr的虛擬運行時間vruntime小於子進程se的虛擬運行時間, 即在紅黑樹中父進程curr更靠左(前), 這就意味着父進程將在子進程之前被調度. 這種情況下如果設置了sysctl_sched_child_runs_first標識, 這時候我們必須採取策略保證子進程先運行, 可以通過交換curlr和se的vruntime值, 來保證se進程(子進程)的vruntime小於curr.

1.4 適應遷移的vruntime值

在task_fork_fair函數的最後, 使用了一個小技巧, 通過place_entity計算出的基準虛擬運行時間, 減去了運行隊列的min_vruntime.

    se->vruntime -= cfs_rq->min_vruntime;

我們前面講解place_entity的時候說到, 新創建的進程和睡眠後甦醒的進程爲了保證他們的vruntime與系統中進程的vruntime差距不會太大, 會使用place_entity來設置其虛擬運行時間vruntime, 在place_entity中重新設置vruntime值,以cfs_rq->min_vruntime值爲基礎,給予一定的補償,但不能補償太多.這樣由於休眠進程在喚醒時或者新進程創建完成後會獲得vruntime的補償,所以它在醒來和創建後有能力搶佔CPU是大概率事件,這也是CFS調度算法的本意,即保證交互式進程的響應速度,因爲交互式進程等待用戶輸入會頻繁休眠

但是這樣子也會有一個問題, 我們是以某個cfs就緒隊列的min_vruntime值爲基礎來設定的, 在多CPU的系統上,不同的CPU的負載不一樣,有的CPU更忙一些,而每個CPU都有自己的運行隊列,每個隊列中的進程的vruntime也走得有快有慢,比如我們對比每個運行隊列的min_vruntime值,都會有不同, 如果一個進程從min_vruntime更小的CPU (A) 上遷移到min_vruntime更大的CPU (B) 上,可能就會佔便宜了,因爲CPU (B) 的運行隊列中進程的vruntime普遍比較大,遷移過來的進程就會獲得更多的CPU時間片。這顯然不太公平

同樣的問題出現在剛創建的進程上, 還沒有投入運行, 沒有加入到某個就緒隊列中, 它以某個就緒隊列的min_vruntime爲基準設置了虛擬運行時間, 但是進程不一定在當前CPU上運行, 即新創建的進程應該是可以被遷移的.

CFS是這樣做的:

  • 當進程從一個CPU的運行隊列中出來 (dequeue_entity) 的時候,它的vruntime要減去隊列的min_vruntime值
  • 而當進程加入另一個CPU的運行隊列 ( enqueue_entiry) 時,它的vruntime要加上該隊列的min_vruntime值
  • 當進程剛剛創建以某個cfs_rq的min_vruntime爲基準設置其虛擬運行時間後,也要減去隊列的min_vruntime值

這樣,進程從一個CPU遷移到另一個CPU之後,vruntime保持相對公平。

參照sched: Remove the cfs_rq dependency from set_task_cpu() To prevent boost or penalty in the new cfs_rq caused by delta min_vruntime between the two cfs_rqs, we skip vruntime adjustment.

減去min_vruntime的情況如下

dequeue_entity():

    if (!(flags & DEQUEUE_SLEEP))
        se->vruntime -= cfs_rq->min_vruntime;

task_fork_fair():

    se->vruntime -= cfs_rq->min_vruntime;

switched_from_fair():
    if (!se->on_rq && p->state != TASK_RUNNING) 
    {
        /*
         * Fix up our vruntime so that the current sleep doesn't
         * cause 'unlimited' sleep bonus.
         */
        place_entity(cfs_rq, se, 0);
        se->vruntime -= cfs_rq->min_vruntime;
    }

加上min_vruntime的情形

enqueue_entity:
// http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L3196

    if (!(flags & ENQUEUE_WAKEUP) || (flags & ENQUEUE_WAKING))
        se->vruntime += cfs_rq->min_vruntime;

attach_task_cfs_rq:
// http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L8267

if (!vruntime_normalized(p))
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章