Linux 2.6 調度系統分析——在 2.4 之上進步__Linux

來源:互聯網
上載者:User

Linux 2.6 調度系統分析
內容:
1. 前言
2. 新的資料結構 runqueue
3. 改進後的 task_struct
4. 新的已耗用時間片表現
5. 最佳化了的優先順序計算方法
6. 進程平均等待時間 sleep_avg
7. 更精確的互動式進程優先
8. 調度器
9. 調度器對核心搶佔啟動並執行支援
10. 調度器相關的Server Load Balancer
11. NUMA 結構下的調度
12. 調度器的即時效能
13. 後記:從調度器看 Linux 發展
參考資料
關於作者
相關內容:
Linux 2.4 調度系統分析
developerWorks Toolbox subscription
在 Linux 專區還有:
教程
工具與產品
代碼與組件
文章
在 2.4 之上進步

楊沙洲 (pubb@163.net)
國防科技大學電腦學院
2004 年 4 月 本文從 Linux 2.4 調度系統的缺陷入手,詳細分析了 Linux 2.6 調度系統的原理和實現細節,並對與調度系統相關的Server Load Balancer、NUMA 結構以及即時效能進行了分析和評價。文末,作者從調度系統的發展和實現出發,對 Linux 的發展特點和方向提出了自己的看法。

1. 前言

Linux 的市場非常廣闊,從案頭工作站到低端伺服器,它都是任何商用作業系統的有力競爭者。目前,Linux 正全力進軍嵌入式系統和高端伺服器系統領域,但它的技術缺陷限制了它的競爭力:缺乏對即時任務的支援,多處理機可擴充性差。在 2.4 核心中,造成這兩個弱項的關鍵原因之一就是調度器設計上的缺陷。

2.6 調度系統從設計之初就把開發重點放在更好滿足即時性和多處理機並行性上,並且基本實現了它的設計目標。主要設計者,傳奇式人物 Ingo Molnar 將新調度系統的特性概括為如下幾點: 繼承和發揚 2.4 版調度器的特點: 互動式作業優先 輕載條件下調度/喚醒的高效能 公平共用 基於優先順序調度 高 CPU 使用率 SMP 高效親和 即時調度和 cpu 綁定等調度手段 在此基礎之上的新特性: O(1)調度演算法,調度器開銷恒定(與當前系統負載無關),即時效能更好 高可擴充性,鎖粒度大幅度減小 新設計的 SMP 親和方法 最佳化計算密集型的批次工作的調度 重載條件下調度器工作更平滑 子進程先於父進程運行等其他改進

在 2.5.x 的實驗版本中,新的調度器的開發一直受到廣泛關注,實測證明它的確使系統效能得到很大改善。本文就從新設計的資料結構開始,圍繞 2.6 對於 2.4 所作的改進,對 2.6 調度系統的原理和實現細節進行分析。2.6 調度器設計相當複雜,文中還存在很多需要繼續研究的地方,特別是各個調度參數的設定,隨著核心版本的升級,可能還會繼續修正。

2. 新的資料結構 runqueue

我們知道,在 2.4 核心中,就緒進程隊列是一個全域資料結構,調度器對它的所有操作都會因全域自旋鎖而導致系統各個處理機之間的等待,使得就緒隊列成為一個明顯的瓶頸。

2.4 的就緒隊列是一個簡單的以 runqueue_head 為頭的雙向鏈表,在 2.6 中,就緒隊列定義為一個複雜得多的資料結構 struct runqueue,並且,尤為關鍵的是,每一個 CPU 都將維護一個自己的就緒隊列,--這將大大減小競爭。

O(1)演算法中很多關鍵技術都與 runqueue 有關,所以,我們對調度器的分析就先從 runqueue 結構開始。

1) prio_array_t *active, *expired, arrays[2]

