先備知識:
Linux 核心設計: 作業系統術語及概念
UNIX 作業系統 fork/exec 系統呼叫的前世今生
並行程式設計: 排程器原理
Linux 核心設計: 不僅是個執行單元的 Process
Linux 核心設計: 記憶體管理
Linux CPU 排程器從 1990 年誕生以來,便隨著科技的進步不斷演化,這是令當初的開發者們始料未及的
延伸閱讀 : Linux 核心設計: 發展動態回顧
Linux v0.01 ~ Linux v1.2
與排程有關的 fields :
當要挑選下一個任務時,排程器會走訪陣列並找出可執行且 counter 最大的任務。
while (--i) {
if (!*--p)
continue;
if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
c = (*p)->counter, next = i;
}
若所有陣列中所有可執行的任務的 counter 皆為零,排程器便會重新計算其 counter
(*p)->counter = ((*p)->counter >> 1) + (*p)->priority;
其他狀態的任務有剩餘的 counter 則會除以二後加入新的 counter 中。
此舉可以確保 I/O bound 的任務能被優先選到。
Early Scheduler 可以處理大多數的情況,但不支援多核處理,因排程器無法顧慮 cache affinity 以及多執行序的同步問題 (Synchronization),改變勢在必行。
參考:Linux Kernel 2.4 Internals
Linux v2.0~ Linux v2.4
O(n) scheduler 的演算法跟 Early scheduler 並無太大的差異,皆是在走訪 runqueue 後選出「最適合」的任務來執行。
差異在於 O(n) scheduler 是由 goodness function.
goodness fuction 會依據任務的 cache affinity, 權重以及是否為即時任務來決定其「適合」的程度。
任務會依照其權重大小被分成不同的類別,而不同的類別的任務則會對應到不同的排程策略 (policy)
其他有關排程的 fiels:
開始正式支援 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 來決定是否要切換當前任務。
與單核處理不同,在多核處理的架構下,排程器需考慮的不僅有任務的權重,還需考慮 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。
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.
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...
}
the function determines whether the process should preempt the current process of some CPU
為了避免「idle task 或低優先權的任務跑的不亦樂乎,高優先權的任務卻在苦等」的情況發生,當一任務被加入 runqueue 時,會由 reschedule_idle()
來決定是否要搶佔。
reschedule_idle()
藉由將欲搶佔的任務的 need_resched
類別設為 1
,使其所在的 CPU 呼叫排程器,重新排程:
Linux v2.6 ~ Linux v2.6.22
Linux v2.6.23 ~ Linux v6.5
Linux v6.6 ~