---
tags: _核心設計
breaks: true
---
# Linux 排程器研究
## CFS 排程器
先來介紹自從核心版本 2.6 之後引進的 CFS (Completely Fair Scheduler) 的概念和實作原理。根據[官方文檔介紹](https://www.kernel.org/doc/html/latest/scheduler/sched-design-CFS.html)和[从几个问题开始理解CFS调度器](http://linuxperf.com/?p=42),為了實現有效率和公平的排程引入了 RBTree 的資料結構和 virtual runtime 的概念,無關乎 cpu 的 jiffy (就是 system clock 一個 tick 的長度) 或是本身運算能力,一切排程和演算法也是依據這個值在進行的。
首先,每個 CFS 的 runqueue (也就是排程器的佇列) 都有設個 `sysctl_sched_latency`,這是每個 schedule entity 的等待時間,也就是這個 runqueue 的執行周期。再按各個 process 的優先權 (nice 值) 不同,去加權分配就是每個 process 的 vruntime。從 vruntime 最小的開始執行。執行完各自的時間片段之後會再加上一個 CPU virtual runtime `delta_exec`。再去尋找下一個最小的 vruntime。
###### spoiler 文中的 *Process 和 Schedule entity*
>
> [color=#bee]
> process 的資料型態是 [`struct task_struct`](https://elixir.bootlin.com/linux/latest/source/include/linux/sched.h#L632),schedule entity 其實應該是 process 的 fields。
>
> schedule entity 的資料型態是 [`struct sched_entity`](https://elixir.bootlin.com/linux/latest/source/include/linux/sched.h#L451),而 `vruntime`、`cfs_rq` 和 `weight` 則是 [`struct sched_entity`](https://elixir.bootlin.com/linux/latest/source/include/linux/sched.h#L451) 的 fields。
>
> 上面解釋原理在概念上是同一個東西,所以當作同一個詞就行。
>
<br>
順帶一提,[nice 值](https://en.wikipedia.org/wiki/Nice_(Unix))是決定 CPU priority 的值,也就是決定 CPU 執行片段長度 (vruntime) 的依據,由最高優先的 -20 到最低優先的 19,按權重計算 vruntime。
###### *nice 值決定 vruntime 實作原理*
> [color=#bee]
> 有正負號的參數自然不會是直接做四則運算,而是透過指數運算。
> 當前 Linux kernel 使用的公式轉換如下
>> [color=#afa]
>> weight = 1024 * 1.25^(-nice)
>>
> 這部份 linux kernel 是用查表的
>
> [core.c](https://elixir.bootlin.com/linux/latest/source/kernel/sched/core.c)
> ```c=7932
> /*
> * Nice levels are multiplicative, with a gentle 10% change for every
> * nice level changed. I.e. when a CPU-bound task goes from nice 0 to
> * nice 1, it will get ~10% less CPU time than another CPU-bound task
> * that remained on nice 0.
> *
> * The "10% effect" is relative and cumulative: from _any_ nice level,
> * if you go up 1 level, it's -10% CPU usage, if you go down 1 level
> * it's +10% CPU usage. (to achieve that we use a multiplier of 1.25.
> * If a task goes up by ~10% and another task goes down by ~10% then
> * the relative distance between them is ~25%.)
> */
> const int sched_prio_to_weight[40] = {
> /* -20 */ 88761, 71755, 56483, 46273, 36291,
> /* -15 */ 29154, 23254, 18705, 14949, 11916,
> /* -10 */ 9548, 7620, 6100, 4904, 3906,
> /* -5 */ 3121, 2501, 1991, 1586, 1277,
> /* 0 */ 1024, 820, 655, 526, 423,
> /* 5 */ 335, 272, 215, 172, 137,
> /* 10 */ 110, 87, 70, 56, 45,
> /* 15 */ 36, 29, 23, 18, 15,
> };
> ```
> 換算過的參數再做簡單的正規化
>
> weight_factor = weight ~nice=0~ / weight ~curr~ ~process~
>
> 這就是用來計算 cpu 運算時間片段長度的加權比重了
>
> [fair.c](https://elixir.bootlin.com/linux/latest/source/kernel/sched/fair.c)
> ```c=219
> /*
> * delta_exec * weight / lw.weight
> * OR
> * (delta_exec * (weight * lw->inv_weight)) >> WMULT_SHIFT
> *
> * Either weight := NICE_0_LOAD and lw \e sched_prio_to_wmult[], in which case
> * we're guaranteed shift stays positive because inv_weight is guaranteed to
> * fit 32 bits, and NICE_0_LOAD gives another 10 bits; therefore shift >= 22.
> *
> * Or, weight =< lw.weight (because lw.weight is the runqueue weight), thus
> * weight/lw.weight <= 1, and therefore our shift will also be positive.
> */
> static u64 __calc_delta(u64 delta_exec, unsigned long weight, struct load_weight *lw)
> {
> u64 fact = scale_load_down(weight);
> int shift = WMULT_SHIFT;
>
> __update_inv_weight(lw);
>
> if (unlikely(fact >> 32)) {
> while (fact >> 32) {
> fact >>= 1;
> shift--;
> }
> }
>
> fact = mul_u32_u32(fact, lw->inv_weight);
>
> while (fact >> 32) {
> fact >>= 1;
> shift--;
> }
>
> return mul_u64_u32_shr(delta_exec, fact, shift);
> }
> ```
>
> 其中 `delta_exec` 就是 CPU virtual runtime。如果 nice 值為負,加權比重就會小於 1,CPU virtual runtime 就會比較小,那 virtual runtime 成長的幅度就會比較慢,最終得到的 cpu 運算時間就會相對比較多 (差一級約莫會差 10% 的 CPU 運算時間)。
>
> 參見 [Linux Scheduler – CFS and Virtual Run Time (vruntime)](https://oakbytes.wordpress.com/2012/07/03/linux-scheduler-cfs-and-virtual-run-time/) 介紹 nice 值如何影響 virtual rumtime
>
<br>
與此同時,每個 runqueue 還有另外設置 `sysctl_sched_min_granularity` 和 `min_vruntime`。
- 前者一方面顧名思義就是用來確保每個 process 有保障的 CPU 使用時間,時間沒到誰都不能把他從 CPU 上換下來;另一方面用來避免當 process 數量過多導致各個 process 分配到的 vruntime 太小的時候就以 `sysctl_sched_min_granularity` 作為 vruntime,此時的 runqueue 週期就會是 ***`sysctl_sched_min_granularity` * process 數量***,而忽略原本的 `sysctl_sched_latency`。
:::spoiler 對應核心原始碼<br>`__sched_period()` 中用到了 `sysctl_sched_min_granularity`
[fair.c](https://elixir.bootlin.com/linux/latest/source/kernel/sched/fair.c)
```c=682
/*
* The idea is to set a period in which each task runs once.
*
* When there are too many tasks (sched_nr_latency) we have to stretch
* this period because otherwise the slices get too small.
*
* p = (nr <= nl) ? l : l*nr/nl
*/
static u64 __sched_period(unsigned long nr_running)
{
if (unlikely(nr_running > sched_nr_latency))
return nr_running * sysctl_sched_min_granularity;
else
return sysctl_sched_latency;
}
```
:::
- 後者則是會紀錄當前 runqueue 中最小的 vruntime,作為當前這個 runqueue 的一個參考值,用來讓新進 process 的 vruntime 的初值有個依據,也在喚醒 process 之後更新 vruntime 時有個依據。
:::spoiler 對應核心原始碼<br> `place_entity()` 中用到了 `vruntime`
> [color=#bee]
> [fair.c](https://elixir.bootlin.com/linux/latest/source/kernel/sched/fair.c)
> ```c=4085
> static void
> place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial)
> {
> u64 vruntime = cfs_rq->min_vruntime;
>
> /*
> * The 'current' period is already promised to the current tasks,
> * however the extra weight of the new task will slow them down a
> * little, place the new task so that it fits in the slot that
> * stays open at the end.
> */
> if (initial && sched_feat(START_DEBIT))
> vruntime += sched_vslice(cfs_rq, se);
>
> /* sleeps up to a single latency don't count. */
> if (!initial) {
> unsigned long thresh = sysctl_sched_latency;
>
> /*
> * Halve their sleep time's effect, to allow
> * for a gentler effect of sleepers:
> */
> if (sched_feat(GENTLE_FAIR_SLEEPERS))
> thresh >>= 1;
>
> vruntime -= thresh;
> }
>
> /* ensure we never gain time by being placed backwards. */
> se->vruntime = max_vruntime(se->vruntime, vruntime);
> }
> ```
> - `initial` 表示新進 process,`!intial` 表示喚醒 process。
> - 會影響新進 process vruntime 的初值的來有兩個變數 `sysctl_sched_child_runs_first` 和 scheduling features 其中的 `START_DEBIT` (參見 [feature.h](https://elixir.bootlin.com/linux/latest/source/kernel/sched/features.h) 查看所有的 scheduling features)
> - `sysctl_sched_child_runs_first` 這是 Linux 核心參數 kernel.sched_child_runs 設定 child process 優先於 parent process 與否
> ```c=10673
> 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.
> */
> swap(curr->vruntime, se->vruntime);
> resched_curr(rq);
> }
> ```
> - `sched_feat(START_DEBIT)` 則是將新進 process 的 vruntime 設的比原本參考值 (min_vruntime) 更大一點,避免 process 透過不斷的 `fork()` 取得 CPU 使用時間
> - 還有為了避免休眠了一段之後的 process vruntime 跟其他 process 差距過大 (這會導致得到的 CPU 時間會跟其他 process 差距過大),要重新設定。減去的部份是對於休眠了一段時間的補償 (因為 vruntime 愈小愈容易得到 CPU 的時間)。
:::
## 衍生議題
接著,根據引導閱讀的兩篇文章說起:
#### 第一篇 [Linux fork之后,到底是子进程先运行还是父进程先运行](https://blog.csdn.net/dog250/article/details/105756168) :zzz:
其實執行順序追根究底就是一個核心參數 kernel.sched_child_runs_first
```bash
# 察看系統預設值
sysctl -a | grep kernel.sched_child_runs_first
# 設成 child process 先執行
sysctl -w kernel.sched_child_runs_first=1
```
為了測試這個參數實際效用,這篇針對 parent process 和 child process 執行順序的測試採用的是 [Systemtap](https://sourceware.org/systemtap/),而不是採用最常見最籠統的 printf(),理由是 printf() 的執行周期和成本不小,不一定能夠顯示正確的執行順序。而且執行過後事實證明結果好像也不如預期。
這是另外寫的一篇 ==[Systemtab 的介紹](/fHpOveNOSZad5_R1YTAshA)==
:::spoiler 以下是原文用來分析問題的 Systemtab script<br> 不過實際跑過一遍結果跳了個錯誤訊息 `ERROR: Skipped too many probes, check MAXSKIPPED or try again with stap -t for more details.`
>[color=#bee]
> ```stap=
> global g_se;
> global g_cfs_rq;
>
> probe begin {
> g_cfs_rq = 0;
> g_se = 0;
> }
>
> probe kernel.function("__schedule")
> {
> t_curr = task_current();
> if (task_execname(t_curr) == "a.out")
> printf("[_schedule] current task: %s[%d]\n", task_execname(t_curr), task_pid(t_curr));
> }
>
> probe kernel.function("do_exit")
> {
> t_curr = task_current();
> if (task_execname(t_curr) == "a.out")
> printf("Exit task: %s[%d]\n", task_execname(t_curr), task_pid(t_curr));
> }
>
> probe kernel.function("pick_next_task_fair")
> {
> g_cfs_rq = &$rq->cfs;
> }
>
> function container_of_entity:long(se:long)
> {
> offset = &@cast(0, "struct task_struct")->se;
> return se - offset;
> }
>
> probe kernel.function("pick_next_task_fair").return
> {
> if($return != 0) {
> se = &$return->se;
> t_se = container_of_entity(se);
> t_curr = task_current();
> if (task_execname(t_se) == "a.out" || task_execname(t_curr) == "a.out") {
> printf("[pick_next_task_fair] Return task: %s[%d] From current: %s[%d]\n", task_execname(t_se), task_pid(t_se), task_execname(t_curr), task_pid(t_curr));
> }
> }
> }
>
>
> probe kernel.function("wake_up_new_task")
> {
> g_se = &$p->se;
> g_cfs_rq = @cast(g_se, "struct sched_entity")->cfs_rq;
> }
>
> probe kernel.function("wake_up_new_task").return
> {
> t_se = container_of_entity(g_se);
> tname = task_execname(t_se);
> vruntime = @cast(g_se, "struct sched_entity")->vruntime;
> if (tname == "a.out") {
> curr = @cast(g_cfs_rq, "struct cfs_rq")->curr;
> t_curr = container_of_entity(curr);
> curr_vruntime = @cast(curr, "struct sched_entity")->vruntime;
> printf("[wake_up_new_task] current:[%s][%d] curr:%d new:%d del:%d\n",
> task_execname(t_curr), task_pid(t_curr), curr_vruntime, vruntime,
> curr_vruntime - vruntime);
> }
> g_se = 0;
> g_cfs_rq = 0;
> }
>
> probe kernel.function("place_entity")
> {
> t_initial = $initial;
> if (t_initial == 1) {
> g_cfs_rq = $cfs_rq;
> g_se = $se;
> }
> }
> probe kernel.function("place_entity").return
> {
> if (g_se) {
> t_se = container_of_entity(g_se);
> tname = task_execname(t_se);
> vruntime = @cast(g_se, "struct sched_entity")->vruntime;
> if (tname == "a.out") {
> curr = @cast(g_cfs_rq, "struct cfs_rq")->curr;
> t_curr = container_of_entity(curr);
> curr_vruntime = @cast(curr, "struct sched_entity")->vruntime;
> printf("[place_entity] name:[%s][%d] curr:%d new:%d delta:%d\n",
> task_execname(t_curr), task_pid(t_curr), curr_vruntime, vruntime,
> curr_vruntime - vruntime);
> }
> g_se = 0;
> g_cfs_rq = 0;
> }
> }
> ```
> - 第一組追蹤的函式是進入和離開當前排程器任務的 [`__schedule()`](https://elixir.bootlin.com/linux/v5.7.1/source/kernel/sched/core.c#L3962) 和 [`do_exit()`](https://elixir.bootlin.com/linux/v5.7.1/source/kernel/exit.c#L706) 是排程器運作主要的函式,再用 [task_current()](https://elixir.bootlin.com/linux/v5.7.1/source/kernel/sched/sched.h#L1662) 輸出當前的 process
>
> - 第二組追蹤的函式是針對 [cgroup](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/6/html/resource_management_guide/ch01) 去優化過的排程函式 [pick_next_task_fair()](https://elixir.bootlin.com/linux/v5.7.1/source/kernel/sched/fair.c#L6957) 的進入點和離開點,輸出替換上來的 process 和用 `container_of_entity()` 取得的 parent process
>
> - 最後一組是喚醒新創 process 的 [`wake_up_new_task()`](https://elixir.bootlin.com/linux/latest/source/kernel/sched/core.c#L2926) 和[重新設定 vruntime](https://stackoverflow.com/questions/8016154/linux-cfs-completely-fair-scheduler-latency) 的 [`place_entity()`](https://elixir.bootlin.com/linux/latest/source/kernel/sched/fair.c#L4086),因為是緊接著的工作,所以感覺可能會有點多餘,忠於原著,暫時先做保留。
:::
<br>
檢查出來的問題在於,在 child process 剛創建時,確實優先順序是高於 parent process,但是 child process 還沒準備好,也就不足已被 wakeup。原文作者試圖透過 systemtap 的 script 手動解決這個問題。
:::spoiler 原文實驗用的 script 附在下面,實際操作了一遍還是沒能成功,出現了錯誤訊息: `ERROR: read fault [man error::fault] at 0x8d8 near operator '@cast' at correction.stp:40:11`
```stap=
#!/usr/bin/stap -g
global g_p;
probe begin {
g_p = 0;
}
%{
static void *(*_resched_task)(struct task_struct *p);
%}
function resched(tsk:long, tskp:long)
%{
struct task_struct *task = NULL, *parent = NULL;
struct sched_entity *pse = NULL, *cse = NULL;
task = (struct task_struct *)STAP_ARG_tsk;
parent = (struct task_struct *)STAP_ARG_tskp;
cse = &task->se;
pse = &parent->se;
if (_resched_task == NULL)
_resched_task = (void *)kallsyms_lookup_name("resched_task");
if (_resched_task && pse->vruntime < cse->vruntime) {
swap(pse->vruntime, cse->vruntime);
STAP_PRINTF("---[%lu]------[%lu]-------\n", pse->vruntime, cse->vruntime);
_resched_task(current);
}
%}
probe kernel.function("check_preempt_wakeup")
{
g_p = $p;
}
// 这里的trick在于,由于我们的父子a.out都是纯CPU型的,只在创建时被wakeup一次,所以hook该点。
probe kernel.function("check_preempt_wakeup").return
{
parent = @cast(g_p, "struct task_struct")->parent;
// 这里过滤掉了除了我们的fork场景之外的所有其它的wakeup场景。
if (task_execname(g_p) == "a.out" || task_execname(parent) == "a.out") {
resched(g_p, parent);
}
g_p = 0;
}
```
:::
<br>
還是沒能成功將 child process 優先於 parent process 執行。由於對 systemtap 的研究還不夠透澈,暫時還沒辦法做問題排除。 :dizzy_face:
###### 為何 `sysctl -w kernel.sched_child_runs_first=1` 無效
根據 Linus Torvalds 在[這份文件](https://yarchive.net/comp/linux/child-runs-first.html)中提到其實 child runs first 和 parent runs first 各有各的理由和問題,所以首先這本來就算是偏好和建議而非保證。在最後的嘗試發現 child runs first 對於 bash 來說的問題比較大,所以現階段是選擇 parent runs first。而單純的參數設定也因為上述問題而沒用。
#### 第二篇 [从一个CFS调度案例谈Linux系统卡顿的根源](https://blog.csdn.net/dog250/article/details/105710571) :zzz:
這個議題在於排程的運作好像不錯,以及處理器的使用率似乎好高,但系統操作起來卻有些卡頓。原文嘗試了幾次才成功重現並分析了問題,我照著原方法試了幾次都沒遇到,所以就不附過程了,畢竟套一句原作者 [dog250](https://blog.csdn.net/dog250) 說的「如果這麼容易重現問題 Linux 社群應該早就修掉了。對於問題的分析過程可以參見原文,這裡講講問題發生的情境和描述。
起因於單純用紅黑樹和上述演算法去實作 CFS 的話倒還單純,可為了提升效能和資源的使用率,CFS 後來又引入並支援了許多 features,包括後來發現問題的 `LAST_BUDDY` 這個 feature。
這是一個好比生產者消費者這種有對應關係或順序的 process 在輪流 sleep 和 wakeup 的過程中,理論上都會有個理論上是中斷點,下一次 rescheduling 的過程中再從這個中斷點繼續。但是有的時候 CPU 會因為 cache locality 的關係讓一些原本可能是 wakeup 之後的接續點 preempt,因此打亂了這些理論上的順序。