操作系統3:CPU調度
基礎
CPU利用率=CPU充分利用時間/CPU使用總時間
來源:https://www.zhihu.com/question/266544961
Schedule和dispatch的區別?
用“主要業務邏輯”做在哪里來區分。
schedule: 調度。 scheduler是具有重業務邏輯的,也就是調度算法。例如,linux kernel從中斷出來后,會切入到scheduler routine,因為這里面的邏輯很多(CFS,RR,FIFO等調度算法),所以叫做 調度。
dispatch: 分發、派發。 dispatcher是輕業務邏輯或者沒有業務邏輯,純粹簡單的轉發、派發。例如,負載均衡的dispatcher收到請求后,通過哈希表找到response server,直接將請求派發下去,本身並不做業務邏輯。
有的操作系統scheduler和dispatcher是分開的,有的是合二為一的,例如后面要講的linux版本中就是
調度器調度的時機舉例:
第一種是因為該進程需要資源,所以處於等待狀態
第二種是時間片被搶走了,例如有一個高優先級的進程進入就緒隊列
第三種是需要的資源到了,因為優先級比較高,搶奪了時間片。這種和第二種其實是一回事
還有其他的許多情況需要調用調度器,例如CPU時間片到期等等
非搶占式:進程自願交出CPU資源引起的調度
分配器做的事情不涉及到決策,是固定的事情
分配延時應該盡可能減少
等待時間:一個進程的任務很可能是不能在一次就執行完的,可能需要多次等待,所以這是個累計時間
響應時間:到調度器開始響應就算完,不需要完成響應
先來先服務算法(FCFS)
原理和名稱一樣,用一個隊列就可以了
顯然這個例子里平均等待時間不是最優的,但是CPU利用率的確是100%
換一下順序,平均等待時間就好多了:
最短優先算法(SJF)
這個算法倒是能使得平均等待時間最短
也可以是搶占式的:
每次有進程進入就緒狀態時,比較時是和原來在執行的進程的剩余執行所需時間相比
兩種策略對比:
但是這種算法有致命問題:進程是沒辦法准確預報自己花費的CPU時間的(執行時間可能和資源和實際情況有關),所以只能停留在理論上23333
但是也不是完全不能預估,只不過就是要基於概率模型了,也就是有預估錯誤的可能性:
tn表示第n次的真實時間
最高響應比優先算法(HRN)
W表示該進程自從進入就緒隊列至今的等待時間
HRN是大於等於1的,基准值是1
總是為相對等待時間較長的進程服務,這個“相對”表示的是相對於它的預估的CPU時間:需要CPU時間長的,就應該等待的長一點,反之,就應該短一點
優先權法
這是比較流行的一種方法
在PCB中加一個優先數
可以使用堆來每次獲得優先級最高的進程
優先權法的一個問題:
從上也可以看出來不是什么大問題,所以這種算法是比較好的,也是在實際操作系統中比較常用的一種算法
輪轉法
這種方法最原始的思想就是這樣的,眾生平等,輪流來
這種思路的一個好處是,任何一個進程的等待時間是有上限的,所以這種算法也是很流行的
分配給不同的進程時每次切換都要做一次上下文切換,對效率是有影響的
所以時間片長度的選擇是很重要的
q大的話完成一個進程不需要做切換,就是先到顯得了(FCFS),也不好
多層隊列法
就緒隊列中的進程是有不同的訴求的,有的側重於低響應時間,有的側重於充足的處理時間
前台側重於響應時間,使用輪轉法;后台對響應時間不那么敏感,使用先來先服務算法
不同隊列間有不同的優先級
- 系統調度要盡量實時響應
- 交互要盡量及時
- 批處理和用戶交互沒什么關系,所以響應時間沒那么重要
現在時間片的競爭主要是隊列間的競爭
多層反饋隊列法
反饋指的是就緒進程在隊列間的遷移,這主要考慮到進程在不同情況下、任務下對實時性的需求是有變化的,有時候重交互,有時候重計算
舉一個例子:
注意這只是調度方法中的一種,是否合理要看實際需求能否被滿足
如何設計一個多層反饋隊列:
實時調度
希望調度要很及時,但是不同任務的及時性的要求是不一樣的
但是這一點是很難的,特別是通用性的操作系統,幾乎不可能實現及時性(因為不同的進程要求不一樣,用統一的調度方法當然不可能完全合適)
- 在嵌入式系統中使用硬實時調度,也就是時間節點是明確的,如果沒有按時完成就作廢
- 通用操作系統一般滿足的是軟實時調度,只能是盡量滿足
調度算法的評估
要設計評估方法,盡量在早期就能對調度算法進行評估,不能依賴實際測試(因為開發周期長),所以下圖中的第三種方案一般不靠譜。由於實際情況是非常復雜的,要確定和理論計算非常難,所以確定模型法也一般不靠譜
排隊模型:先建立一個模型來模擬實際情況對算法進行測試,但是這對於模型的准確性就有了要求,要求其能盡可能反映實際使用場景,但是模型的准確率也是不容易很高的
仿真:采集實際情況的數據,臨時搭建平台進行測試,采集指標
這種方法的效果還是不錯的,理論難度也比較低,是一種偏工程性的方法
linux調度算法
內存空間有4G,其中3G是用戶空間,1G是內核空間
將優先權法和輪轉法相結合:時間片到了之后引起中斷響應,中斷時從當前執行的進程的PCB拿出來,訪問counter並減一,如果不等於0,就該進程繼續響應;如果減到0,就重新設置counter(不設置統一的值,可以給每個進程設置一個值)、調用scheduler基於優先級重新調度
這是Linux較為早期時候的調度算法
sched_data:表示該CPU最近在為哪個進程在服務,該進程的上下文就保存在這里,這是為了多CPU而設置的
prev指向調度前的進程的PCB,next是將要調度給的進程的PCB,p是一個中間變量
tmp:用於優化用,沒有實際含義
this_cpu:當前進程使用的是哪個CPU
c:臨時變量,保存進程的優先權
和調度無關,如果有錯就跳出,不繼續調度
current指向當前執行進程的PCB。正如前面所說的那樣,Linux操作系統中沒有狀態字來表示進程是正在執行的,TASK_RUNNING的含義是就緒狀態。所以這里就用current來指向正在執行的進程的PCB
如果正在處於中斷,就不繼續調度了,直接返回
和同步相關
softirq:軟中斷。這里表示如果有軟中斷的工作還沒有完成,就先去做這些工作
和調度直接相關。
輪轉法:
如果prev的counter變成了0,就重新設置,注意設置時候傳入的參數是prev->nice,也就是那個進程自己給自己設置的優先級,基於此(當然不光只考慮這一個參數)來重新設置counter
state表示進程狀態
default處理其他狀態的情況,從就緒隊列中刪除,不再繼續執行
TASK_INTERRUPTABLE這個是怎么回事呢?它是為了處理這樣一種特殊情況:
當前進程在執行的過程中需要進行一次中斷,例如訪問IO,但是正在它將自己的狀態由TASK_RUNNING換為TASK_INTERRUPTABLE后,CPU時間片到期,通過中斷發生了進程調度,它的counter也變成了0,被從current變成prev,加入了就緒隊列的末尾。此時將它從TASK_INTERRUPTABLE狀態恢復(也就是從等待隊列中除去),也就是對於它的原有中斷要放在之后再響應了
因為正在做調度(因為調度也是需要CPU時間的),所以不允許在下個時間片發生schedule()
先將next的初值置為idle task,也就是初始進程,並將其優先級設置為最小的。意思就是一般不調度這個原始進程,如果沒有其他進程需要調度的話才調度,這個原始進程做一些進程監測和管理之類的工作
選擇最高優先級的部分在still_running_back中完成
看算法,就是一次遍歷
這種情況出現在所有進程的goodness都是0的情況下,意思就是就緒隊列中的所有進程的counter都是0了,也就是時間片的時間都已經到了
recalculate:
注意這里使用了一個C語言的特性——復合語句,就是花括號括起來的部分,和代碼塊的概念比較類似。這里定義的局部變量只能在這里面使用,外部無法訪問
>>1
表示右移一位,也就是除2
然后再執行上面的still_running_back,就一定能選出c和next了
開始布置現場
這種不需要內存管理切換和上下文切換,所以之后到最后一步
kstat.content_switch記錄了從開機以來的上下文切換次數
prepare_to_switch是做上下文切換的准備工作
注意一點,648行完了執行的還是649行嗎?其實不是的,因為在648行已經完成了寄存器的轉換,此時PC寄存器中保存的已經是另外的進程的PCB中的PC值了。649行何時執行呢?只有當這一次失去時間片的進程再次被調度才會被執行(會不會有不切換的情況呢,也就是獲得調度的進程就是當前進程?這是不會的,因為看前面,如果不切換的話會直接從前面就調到same_process)
判斷是否有調度需求,如果沒有的話schedule()函數就結束了,返回內核中調用該函數處。
由此可見,schedule函數其實還做了dispatch的工作
goodness函數
什么是實時進程
https://blog.csdn.net/helloanthea/article/details/28877221
Linux的進程分普通進程和實時進程,普通進程即非實時進程SCHED_OTHER或SCHED_NORMAL,而實時進程又分SCHED_FIFO與SCHED_RR,實時進程的優先級(099)都比普通進程的優先級(100139)高,且直到死亡之前始終是活動進程,當系統中有實時進程運行時,普通進程幾乎是無法分到時間片的(只能分到5%的CPU時間)。