runqueue 中最關鍵的資料結構。每個 CPU 的就緒隊列按時間片是否用完分為兩部分,分別通過 active 指標和 expired 指標訪問,active 指向時間片沒用完、當前可被調度的就緒進程,expired 指向時間片已用完的就緒進程。每一類就緒進程都用一個 struct prio_array 的結構表示:

 

 
圖1:active、expired 數組樣本

圖中的 task 並不是 task_struct 結構指標,而是 task_struct::run_list,這是一個小技巧,詳見下文 run_list 的解釋。

在 2.4 版的核心裡,尋找最佳候選就緒進程的過程是在調度器 schedule() 中進行的,每一次調度都要進行一次(在 for 迴圈中調用 goodness()),這種尋找過程與當前就緒進程的個數相關,因此,尋找所耗費的時間是 O(n) 級的,n 是當前就緒進程個數。正因為如此,調度動作的執行時間就和當前系統負載相關,無法給定一個上限,這與即時性的要求相違背。

在新的 O(1) 調度中,這一尋找過程分解為 n 步,每一步所耗費的時間都是 O(1) 量級的。

prio_array 中包含一個就緒隊列數組,數組的索引是進程的優先順序(共 140 級,詳見下 "static_prio" 屬性的說明),相同優先順序的進程放置在相應數組元素的鏈表 queue 中。調度時直接給出就緒隊列 active 中具有最高優先順序的鏈表中的第一項作為候選進程(參見"調度器"),而優先順序的計算過程則分布到各個進程的執行過程中進行(見"最佳化了的優先順序計算方法")。

為了加速尋找存在就緒進程的鏈表,2.6 核心又建立了一個位映射數組來對應每一個優先順序鏈表,如果該優先順序鏈表非空,則對應位為 1,否則為 0。核心還要求每個體繫結構都構造一個 sched_find_first_bit() 函數來執行這一搜尋操作,快速定位第一個非空的就緒進程鏈表。

採用這種將集中計算過程分散進行的演算法,保證了調度器啟動並執行時間上限,同時在記憶體中保留更加豐富的資訊的做法也加速了候選進程的定位過程。這一變化簡單而又高效,是 2.6 核心中的亮點之一。

arrays 二元數組是兩類就緒隊列的容器,active 和 expired 分別指向其中一個。active 中的進程一旦用完了自己的時間片,就被轉移到 expired 中,並設定好新的初始時間片;而當 active 為空白時,則表示當前所有進程的時間片都消耗完了,此時,active 和 expired 進行一次對調,重新開始下一輪的時間片遞減過程(參見"調度器")。

回憶一下 2.4 調度系統,進程時間片的計算是比較耗時的,在早期核心版本中,一旦時間片耗盡,就在時鐘中斷中重新計算時間片,後來為了提高效率,減小時鐘中斷的處理時間,2.4 調度系統在所有就緒進程的時間片都耗完以後在調度器中一次性重算。這又是一個 O(n) 量級的過程。為了保證 O(1) 的調度器執行時間,2.6 的時間片計算在各個進程耗盡時間片時單獨進行,而通過以上所述簡單的對調來完成時間片的輪轉(參見"調度器")。這又是 2.6 調度系統的一個亮點。

2) spinlock_t lock

runqueue 的自旋鎖,當需要對 runqueue 進行操作時,仍然應該鎖定,但這個鎖定操作隻影響一個 CPU 上的就緒隊列,因此,競爭發生的機率要小多了。

3) task_t *curr

本 CPU 正在啟動並執行進程。

4) tast_t *idle

指向本 CPU 的 idle 進程,相當於 2.4 中 init_tasks[this_cpu()] 的作用。

5) int best_expired_prio

記錄 expired 就緒進程組中的最高優先順序(數值最小)。該變數在進程進入 expired 隊列的時候儲存(schedule_tick()),用途見 "expired_timestamp"的解釋)。

6) unsigned long expired_timestamp

當新一輪的時間片遞減開始後,這一變數記錄著最早發生的進程耗完時間片事件的時間(jiffies 的絕對值,在 schedule_tick() 中賦),它用來表徵 expired 中就緒進程的最長等待時間。它的使用體現在 EXPIRED_STARVING(rq) 宏上。

