我們也講解了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))