# 《Demystifying the Linux CPU Scheduler》重點提示
先備知識:
[Linux 核心設計: 作業系統術語及概念](https://hackmd.io/@sysprog/linux-concepts)
[UNIX 作業系統 fork/exec 系統呼叫的前世今生](https://hackmd.io/@sysprog/unix-fork-exec)
[並行程式設計: 排程器原理](https://hackmd.io/@sysprog/concurrency-sched)
[Linux 核心設計: 不僅是個執行單元的 Process](https://hackmd.io/@sysprog/linux-process)
[Linux 核心設計: 記憶體管理](https://hackmd.io/@sysprog/linux-memory)
## 2. The Linux CPU scheduler
Linux CPU 排程器從 1990 年誕生以來,便隨著科技的進步不斷演化,這是令當初的開發者們始料未及的
>延伸閱讀 : [Linux 核心設計: 發展動態回顧](https://hackmd.io/@sysprog/linux-dev-review)
### Early scheduler
>Linux v0.01 ~ Linux v1.2
與排程有關的 fields :
- priority :
靜態權重,其的意義是該任務的在 Round Robin 中的 timeslice ,越重要的任務分的越多的 timeslice.
- counter :
動態權重,其的意義是該任務剩餘的 timeslice ,會隨著時間減少,當任務的 conter 小於零時,排程器便會選擇下一個任務。
當要挑選下一個任務時,排程器會走訪陣列並找出可執行且 counter 最大的任務。
```c
while (--i) {
if (!*--p)
continue;
if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
c = (*p)->counter, next = i;
}
```
若所有陣列中所有可執行的任務的 counter 皆為零,排程器便會重新計算其 counter
```c
(*p)->counter = ((*p)->counter >> 1) + (*p)->priority;
```
其他狀態的任務有剩餘的 counter 則會除以二後加入新的 counter 中。
此舉可以確保 I/O bound 的任務能被優先選到。
Early Scheduler 可以處理大多數的情況,但不支援多核處理,因排程器無法顧慮 cache affinity 以及多執行序的同步問題 (Synchronization),改變勢在必行。
### O(n) scheduler
>參考:[Linux Kernel 2.4 Internals](https://www.iitk.ac.in/LDP/LDP/lki/lki.pdf)
Linux v2.0~ Linux v2.4
O(n) scheduler 的演算法跟 Early scheduler 並無太大的差異,皆是在走訪 runqueue 後選出「最適合」的任務來執行。
差異在於 O(n) scheduler 是由 goodness function.
goodness fuction 會依據任務的 cache affinity, 權重以及是否為即時任務來決定其「適合」的程度。
任務會依照其權重大小被分成不同的類別,而不同的類別的任務則會對應到不同的排程策略 (policy)
- SCHED_OTHER (traditional UNIX process)
- SCHED_FIFO (POSIX.1b FIFO realtime process)
- SCHED_RR (POSIX round−robin realtime process)
當任務的 conter 耗盡時,排程器會立刻賦予其新的 timeslice 並將其放到 runqueue 的末端,如此一來若有相同權重的任務便會被輪流執行到
- SCHED_YIELD
其他有關排程的 fiels:
- rt_priority:
即時任務的權重,排程器會依此為任務類別為 SCHED_RR 或 SCHED_FIFO 的任務排序
- has_cpu :
用來表示任務是否有被指派到特定的 CPU
- processor :
用來表示最近一次執行此任務的 CPU
開始正式支援 SMP,使得處理器能處理的任務規模不同日而語,
o(n) scheduler 的基本架構跟 Early scheduler 一樣,都是先走訪整個 runqueue 並找出最「適合」的任務進行排程,兩者的差異在於 o(n) scheduler 支援 SMP
Linux scheduler behavior has two paths involved in it they are :
在多核處理的架構中,每個核(core)個自(exclusively)從 global runqueue 中挑選「最適合」該核的任務。
在挑選的過程中,`schedule()`會逐一比較 runqueue 中所有任務的 goodness value 來決定是否要切換當前任務。
#### goodness function
與單核處理不同,在多核處理的架構下,排程器需考慮的不僅有任務的權重,還需考慮 cache affinity
一個任務的 goodness value 是由 goodness function 所計算,一個任務的 goodness value 愈高代表該任務越適合當前的核。
即時行程與普通行程的 goodness value 在計算的方法上大不相同:
- 在計算即時任務的 goodness value 時只考慮其 rt_priority 而不考慮其剩餘的 timeslice,並額外加上 1000 以確保即時行程的 goodness value 會永遠大於普通行程的 goodness value.
- 相較於即時行程,普通行程更在乎 fairness ,即任務得到的 CPU 時間需要與其 nice value 成比例。
因此 goodness value 的計算方式為該任務
剩餘的 timeslic 加上其 nice value (20 - p->nice)
此外考量到 cache affinity 對效能的影響,若當前 core 為該普通任務上一次運行的 core(哇!真巧),則其 goodness value 會再加上額外的常數 PROC_CHANGE_PENALTY ,即相較於其他同樣 goodness value 的任務「更適合」當前的 core。
```c
static inline int goodness(struct task_struct *p, int this_cpu,
struct mm_struct *this_mm)
{
int weight = -1;
if (p->policy & SCHED_YIELD)
goto out;
/* Non-RT process - normal case first. */
if (p->policy == SCHED_OTHER) {
weight = p->counter;
if (!weight)
goto out;
#ifdef CONFIG_SMP
/* Give a largish advantage to the same processor. */
if (p->processor == this_cpu)
weight += PROC_CHANGE_PENALTY;
#endif
/* .. and a slight advantage to the current MM */
if (p->mm == this_mm || !p->mm)
weight += 1;
weight += 20 - p->nice;
goto out;
}
/* Realtime process */
weight = 1000 + p->rt_priority;
out:
return weight;
}
```
倘若 runqueue 中所有任務的 goodness value 皆小於等於零,則代表所有任務都消耗完各自的 timeslice 。
此時 schedule() 會重新賦予每個任務新的 timeslice 。
要注意即時任務一但耗盡其 timeslice , schedule() 便會為重新賦予其新的 timeslice ,
並將其移動至 runqueue 的底部,確保相同優先權的的任務會被輪流執行到。
反之普通任務只有在 runqueue 中所有任務皆耗盡其 timeslice 時才會重新得到 timesice.
#### scheduler
```c
asmlinkage void schedule(void)
{
/* process descriptors: prev: currently running; p: currently iterating */
struct task_struct *prev, *p;
int this_cpu;
// ...skip...
/* move an exhausted RR process to be last.. */
if (unlikely(prev->policy == SCHED_RR))
if (!prev->counter) {
prev->counter = NICE_TO_TICKS(prev->nice);
move_last_runqueue(prev);
}s
repeat_schedule:
/* Default process to select.. */
next = idle_task(this_cpu);
c = -1000;
list_for_each(tmp, &runqueue_head) {
p = list_entry(tmp, struct task_struct, run_list);
if (can_schedule(p, this_cpu)) {
int weight = goodness(p, this_cpu, prev->active_mm);
if (weight > c)
c = weight, next = p;
}
}
if (unlikely(!c)) {
struct task_struct *p;
spin_unlock_irq(&runqueue_lock);
read_lock(&tasklist_lock);
for_each_task(p)
p->counter = (p->counter >> 1) + NICE_TO_TICKS(p->nice);
read_unlock(&tasklist_lock);
spin_lock_irq(&runqueue_lock);
goto repeat_schedule;
}
// ...skip...
}
```
#### reschedule_idle
>the function determines whether the process should preempt the current process of some CPU
為了避免「[idle task 或低優先權的任務跑的不亦樂乎,高優先權的任務卻在苦等](https://people.ece.ubc.ca/sasha/papers/eurosys16-final29.pdf)」的情況發生,當一任務被加入 runqueue 時,會由 `reschedule_idle()` 來決定是否要搶佔。
`reschedule_idle()` 藉由將欲搶佔的任務的 `need_resched` 類別設為 `1` ,使其所在的 CPU 呼叫排程器,重新排程:
1. 該任務上一次使用的 CPU (p->process)剛好 為Idle
2. 其他 Idle 的CPU
3. 正在運行 Goodness value 相較於該任務最低的任務 的CPU
>參考 [令人詬病的 Linux 2.4 排程器](https://hackmd.io/@sysprog/linux-scheduler#%E4%BB%A4%E4%BA%BA%E8%A9%AC%E7%97%85%E7%9A%84-Linux-24-%E6%8E%92%E7%A8%8B%E5%99%A8:~:text=the%20Linux%20Kernel-,%E4%BB%A4%E4%BA%BA%E8%A9%AC%E7%97%85%E7%9A%84%20Linux%202.4%20%E6%8E%92%E7%A8%8B%E5%99%A8,-%E5%9B%9E%E9%A1%A7%20Linux%202.4)
### O(1) scheduler
Linux v2.6 ~ Linux v2.6.22
### CFS
Linux v2.6.23 ~ Linux v6.5
### EEVDF
Linux v6.6 ~