1. 程式人生 > >Linux程序排程器概述--Linux程序的管理與排程(十五)

Linux程序排程器概述--Linux程序的管理與排程(十五)

記憶體中儲存了對每個程序的唯一描述, 並通過若干結構與其他程序連線起來.

排程器面對的情形就是這樣, 其任務是在程式之間共享CPU時間, 創造並行執行的錯覺, 該任務分為兩個不同的部分, 其中一個涉及排程策略, 另外一個涉及上下文切換.

1 背景知識

1.1 什麼是排程器

通常來說,作業系統是應用程式和可用資源之間的媒介。

典型的資源有記憶體和物理裝置。但是CPU也可以認為是一個資源,排程器可以臨時分配一個任務在上面執行(單位是時間片)。排程器使得我們同時執行多個程式成為可能,因此可以與具有各種需求的使用者共享CPU。

核心必須提供一種方法, 在各個程序之間儘可能公平地共享CPU時間, 而同時又要考慮不同的任務優先順序.

排程器的一個重要目標是有效地分配 CPU 時間片,同時提供很好的使用者體驗。排程器還需要面對一些互相沖突的目標,例如既要為關鍵實時任務最小化響應時間, 又要最大限度地提高 CPU 的總體利用率.

排程器的一般原理是, 按所需分配的計算能力, 向系統中每個程序提供最大的公正性, 或者從另外一個角度上說, 他試圖確保沒有程序被虧待.

1.2 排程策略

傳統的Unix作業系統的都奧杜演算法必須實現幾個互相沖突的目標:

  • 程序響應時間儘可能快

  • 後臺作業的吞吐量儘可能高

  • 儘可能避免程序的飢餓現象

  • 低優先順序和高優先順序程序的需要儘可能調和等等

排程策略(scheduling policy)的任務就是決定什麼時候以怎麼樣的方式選擇一個新程序佔用CPU執行.

傳統作業系統的排程基於分時(time sharing)技術: 多個程序以”時間多路服用”方式執行, 因為CPU的時間被分成”片(slice)”, 給每個可執行程序分配一片CPU時間片, 當然單處理器在任何給定的時刻只能執行一個程序.

如果當前可執行程序的時限(quantum)到期時(即時間片用盡), 而該程序還沒有執行完畢, 程序切換就可以發生.

分時依賴於定時中斷, 因此對程序是透明的, 不需要在承租中插入額外的程式碼來保證CPU分時.

排程策略也是根據程序的優先順序對他們進行分類. 有時用複雜的演算法求出程序當前的優先順序, 但最後的結果是相同的: 每個程序都與一個值(優先順序)相關聯, 這個值表示把程序如何適當地分配給CPU.

在linux中, 程序的優先順序是動態的. 排程程式跟蹤程序正在做什麼, 並週期性的調整他們的優先順序. 在這種方式下, 在較長的時間間隔內沒有任何使用CPU的程序, 通過動態地增加他們的優先順序來提升他們. 相應地, 對於已經在CPU上運行了較長時間的程序, 通過減少他們的優先順序來處罰他們.

1.3 程序飢餓

程序飢餓,即為Starvation,指當等待時間給程序推進和響應帶來明顯影響稱為程序飢餓。當飢餓到一定程度的程序在等待到即使完成也無實際意義的時候稱為飢餓死亡。

產生飢餓的主要原因是

在一個動態系統中,對於每類系統資源,作業系統需要確定一個分配策略,當多個程序同時申請某類資源時,由分配策略確定資源分配給程序的次序。

有時資源分配策略可能是不公平的,即不能保證等待時間上界的存在。在這種情況下,即使系統沒有發生死鎖,某些程序也可能會長時間等待.當等待時間給程序推進和響應帶來明顯影響時,稱發生了程序飢餓,當飢餓到一定程度的程序所賦予的任務即使完成也不再具有實際意義時稱該程序被餓死。

舉個例子,當有多個程序需要列印檔案時,如果系統分配印表機的策略是最短檔案優先,那麼長檔案的列印任務將由於短檔案的源源不斷到來而被無限期推遲,導致最終的飢餓甚至餓死。

2 linux程序的分類

2.1 程序的分類

當涉及有關排程的問題時, 傳統上把程序分類為”I/O受限(I/O-dound)”或”CPU受限(CPU-bound)”.