上面已經提到,每個 CPU 上維護了兩個就緒隊列,active 和 expired。一般情況下,時間片結束的進程應該從 active 隊列轉移到 expired 隊列中(schedule_tick()),但如果該進程是互動式進程,調度器就會讓其保持在 active 隊列上以提高它的響應速度。這種措施不應該讓其他就緒進程等待過長時間,也就是說,如果 expired 隊列中的進程已經等待了足夠長時間了,即使是互動式進程也應該轉移到 expired 隊列上來,排空 active。這個閥值就體現在EXPIRED_STARVING(rq) 上:在 expired_timestamp 和 STARVATION_LIMIT 都不等於 0 的前提下,如果以下兩個條件都滿足,則 EXPIRED_STARVING() 返回真: (當前絕對時間 - expired_timestamp) >= (STARVATION_LIMIT * 隊列中所有就緒進程總數 + 1),也就是說 expired 隊列中至少有一個進程已經等待了足夠長的時間; 正在啟動並執行進程的靜態優先順序比 expired 隊列中最高優先順序要低(best_expired_prio,數值要大),此時當然應該儘快排空 active 切換到expired 上來。

7) struct mm_struct *prev_mm

儲存進程切換後被調度下來的進程(稱之為 prev)的 active_mm 結構指標。因為在 2.6 中 prev 的 active_mm 是在進程切換完成之後釋放的(mmdrop()),而此時 prev 的 active_mm 項可能為 NULL,所以有必要在 runqueue 中預先保留。

8) unsigned long nr_running

本 CPU 上的就緒進程數,該數值是 active 和 expired 兩個隊列中進程數的總和,是說明本 CPU 負載情況的重要參數(詳見"調度器相關的Server Load Balancer")。

9) unsigned long nr_switches

記錄了本 CPU 上自調度器運行以來發生的進程切換的次數。

10) unsigned long nr_uninterruptible

記錄本 CPU 尚處於 TASK_UNINTERRUPTIBLE 狀態的進程數,和負載資訊有關。

11) atomic_t nr_iowait

記錄本 CPU 因等待 IO 而處於休眠狀態的進程數。

12) unsigned long timestamp_last_tick

本就緒隊列最近一次發生調度事件的時間,在Server Load Balancer的時候會用到(見"調度器相關的Server Load Balancer")。

13) int prev_cpu_load[NR_CPUS]

記錄進行Server Load Balancer時各個 CPU 上的負載狀態(此時就緒隊列中的 nr_running 值),以便分析負載情況(見"調度器相關的Server Load Balancer")。

14) atomic_t *node_nr_running; int prev_node_load[MAX_NUMNODES]

這兩個屬性僅在 NUMA 結構下有效,記錄各個 NUMA 節點上的就緒進程數和上一次Server Load Balancer操作時的負載情況(見"NUMA 結構下的調度")。

15) task_t *migration_thread

指向本 CPU 的遷移進程。每個 CPU 都有一個核心線程用於執行進程遷移操作(見"調度器相關的Server Load Balancer")。

16) struct list_head migration_queue

需要進行遷移的進程列表(見"調度器相關的Server Load Balancer")。

調度系統代碼結構 絕大多數調度系統的實現代碼,包括 runqueue 結構的定義,都在[kernel/sched.c]檔案中,這樣做的目的是將所有調度系統的代碼集中起來,便於更新和替換。除非特別註明,本文所引代碼和函數實現均位於[kernel/sched.c]中。

3. 改進後的 task_struct

2.6 版的核心仍然用 task_struct 來表徵進程,儘管對線程進行了最佳化,但線程的核心表示仍然與進程相同。隨著調度器的改進,task_struct 的內容也有了改進,互動式進程優先支援、核心搶佔支援等新特性,在 task_struct 中都有所體現。在 task_struct 中,有的屬性是新增加的,有的屬性的值的含義發生了變化,而有的屬性僅僅是改了一下名字。