out是什么呢:
out其實就是程序出口
將時間片的剩余值作為優先權,剩余時間越多越優先執行
mm是進程中管理內存的數據結構
優先權增加是因為不需要切換內存管理空間,上下文切換起來會更快
由此可見這里的nice是越小越好
評價
時間復雜度為O(n),n是就緒隊列中的進程個數,這個應該比較好想,這個時間開銷也還行,所以就一直用了。
但是有更好的:
優化
創建一個鏈表數組,數組的每個位置代表一個優先級,就緒狀態的進程直接掛到數組的對應位置上。然后設置一個字節,表示當前有進程的最高的優先級是多少,這樣調度的時候直接去找就好了。這種方法的復雜度是O(1)的
但是如果只使用一個數組的話,就會發生,高優先級的進程未完成的話會被反復調度,低優先級的進程無法獲得時間片。所以實際上是設置了兩個數組的,一個是active數組,一個是non_active數組,選擇只在active數組進行,調度完成的進程回到non_active數組。只有當active數組上的進程都被調度一遍后,active數組和non_active數組交換(這是很簡單的,只需要指向數組的指針交換一下就好)
但是這其實也不是十全十美的,例如它還有公平性的問題:有的應用程序可以開多進程從而有較大的可能性被調度執行,之后版本的Linux在此有了優化