型別 別稱 描述 示例
I/O受限型 I/O密集型 頻繁的使用I/O裝置, 並花費很多時間等待I/O操作的完成 資料庫伺服器, 文字編輯器
CPU受限型 計算密集型 花費大量CPU時間進行數值計算 圖形繪製程式

另外一種分類法把程序區分為三類:

型別 描述 示例
互動式程序(interactive process) 此類程序經常與使用者進行互動, 因此需要花費很多時間等待鍵盤和滑鼠操作. 當接受了使用者的輸入後, 程序必須很快被喚醒, 否則使用者會感覺系統反應遲鈍 shell, 文字編輯程式和圖形應用程式
批處理程序(batch process) 此類程序不必與使用者互動, 因此經常在後臺執行. 因為這樣的程序不必很快相應, 因此常受到排程程式的怠慢 程式語言的編譯程式, 資料庫搜尋引擎以及科學計算
實時程序(real-time process) 這些程序由很強的排程需要, 這樣的程序絕不會被低優先順序的程序阻塞. 並且他們的響應時間要儘可能的短 視訊音訊應用程式, 機器人控制程式以及從物理感測器上收集資料的程式

注意

前面的兩類分類方法在一定程式上相互獨立

例如, 一個批處理程序很有可能是I/O受限的(如資料庫伺服器), 也可能是CPU受限的(比如圖形繪製程式)

2.2 實時程序與普通程序

在linux中, 排程演算法可以明確的確認所有實時程序的身份, 但是沒辦法區分互動式程式和批處理程式(統稱為普通程序), linux2.6的排程程式實現了基於程序過去行為的啟發式演算法, 以確定程序應該被當做互動式程序還是批處理程序. 當然與批處理程序相比, 排程程式有偏愛互動式程序的傾向

根據程序的不同分類Linux採用不同的排程策略.

對於實時程序,採用FIFO或者Round Robin的排程策略.

對於普通程序,則需要區分互動式和批處理式的不同。傳統Linux排程器提高互動式應用的優先順序,使得它們能更快地被排程。而CFS和RSDL等新的排程器的核心思想是”完全公平”。這個設計理念不僅大大簡化了排程器的程式碼複雜度,還對各種排程需求的提供了更完美的支援.

注意Linux通過將程序和執行緒排程視為一個,同時包含二者。程序可以看做是單個執行緒,但是程序可以包含共享一定資源(程式碼和/或資料)的多個執行緒。因此程序排程也包含了執行緒排程的功能.

linux程序的排程演算法其實經過了很多次的演變, 但是其演變主要是針對與普通程序的, 因為前面我們提到過根據程序的不同分類Linux採用不同的排程策略.實時程序和普通程序採用了不同的排程策略, 更一般的普通程序還需要啟發式的識別批處理程序和互動式程序.

實時程序的排程策略比較簡單, 因為實時程序值只要求儘可能快的被響應, 基於優先順序, 每個程序根據它重要程度的不同被賦予不同的優先順序,排程器在每次排程時, 總選擇優先順序最高的程序開始執行. 低優先順序不可能搶佔高優先順序, 因此FIFO或者Round Robin的排程策略即可滿足實時程序排程的需求.

但是普通程序的排程策略就比較麻煩了, 因為普通程序不能簡單的只看優先順序, 必須公平的佔有CPU, 否則很容易出現程序飢餓, 這種情況下使用者會感覺作業系統很卡, 響應總是很慢.

此外如何程序中如果存在實時程序, 則實時程序總是在普通程序之前被排程

3 linux排程器的演變

一開始的排程器是複雜度為O(n)的始排程演算法(實際上每次會遍歷所有任務,所以複雜度為O(n)), 這個演算法的缺點是當核心中有很多工時,排程器本身就會耗費不少時間,所以,從linux2.5開始引入赫赫有名的O(1)排程器

然而,linux是集全球很多程式設計師的聰明才智而發展起來的超級核心,沒有最好,只有更好,在O(1)排程器風光了沒幾天就又被另一個更優秀的排程器取代了,它就是CFS排程器Completely Fair Scheduler. 這個也是在2.6核心中引入的,具體為2.6.23,即從此版本開始,核心使用CFS作為它的預設排程器,O(1)排程器被拋棄了, 其實CFS的發展也是經歷了很多階段,最早期的樓梯演算法(SD), 後來逐步對SD演算法進行改進出RSDL(Rotating Staircase Deadline Scheduler), 這個演算法已經是”完全公平”的雛形了, 直至CFS是最終被核心採納的排程器, 它從RSDL/SD中吸取了完全公平的思想,不再跟蹤程序的睡眠時間,也不再企圖區分互動式程序。它將所有的程序都統一對待,這就是公平的含義。CFS的演算法和實現都相當簡單,眾多的測試表明其效能也非常優越