1) state

進程的狀態仍然用 state 表示,不同的是,2.6 裡的狀態常量重新定義了,以方便位操作:

 

新增加的TASK_DEAD指的是已經退出且不需要父進程來回收的進程。

2) timestamp

進程發生調度事件的時間(單位是 nanosecond,見下)。包括以下幾類: 被喚醒的時間(在 activate_task() 中設定); 被切換下來的時間(schedule()); 被切換上去的時間(schedule()); Server Load Balancer相關的賦值(見"調度器相關的Server Load Balancer")。

從這個值與目前時間的差值中可以分別獲得"在就緒隊列中等待啟動並執行時間長度"、"運行時間長度"等與優先順序計算相關的資訊(見"最佳化了的優先順序計算方法")。

兩種時間單位 系統的時間是以 nanosecond(十億分之一秒)為單位的,但這一數值粒度過細,大部分核心應用僅能取得它的絕對值,感知不到它的精度。
時間相關的核心應用通常圍繞時鐘中斷進行,在 Linux 2.6 中,系統時鐘每 1 毫秒中斷一次(時鐘頻率,用 HZ 宏表示,定義為 1000,即每秒中斷 1000 次,--2.4 中定義為 100,很多應用程式也仍然沿用 100 的時鐘頻率),這個時間單位稱為一個 jiffie。很多核心應用都是以 jiffies 作為時間單位,例如進程的已耗用時間片。
jiffies 與絕對時間之間的轉換公式如下:
nanosecond=jiffies*1000000
核心用兩個宏來完成兩種時間單位的互換:JIFFIES_TO_NS()、NS_TO_JIFFIES(),很多時間宏也有兩種形式,例如 NS_MAX_SLEEP_AVG 和 MAX_SLEEP_AVG。

3) prio

優先順序,相當於 2.4 中 goodness() 的計算結果,在 0~MAX_PRIO-1 之間取值(MAX_PRIO 定義為 140),其中 0~MAX_RT_PRIO-1 (MAX_RT_PRIO 定義為100)屬於即時進程範圍,MAX_RT_PRIO~MX_PRIO-1 屬於非即時進程。數值越大,表示進程優先順序越小。

2.6 中,動態優先順序不再統一在調度器中計算和比較,而是獨立計算,並儲存在進程的 task_struct 中,再通過上面描述的 priority_array 結構自動排序。

prio 的計算和很多因素相關,在"最佳化了的優先順序計算方法"中會詳細討論。

4) static_prio

靜態優先順序,與 2.4 的 nice 值意義相同,但轉換到與 prio 相同的取值區間。

nice 值沿用 Linux 的傳統,在 -20 到 19 之間變動,數值越大,進程的優先順序越小。nice 是使用者可維護的,但僅影響非即時進程的優先順序。2.6 核心中不再儲存 nice 值,而代之以 static_prio。進程初始時間片的大小僅決定於進程的靜態優先順序,這一點不論是即時進程還是非即時進程都一樣,不過即時進程的static_prio 不參與優先順序計算。

nice 與 static_prio 之間的關係如下:
static_prio = MAX_RT_PRIO + nice + 20

 

核心定義了兩個宏用來完成這一轉換:PRIO_TO_NICE()、NICE_TO_PRIO()。

5) activated

表示進程因什麼原因進入就緒態,這一原因會影響到調度優先順序的計算。activated 有四個值: -1,進程從 TASK_UNINTERRUPTIBLE 狀態被喚醒; 0,預設值,進程原本就處於就緒態; 1,進程從 TASK_INTERRUPTIBLE 狀態被喚醒,且不在中斷上下文中; 2,進程從 TASK_INTERRUPTIBLE 狀態被喚醒,且在中斷上下文中。
activated 初值為 0,在兩個地方修改,一是在 schedule() 中,被恢複為 0,另一個就是 activate_task(),這個函數由 try_to_wake_up() 函數調用,用於啟用休眠進程: 如果是中斷服務程式調用的 activate_task(),也就是說進程由中斷啟用,則該進程最有可能是互動,因此,置 activated=2;否則置activated=1。 如果進程是從 TASK_UNINTERRUPTIBLE 狀態中被喚醒的,則 activated=-1(在try_to_wake_up()函數中)。
activated 變數的具體含義和使用見"最佳化了的優先順序計算方式"。