欄位 版本
O(n)的始排程演算法 linux-0.11~2.4
O(1)排程器 linux-2.5
CFS排程器 linux-2.6~至今

4 Linux的排程器設計

4.1 linux程序排程器的框架

2個排程器

可以用兩種方法來啟用排程

  • 一種是直接的, 比如程序打算睡眠或出於其他原因放棄CPU

  • 另一種是通過週期性的機制, 以固定的頻率執行, 不時的檢測是否有必要

因此當前linux的排程程式由兩個排程器組成:主排程器週期性排程器(兩者又統稱為通用排程器(generic scheduler)核心排程器(core scheduler))

並且每個排程器包括兩個內容:排程框架(其實質就是兩個函式框架)及排程器類

6種排程策略

linux核心目前實現了6中排程策略(即排程演算法), 用於對不同型別的程序進行排程, 或者支援某些特殊的功能

比如SCHED_NORMAL和SCHED_BATCH排程普通的非實時程序, SCHED_FIFO和SCHED_RR和SCHED_DEADLINE則採用不同的排程策略排程實時程序, SCHED_IDLE則在系統空閒時呼叫idle程序.

idle的執行時機

idle 程序優先順序為MAX_PRIO,即最低優先順序。

早先版本中,idle是參與排程的,所以將其優先順序設為最低,當沒有其他程序可以執行時,才會排程執行 idle

而目前的版本中idle並不在執行佇列中參與排程,而是在cpu全域性執行佇列rq中含idle指標,指向idle程序, 在排程器發現執行佇列為空的時候執行, 調入執行

欄位 描述 所在排程器類
SCHED_NORMAL (也叫SCHED_OTHER)用於普通程序,通過CFS排程器實現。SCHED_BATCH用於非互動的處理器消耗型程序。SCHED_IDLE是在系統負載很低時使用 CFS
SCHED_BATCH SCHED_NORMAL普通程序策略的分化版本。採用分時策略,根據動態優先順序(可用nice()API設定),分配CPU運算資源。注意:這類程序比上述兩類實時程序優先順序低,換言之,在有實時程序存在時,實時程序優先排程。但針對吞吐量優化, 除了不能搶佔外與常規任務一樣,允許任務執行更長時間,更好地使用快取記憶體,適合於成批處理的工作 CFS
SCHED_IDLE 優先順序最低,在系統空閒時才跑這類程序(如利用閒散計算機資源跑地外文明搜尋,蛋白質結構分析等任務,是此排程策略的適用者) CFS-IDLE
SCHED_FIFO 先入先出排程演算法(實時排程策略),相同優先順序的任務先到先服務,高優先順序的任務可以搶佔低優先順序的任務 RT
SCHED_RR 輪流排程演算法(實時排程策略),後者提供 Roound-Robin 語義,採用時間片,相同優先順序的任務當用完時間片會被放到佇列尾部,以保證公平性,同樣,高優先順序的任務可以搶佔低優先順序的任務。不同要求的實時任務可以根據需要用sched_setscheduler() API設定策略 RT
SCHED_DEADLINE 新支援的實時程序排程策略,針對突發型計算,且對延遲和完成時間高度敏感的任務適用。基於Earliest Deadline First (EDF) 排程演算法 DL

linux核心實現的6種排程策略, 前面三種策略使用的是cfs排程器類,後面兩種使用rt排程器類, 最後一個使用DL排程器類

5個排程器類

而依據其排程策略的不同實現了5個排程器類, 一個排程器類可以用一種種或者多種排程策略排程某一類程序, 也可以用於特殊情況或者排程特殊功能的程序.