6) sleep_avg

進程的平均等待時間(以 nanosecond 為單位),在 0 到 NS_MAX_SLEEP_AVG 之間取值,初值為 0,相當於進程等待時間與已耗用時間的差值。sleep_avg 所代表的含義比較豐富,既可用於評價該進程的"互動程度",又可用於表示該進程需要啟動並執行緊迫性。這個值是動態優先順序計算的關鍵因子,sleep_avg 越大,計算出來的進程優先順序也越高(數值越小)。在下文"進程平均等待時間 sleep_avg" 中會詳細分析 sleep_avg 的變化過程。

7) interactive_credit

這個變數記錄了本進程的"互動程度",在 -CREDIT_LIMIT 到 CREDIT_LIMIT+1 之間取值。進程被建立出來時,初值為 0,而後根據不同的條件加 1 減 1,一旦超過 CREDIT_LIMIT(只可能等於 CREDIT_LIMIT+1),它就不會再降下來,表示進程已經通過了"互動式"測試,被認為是互動式進程了。interactive_credit具體的變化過程在"更精確的互動式進程優先"中會詳細描述。

8) nvcsw/nivcsw/cnvcsw/cnivcsw

進程切換計數。

9) time_slice

進程的時間片餘額,相當於 2.4 的 counter,但不再直接影響進程的動態優先順序。在"新的已耗用時間片表現"中專門分析了 time_slice 的行為。

10) first_time_slice

0 或 1,表示是否是第一次擁有時間片(剛建立的進程)。這一變數用來判斷進程結束時是否應當將自己的剩餘時間片返還給父進程(見"新的已耗用時間片表現")。

11) run_list

前面提到,優先順序數組 prio_array 結構中按順序排列了各個優先順序下的所有進程,但實際上數組中每一個元素都是 list_head 結構,以它為表頭的鏈表中的每一個元素也是 list_head,其中連結的就是 task_struct 中的 run_list 成員。這是一個節省空間的、加速訪問的小技巧:調度器在 prio_array 中找到相應的 run_list,然後通過 run_list 在 task_struct 中的固定位移量找到對應的 task_struct(參見 enqueue_task()、dequeue_task() 和 list.h 中的操作)。

12) array

記錄當前 CPU 的活躍就緒隊列(runqueue::active)。

13) thread_info

當前進程的一些運行環境資訊,其中有兩個結構成員與調度關係緊密: preempt_count:初值為 0 的非負計數器,大於 0 表示核心不宜被搶佔; flags:其中有一個 TIF_NEED_RESCHED 位,相當於 2.4 中的 need_resched 屬性,如果當前運行中的進程此位為 1,則表示應該儘快啟動調度器。

在 2.4 中,每個進程的 task_struct 都位於該進程核心棧的頂端(低址部分),核心可以通過棧寄存器 ESP 輕鬆訪問到當前進程的 task_struct。在 2.6 中,仍然需要頻繁訪問這個名為 current 的資料結構,但現在,進程核心棧頂儲存的是其中的 thread_info 屬性,而不是完整的 task_struct 了。這樣做的好處是僅將最關鍵的、訪問最頻繁的運行環境儲存在核心棧裡(仍然是兩個頁大小),而將 task_struct 大部分內容通過 thread_info::task 指標儲存在棧外,以方便擴充。thread_info 的分配方式和訪問方式與 2.4 中的 task_struct 完全相同,現在的 current 需要這樣來訪問:

 

 
圖2:現在的current