排程器類 描述 對應排程策略
stop_sched_class 優先順序最高的執行緒,會中斷所有其他執行緒,且不會被其他任務打斷
作用
1.發生在cpu_stop_cpu_callback 進行cpu之間任務migration
2.HOTPLUG_CPU的情況下關閉任務
無, 不需要排程普通程序
dl_sched_class 採用EDF最早截至時間優先演算法排程實時程序 SCHED_DEADLINE
rt_sched_class 採用提供 Roound-Robin演算法或者FIFO演算法排程實時程序
具體排程策略由程序的task_struct->policy指定
SCHED_FIFO, SCHED_RR
fair_sched_clas 採用CFS演算法排程普通的非實時程序 SCHED_NORMAL, SCHED_BATCH
idle_sched_class 採用CFS演算法排程idle程序, 每個cup的第一個pid=0執行緒:swapper,是一個靜態執行緒。排程類屬於:idel_sched_class,所以在ps裡面是看不到的。一般執行在開機過程和cpu異常的時候做dump SCHED_IDLE

其所屬程序的優先順序順序為

stop_sched_class -> dl_sched_class -> rt_sched_class -> fair_sched_class -> idle_sched_class

3個排程實體

排程器不限於排程程序, 還可以排程更大的實體, 比如實現組排程: 可用的CPUI時間首先在一半的程序組(比如, 所有程序按照所有者分組)之間分配, 接下來分配的時間再在組內進行二次分配.

這種一般性要求排程器不直接操作程序, 而是處理可排程實體, 因此需要一個通用的資料結構描述這個排程實體,即seched_entity結構, 其實際上就代表了一個排程物件,可以為一個程序,也可以為一個程序組.

linux中針對當前可排程的實時和非實時程序, 定義了型別為seched_entity的3個排程實體

排程實體 名稱 描述 對應排程器類
sched_dl_entity DEADLINE排程實體 採用EDF演算法排程的實時排程實體 dl_sched_class
sched_rt_entity RT排程實體 採用Roound-Robin或者FIFO演算法排程的實時排程實體 rt_sched_class
sched_entity CFS排程實體 採用CFS演算法排程的普通非實時程序的排程實體 fair_sched_class

排程器類的就緒佇列

另外,對於排程框架及排程器類,它們都有自己管理的執行佇列,排程框架只識別rq(其實它也不能算是執行佇列),而對於cfs排程器類它的執行佇列則是cfs_rq(內部使用紅黑樹組織排程實體),實時rt的執行佇列則為rt_rq(內部使用優先順序bitmap+雙向連結串列組織排程實體), 此外核心對新增的dl實時排程策略也提供了執行佇列dl_rq

排程器整體框架

本質上, 通用排程器(核心排程器)是一個分配器,與其他兩個元件互動.

  • 排程器用於判斷接下來執行哪個程序.
    核心支援不同的排程策略(完全公平排程, 實時排程, 在無事可做的時候排程空閒程序,即0號程序也叫swapper程序,idle程序), 排程類使得能夠以模組化的方法實現這些側露額, 即一個類的程式碼不需要與其他類的程式碼互動
    當排程器被呼叫時, 他會查詢排程器類, 得知接下來執行哪個程序

  • 在選中將要執行的程序之後, 必須執行底層的任務切換.
    這需要與CPU的緊密互動. 每個程序剛好屬於某一排程類, 各個排程類負責管理所屬的程序. 通用排程器自身不涉及程序管理, 其工作都委託給排程器類.

每個程序都屬於某個排程器類(由欄位task_struct->sched_class標識), 由排程器類採用程序對應的排程策略排程(由task_struct->policy )進行排程, task_struct也儲存了其對應的排程實體標識

linux實現了6種排程策略, 依據其排程策略的不同實現了5個排程器類, 一個排程器類可以用一種或者多種排程策略排程某一類程序, 也可以用於特殊情況或者排程特殊功能的程序.

排程器類 排程策略 排程策略對應的排程演算法 排程實體 排程實體對應的排程物件
stop_sched_class 特殊情況, 發生在cpu_stop_cpu_callback 進行cpu之間任務遷移migration或者HOTPLUG_CPU的情況下關閉任務
dl_sched_class SCHED_DEADLINE Earliest-Deadline-First最早截至時間有限演算法 sched_dl_entity 採用DEF最早截至時間有限演算法排程實時程序
rt_sched_class SCHED_RR

SCHED_FIFO
Roound-Robin時間片輪轉演算法

FIFO先進先出演算法
sched_rt_entity 採用Roound-Robin或者FIFO演算法排程的實時排程實體
fair_sched_class SCHED_NORMAL

SCHED_BATCH
CFS完全公平懂排程演算法 sched_entity 採用CFS演算法普通非實時程序
idle_sched_class SCHED_IDLE 特殊程序, 用於cpu空閒時排程空閒程序idle

它們的關係如下圖

排程器的組成

5種排程器類為什麼只有3種排程實體

正常來說一個排程器類應該對應一類排程實體, 但是5種排程器類卻只有了3種排程實體?

這是因為排程實體本質是一個可以被排程的物件, 要麼是一個程序(linux中執行緒本質上也是程序), 要麼是一個程序組, 只有dl_sched_class, rt_sched_class排程的實時程序(組)以及fair_sched_class排程的非實時程序(組)是可以被排程的實體物件, 而stop_sched_class和idle_sched_class

為什麼採用EDF實時排程需要單獨的排程器類, 排程策略和排程實體

linux針對實時程序實現了Roound-Robin, FIFO和Earliest-Deadline-First(EDF)演算法, 但是為什麼SCHED_RR和SCHED_FIFO兩種排程演算法都用rt_sched_class排程類和sched_rt_entity排程實體描述, 而EDF演算法卻需要單獨用rt_sched_class排程類和sched_dl_entity排程實體描述

為什麼採用EDF實時排程不用rt_sched_class排程類排程, 而是單獨實現排程類和排程實體?

4.2 程序的排程

首先,我們需要清楚,什麼樣的程序會進入排程器進行選擇,就是處於TASK_RUNNING狀態的程序,而其他狀態下的程序都不會進入排程器進行排程。
系統發生排程的時機如下

  • 呼叫cond_resched()時

  • 顯式呼叫schedule()時

  • 從系統呼叫或者異常中斷返回使用者空間時

  • 從中斷上下文返回使用者空間時

當開啟核心搶佔(預設開啟)時,會多出幾個排程時機,如下

  • 在系統呼叫或者異常中斷上下文中呼叫preempt_enable()時(多次呼叫preempt_enable()時,系統只會在最後一次呼叫時會排程)

  • 在中斷上下文中,從中斷處理函式返回到可搶佔的上下文時(這裡是中斷下半部,中斷上半部實際上會關中斷,而新的中斷只會被登記,由於上半部處理很快,上半部處理完成後才會執行新的中斷訊號,這樣就形成了中斷可重入)

而在系統啟動排程器初始化時會初始化一個排程定時器,排程定時器每隔一定時間執行一箇中斷,在中斷會對當前執行程序執行時間進行更新,如果程序需要被排程,在排程定時器中斷中會設定一個排程標誌位,之後從定時器中斷返回,因為上面已經提到從中斷上下文返回時是有排程時機的,在核心原始碼的彙編程式碼中所有中斷返回處理都必須去判斷排程標誌位是否設定,如設定則執行schedule()進行排程。

而我們知道實時程序和普通程序是共存的,排程器是怎麼協調它們之間的排程的呢,其實很簡單,每次排程時,會先在實時程序執行佇列中檢視是否有可執行的實時程序,如果沒有,再去普通程序執行佇列找下一個可執行的普通程序,如果也沒有,則排程器會使用idle程序進行執行。

之後的章節會放上程式碼進行詳細說明。

系統並不是每時每刻都允許排程的發生,當處於硬中斷期間的時候,排程是被系統禁止的,之後硬中斷過後才重新允許排程。而對於異常,系統並不會禁止排程,也就是在異常上下文中,系統是有可能發生排程的。

4.3 搶佔標識TIF_NEED_RESCHED

核心在檢查need_resched標識TIF_NEED_RESCHED的值判斷是否需要搶佔當前程序, 核心在thread_info的flag中設定了一個標識來標誌程序是否需要重新排程, 即重新排程need_resched標識TIF_NEED_RESCHED, 核心在即將返回使用者空間時會檢查標識TIF_NEED_RESCHED標誌程序是否需要重新排程

系統中每個程序都有一個特定於體系結構的struct thread_info結構, 使用者層程式被排程的時候會檢查struct thread_info中的need_resched標識TLF_NEED_RESCHED標識來檢查自己是否需要被重新排程.

如果核心檢查程序的搶佔標識被設定, 則會在一個關鍵的時刻, 呼叫排程器來完成排程和搶佔的工作

4.4 核心搶佔和使用者搶佔