4. 新的已耗用時間片表現

2.6 中,time_slice 變數代替了 2.4 中的 counter 變數來表示進程剩餘已耗用時間片。time_slice 儘管擁有和 counter 相同的含義,但在核心中的表現行為已經大相徑庭,下面分三個方面討論新的已耗用時間片表現:

1) time_slice 基準值

和 counter 類似,進程的預設時間片與進程的靜態優先順序(在 2.4 中是 nice 值)相關,使用如下公式得出:

 

代入各個宏的值後,結果如圖所示:

 

可見,核心將 100~139 的優先順序映射到 200ms~10ms 的時間片上去,優先順序數值越大,則分配的時間片越小。

和 2.4 中進程的預設時間片比較,當 nice 為 0 時,2.6 的基準值 100ms 要大於 2.4 的 60ms。

進程的平均時間片
核心定義進程的平均時間片 AVG_TIMESLICE 為 nice 值為 0 的時間片長度,根據上述公式計算所得大約是 102ms。這一數值將作為進程已耗用時間的一個基準值參與優先順序計算。

2) time_slice 的變化

進程的 time_slice 值代表進程的已耗用時間片剩餘大小,在進程建立時與父進程平分時間片,在運行過程中遞減,一旦歸 0,則按 static_prio 值重新賦予上述基準值,並請求調度。時間片的遞減和重設在時鐘中斷中進行(sched_tick()),除此之外,time_slice 值的變化主要在建立進程和進程退出過程中:

a) 進程建立
和 2.4 類似,為了防止進程通過反覆 fork 來偷取時間片,子進程被建立時並不分配自己的時間片,而是與父進程平分父進程的剩餘時間片。也就是說,fork 結束後,兩者時間片之和與原先父進程的時間片相等。

b) 進程退出
進程退出時(sched_exit()),根據 first_time_slice 的值判斷自己是否從未重新分配過時間片,如果是,則將自己的剩餘時間片返還給父進程(保證不超過 MAX_TIMESLICE)。這個動作使進程不會因建立短期子進程而受到懲罰(與不至於因建立子進程而受到"獎勵"相對應)。如果進程已經用完了從父進程那分得的時間片,就沒有必要返還了(這一點在 2.4 中沒有考慮)。

3) time_slice 對調度的影響

在 2.4 中,進程剩餘時間片是除 nice 值以外對動態優先順序影響最大的因素,並且休眠次數多的進程,它的時間片會不斷疊加,從而算出的優先順序也更大,調度器正是用這種方式來體現對互動式進程的優先策略。但實際上休眠次數多並不表示該進程就是互動,只能說明它是 IO 密集型的,因此,這種方法精度很低,有時因為誤將頻繁訪問磁碟的資料庫應用當作互動式進程,反而造成真正的使用者終端響應遲緩。

2.6 的調度器以時間片是否耗盡為標準將就緒進程分成 active、expired 兩大類,分別對應不同的就緒隊列,前者相對於後者擁有絕對的調度優先權--僅當active 進程時間片都耗盡,expired 進程才有機會運行。但在 active 中挑選進程時,調度器不再將進程剩餘時間片作為影響調度優先順序的一個因素,並且為了滿足核心可剝奪的要求,時間片太長的非即時互動式進程還會被人為地分成好幾段(每一段稱為一個運行粒度,定義見下)運行,每一段運行結束後,它都從 cpu 上被剝奪下來,放置到對應的 active 就緒隊列的末尾,為其他具有同等優先順序的進程提供啟動並執行機會。

這一操作在 schedule_tick() 對時間片遞減之後進行。此時,即使進程的時間片沒耗完,只要該進程同時滿足以下四個條件,它就會被強制從 cpu 上剝奪下來,重新入隊等候下一次調度: 進程當前在 active 就緒隊列中; 該進程是互動式進程(TASK_INTERACTIVE()返回真,見"更精確的互動式進程優先",nice 大於 12 時,該宏返回恒假); 該進程已經耗掉的時間片(時間片基準值減去剩餘時間片)正好是運行粒度的整數倍; 剩餘時間片不小於運行粒度