而根據程序搶佔發生的時機, 搶佔可以分為核心搶佔和使用者搶佔, 核心搶佔就是指一個在核心態執行的程序, 可能在執行核心函式期間被另一個程序取

一般來說,使用者搶佔發生幾下情況:

  • 從系統呼叫返回使用者空間;

  • 從中斷(異常)處理程式返回使用者空間

核心搶佔發生的時機,一般發生在:

  • 當從中斷處理程式正在執行,且返回核心空間之前。當一箇中斷處理例程退出,在返回到核心態時(kernel-space)。這是隱式的呼叫schedule()函式,當前任務沒有主動放棄CPU使用權,而是被剝奪了CPU使用權。

  • 當核心程式碼再一次具有可搶佔性的時候,如解鎖(spin_unlock_bh)及使能軟中斷(local_bh_enable)等, 此時當kernel code從不可搶佔狀態變為可搶佔狀態時(preemptible again)。也就是preempt_count從正整數變為0時。這也是隱式的呼叫schedule()函式

  • 如果核心中的任務顯式的呼叫schedule(), 任務主動放棄CPU使用權

  • 如果核心中的任務阻塞(這同樣也會導致呼叫schedule()), 導致需要呼叫schedule()函式。任務主動放棄CPU使用權

核心搶佔採用同搶佔標識的類似方法被實現, linux核心在thread_info結構中添加了一個自旋鎖標識preempt_count, 稱為搶佔計數器(preemption counter).

struct thread_info
{
    /*  ......  */
    int preempt_count;   /* 0 => preemptable, <0 => BUG */
    /*  ......  */
}
preempt_count值 描述
>0 禁止核心搶佔, 其值標記了使用preempt_count的臨界區的數目
=0 開啟核心搶佔
<0 鎖為負值, 核心出現錯誤

4.5 週期性排程器scheduler_tick

週期排程器

週期性排程器scheduler_tick由核心時鐘中斷週期性的觸發, 週期性排程器以固定的頻率啟用負責當前程序排程類的週期性排程方法, 以保證系統的併發性, 週期性排程器通過呼叫程序所屬排程器類的task_tick操作完成周期性排程的通知和配置工作, 通過resched_curr函式(早期的resched_task函式)設定搶佔標識TIF_NEED_RESCHED來通知核心在必要的時間由主排程函式完成真正的排程工作, 此種做法稱之為延遲排程策略

4.6 主排程器schedule

主排程器

schedule就是主排程器的工作函式, 在核心中的許多地方, 如果要將CPU分配給與當前活動程序不同的另一個程序, 都會直接呼叫主排程器函式schedule或者其子函式__schedule.

__schedule完成搶佔

  • 完成一些必要的檢查, 並設定程序狀態, 處理程序所在的就緒佇列

  • 排程全域性的pick_next_task選擇搶佔的程序

    如果當前cpu上所有的程序都是cfs排程的普通非實時程序, 則直接用cfs排程, 如果無程式可排程則排程idle程序

    否則從優先順序最高的排程器類sched_class_highest(目前是stop_sched_class)開始依次遍歷所有排程器類的pick_next_task函式, 選擇最優的那個程序執行

  • context_switch完成程序上下文切換

    呼叫switch_mm(), 把虛擬記憶體從一個程序對映切換到新程序中

    呼叫switch_to(),從上一個程序的處理器狀態切換到新程序的處理器狀態。這包括儲存、恢復棧資訊和暫存器資訊

4.7 程序上下文切換context_switch

context_switch流程

context_switch其實是一個分配器, 他會呼叫所需的特定體系結構的方法

  • 呼叫switch_mm(), 把虛擬記憶體從一個程序對映切換到新程序中

    switch_mm更換通過task_struct->mm描述的記憶體管理上下文, 該工作的細節取決於處理器, 主要包括載入頁表, 刷出地址轉換後備緩衝器(部分或者全部), 向記憶體管理單元(MMU)提供新的資訊

  • 呼叫switch_to(),從上一個程序的處理器狀態切換到新程序的處理器狀態。這包括儲存、恢復棧資訊和暫存器資訊

    switch_to切換處理器暫存器的呢內容和核心棧(虛擬地址空間的使用者部分已經通過switch_mm變更, 其中也包括了使用者狀態下的棧, 因此switch_to不需要變更使用者棧, 只需變更核心棧), 此段程式碼嚴重依賴於體系結構, 且程式碼通常都是用匯編語言編寫.

為什麼switch_to需要3個引數

在新程序被選中執行時, 核心恢復到程序被切換出去的點繼續執行, 此時核心只知道誰之前將新程序搶佔了, 但是卻不知道新程序再次執行是搶佔了誰, 因此底層的程序切換機制必須將此前執行的程序(即新程序搶佔的那個程序)提供給context_switch. 由於控制流會回到函式的該中間, 因此無法通過普通函式的返回值來完成. 因此使用了一個3個引數, 但是邏輯效果是相同的, 彷彿是switch_to是帶有兩個引數的函式, 而且返回了一個指向此前執行的程序的指標.

switch_to(prev, next, last);

即

prev = last = switch_to(prev, next);

其中返回的prev值並不是做引數的prev值, 而是prev被再次排程的時候搶佔掉的那個程序last.

4.8 處理程序優先順序

核心使用一些簡單的數值範圍0~139表示內部優先順序, 數值越低, 優先順序越高。

從0~99的範圍專供實時程序使用, nice的值[-20,19]則對映到範圍100~139

核心的優先順序表示

其中task_struct採用了三個成員表示程序的優先順序:prio和normal_prio表示動態優先順序, static_prio表示程序的靜態優先順序.

此外還用了一個欄位rt_priority儲存了實時程序的優先順序

欄位 描述
static_prio 用於儲存靜態優先順序, 是程序啟動時分配的優先順序, ,可以通過nice和sched_setscheduler系統呼叫來進行修改, 否則在程序執行期間會一直保持恆定
prio 儲存程序的動態優先順序
normal_prio 表示基於程序的靜態優先順序static_prio和排程策略計算出的優先順序. 因此即使普通程序和實時程序具有相同的靜態優先順序, 其普通優先順序也是不同的, 程序分叉(fork)時, 子程序會繼承父程序的普通優先順序
rt_priority 用於儲存實時優先順序, 實時程序的優先順序用實時優先順序rt_priority來表示

靜態優先順序static_prio(普通程序)和實時優先順序rt_priority(實時程序)是計算的起點

因此他們也是程序建立的時候設定好的, 我們通過nice修改的就是普通程序的靜態優先順序static_prio

首先通過靜態優先順序static_prio計算出普通優先順序normal_prio, 該工作可以由nromal_prio來完成, 該函式定義在kernel/sched/core.c#L861

核心通過effective_prio設定動態優先順序prio, 計算動態優先順序的流程如下

  • 設定程序的普通優先順序(實時程序99-rt_priority, 普通程序為static_priority)

  • 計算程序的動態優先順序(實時程序則維持動態優先順序的prio不變, 普通程序的動態優先順序即為其普通優先順序)

最後, 我們綜述一下在針對不同型別程序的計算結果

程序型別 實時優先順序rt_priority 靜態優先順序static_prio 普通優先順序normal_prio 動態優先順序prio
EDF排程的實時程序 rt_priority 不使用 MAX_DL_PRIO-1 維持原prio不變
RT演算法排程的實時程序 rt_priority 不使用 MAX_RT_PRIO-1-rt_priority 維持原prio不變
普通程序 不使用 static_prio static_prio static_prio
優先順序提高的普通程序 不使用 static_prio(改變) static_prio 維持原prio不變

4.9 喚醒搶佔

當在try_to_wake_up/wake_up_process和wake_up_new_task中喚醒程序時, 核心使用全域性check_preempt_curr看看是否程序可以搶佔當前程序可以搶佔當前執行的程序.

每個排程器類都因應該實現一個check_preempt_curr函式, 在全域性check_preempt_curr中會呼叫程序其所屬排程器類check_preempt_curr進行搶佔檢查, 對於完全公平排程器CFS處理的程序, 則對應由check_preempt_wakeup函式執行該策略.

新喚醒的程序不必一定由完全公平排程器處理, 如果新程序是一個實時程序, 則會立即請求排程, 因為實時程序優先極高, 實時程序總會搶佔CFS進

核心為了實現完全公平, 對一些互動式程序有補償機制, 這些互動式程序多數情況下屬於睡眠狀態, 只有在接收到訊號以後被喚醒, 比如vim在接收了鍵盤錄入的訊號後被喚醒, 完成工作後又進入睡眠態, 因此我們需要對喚醒的程序做一些補償, 關於補償的內容我們會在各個排程器類的設計中講解.