運行粒度的定義運行粒度 TIMESLICE_GRANULARITY 被定義為與進程的 sleep_avg 和系統總 CPU 數相關的宏。因為 sleep_avg 實際上代表著進程的非已耗用時間與已耗用時間的差值,與互動程度判斷關係密切,所以,運行粒度的定義說明了核心的以下兩個調度策略: 進程互動程度越高,運行粒度越小,這是互動式進程的運行特點所允許的;與之對應,CPU-bound 的進程為了避免 Cache 重新整理,不應該分區; 系統 CPU 數越多,運行粒度越大。

5. 最佳化了的優先順序計算方法

在 2.4 核心中,優先順序的計算和候選進程的選擇集中在調度器中進行,無法保證調度器的執行時間,這一點在前面介紹 runqueue 資料結構的時候已經提及。2.6 核心中候選進程是直接從已按演算法排序的優先順序隊列數組中選取出來的,而優先順序的計算則分散到多處進行。這一節分成兩個部分對這種新的優先順序計算方法進行描述,一部分是優先順序計算過程,一部分是優先順序計算(以及進程入隊)的時機。

1) 優先順序計算過程

動態優先順序的計算主要由 effect_prio() 函數完成,該函數實現相當簡單,從中可見非即時進程的優先順序僅決定於靜態優先順序(static_prio)和進程的sleep_avg 值兩個因素,而即時進程的優先順序實際上是在 setscheduler() 中設定的(詳見"調度系統的即時效能",以下僅考慮非即時進程),且一經設定就不再改變。相比較而言,2.4 的 goodness() 函數甚至要更加複雜,它考慮的 CPU Cache 失效開銷和記憶體切換的開銷這裡都已經不再考慮。

2.6 的動態優先順序演算法的實現關鍵在 sleep_avg 變數上,在 effective_prio() 中,sleep_avg 的範圍是 0~MAX_SLEEP_AVG,經過以下公式轉換後變成-MAX_BONUS/2~MAX_BONUS/2 之間的 bonus:

 

如下圖所示:

 

再用這個 bonus 去減靜態優先順序就得到進程的動態優先順序(並限制在 MAX_RT_PRIO和MAX_PRIO 之間),bonus 越小,動態優先順序數值越大,優先順序越低。也就是說,sleep_avg 越大,優先順序也越高。

MAX_BONUS 定義為 MAX_USER_PRIO*PRIO_BONUS_RATIO/100,也就是說,sleep_avg 對動態優先順序的影響僅在靜態優先順序的使用者優先順序區(100~140)的1/4區間(±5)之內,相對而言,靜態優先順序,也就是使用者指定的 nice 值在優先順序計算的比重要大得多。這也是 2.6 調度系統中變化比較大的一個地方,調度器傾向於更多地由使用者自行設計進程的執行優先順序。

sleep_avg 反映了調度系統的兩個策略:互動式進程優先和分時系統的公平共用,在下一節中我們還要專門分析。

2) 優先順序計算時機

優先順序的計算不再集中在調度器選擇候選進程的時候進行了,只要進程狀態發生改變,核心就有可能計算並設定進程的動態優先順序:

a) 建立進程

在wake_up_forked_process()中,子進程繼承了父進程的動態優先順序,並添加到父進程所在的就緒隊列中。

如果父進程不在任何就緒隊列中(例如它是 IDLE 進程),那麼就通過 effect_prio() Function Compute出子進程的優先順序,而後根據計算結果將子進程放置到相應的就緒隊列中。

b) 喚醒休眠進程

核心調用 recalc_task_prio() 設定從休眠狀態中醒來的進程的動態優先順序,再根據優先順序放置到相應就緒隊列中。

c) 調度到從 TASK_INTERRUPTIBLE 狀態中被喚醒的進程

實際上此時調度器已經選定了候選進程,但考慮到這一類型的進程很有可能是互動式進程,因此此時仍然調用 recalc_task_prio() 對該進程的優先順序進行修正(詳見"進程平均等待時間 sleep_avg"),修正的結果將在下一次調度時體現。

d) 進程因時間片相關的原因被剝奪 cpu

在 schedule_tick() 中(由時鐘中斷啟動),進程可能因兩種原因被剝奪 cpu,一是時間片耗盡,一是因時間片過長而分段。這兩種情況都會調用effect_prio() 重新計算優先順序,重新入隊。

e) 其它時機

這些其它時機包括 IDLE 進程初始化(init_idle())、Server Load Balancer(move_task_away(),詳見"調度器相關的Server Load Balancer")以及修改 nice 值(set_user_nice())、修改調度策略(setscheduler())等主動要求改變優先順序的情況。

由上可見,2.6 中動態優先順序的計算過程在各個進程運行過程中進行,避免了類似 2.4 系統中就緒進程很多時計算過程耗時過長,從而無法預計進程的回應時間的問題。同時,影響動態優先順序的因素集中反映在 sleep_avg 變數上。

6. 進程平均等待時間 sleep_avg

進程的 sleep_avg 值是決定進程動態優先順序的關鍵,也是進程互動程度評價的關鍵,它的設計是 2.6 調度系統中最為複雜的一個環節,可以說,2.6 調度系統的效能改進,很大一部分應該歸功於 sleep_avg 的設計。這一節,我們將專門針對 sleep_avg 的變化和它對調度的影響進行分析。

核心中主要有四個地方會對 sleep_avg 進行修改:休眠進程被喚醒時(activate_task()調用 recalc_task_prio() 函數)、TASK_INTERRUPTIBLE 狀態的進程被喚醒後第一次調度到(schedule()中調用 recalc_task_prio())、進程從 CPU 上被剝奪下來(schedule()函數中)、進程建立和進程退出,其中recalc_task_prio() 是其中複雜度最高的,它通過計算進程的等待時間(或者是在休眠中等待,或者是在就緒隊列中等待)對優先順序的影響來重設優先順序。

1) 休眠進程被喚醒時

此時 activate_task() 以喚醒的時間作為參數調用 recalc_task_prio(),計算休眠等待的時間對優先順序的影響。

在 recalc_task_prio() 中,sleep_avg 可能有四種賦值,並最終都限制在 NS_MAX_SLEEP_AVG 以內:

a) 不變

從 TASK_UNINTERRUPTIBLE 狀態中被喚醒(activated==-1)、互動程度不夠高(!HIGH_CREDIT(p))的使用者進程(p->mm!=NULL)),如果它的 sleep_avg 已經不小於 INTERACTIVE_SLEEP(p) 了,則它的 sleep_avg 不會因本次等待而改變。

b) INTERACTIVE_SLEEP(p)

從 TASK_UNINTERRUPTIBLE 狀態中被喚醒(activated==-1)、互動程度不夠高(!HIGH_CREDIT(p))的使用者進程(p->mm!=NULL)),如果它的 sleep_avg 沒有達到 INTERACTIVE_SLEEP(p),但如果加上本次休眠時間 sleep_time 就達到了,則它的 sleep_avg 就等於 INTERACTIVE_SLEEP(p)。

c) MAX_SLEEP_AVG-AVG_TIMESLICE

使用者進程(p->mm!=NULL),如果不是從 TASK_UNINTERRUPTIBLE 休眠中被喚醒的(p->activated!=-1),且本次等待的時間(sleep_time)已經超過了 INTERACTIVE_SLEEP(p),則它的 sleep_avg 置為 JIFFIES_TO_NS(MAX_SLEEP_AVG-AVG_TIMESLICE)。

d) sleep_avg+sleep_time

如果不滿足上面所有情況,則將 sleep_time 疊加到 sleep_avg 上。此時,sleep_time 要經過兩次修正:

i. 根據

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.