作業系統實驗之處理機排程
實驗要求
- 選擇1~3種程序排程演算法(先來先服務、短作業優先、最高響應比優先、時間片輪轉、優先順序法等)模擬實現程序排程功能;
- 能夠輸入程序的基本資訊,如程序名、到達時間和執行時間等;
- 根據選擇的排程演算法顯示程序排程佇列;
- 根據選擇的排程演算法計算平均週轉時間和平均帶權週轉時間。
我選擇了先來先服務FCFS、短作業優先SJF、最高響應比優先HRN、時間片輪轉RR四種排程演算法,使用java實現。
後面排程演算法均用縮寫表示。
原始碼已上傳到本人github上,建議先看原始碼。
實驗原理
程式碼結構
- Task類用來存放每個任務的詳細資訊,其私有成員記錄任務名字、提交時間執行時間、已執行時間、完成時間、週轉時間、帶權週轉時間,靜態私有成員存放平均週轉時間、平均帶權週轉時間以及任務總數。Task類的構造方法需要任務名、到達時間和執行時間。Task類其餘方法都是setter和getter。
- CPU類表示處理機,私有成員包含排程演算法、當前時間、時間片長度(RR演算法需要用到)任務池、已到達任務池、已到達任務佇列(RR演算法需要用到)。核心的方法包括四個排程演算法以及根據四個排程演算法程式碼中可複用的部分封裝出來的程式碼,比如更新已到達任務池、處理任務、列印時間等等,後續提及時會詳細講解。
程式碼思路
整體思路
和前面的程序排程不同,處理機排程實現的難點在於確定時間的流動,因為CPU何時會處理某個任務和排程演算法有關,而只有知道這個時間才能確定該任務的完成時間,所以CPU類中timeAt用於存放當前時間,在有任務到達或者有任務完成的時候對其進行更新。
CPU類中的任務池和已到達任務池是兩個Set,均用HashSet實現;已到達任務佇列是Queue,用LinkedList實現。
- 任務池: 所有任務的集合。每當完成一個任務就從該集合中刪掉此任務,當任務池中沒有任務則意味著所有任務已完成。
- 已到達任務池:所有已到達任務的集合。所有已經到達且尚未被執行的任務都在此集合裡,故演算法排程主要是操作已到達任務池中的任務,每完成一個任務便從該集合中刪除掉此任務。
- 已到達任務佇列:和已到達任務池類似,RR排程演算法需要一個佇列來輪轉,每當有任務到達,便從已到達任務池中將任務加到已到達任務佇列的隊尾。隊首的任務會被CPU處理一個時間片,然後出列並放到隊尾,直到該任務被處理完畢。
由於各個排程演算法中程式碼可以複用的部分比較多,比如列印時間、填寫各個任務的時間等,筆者在重構多次了完成了一些封裝,會在後面的演算法中具體指出。
FCFS
先來先服務演算法向來是被認為是最簡單的演算法,然而這個排程演算法我重構了近三次,因為寫到後面才會發現FCFS的思想是其他演算法都會用到的,每個排程演算法都需要找到最先到達CPU的任務。對於FCFS而言,其過程不過是重複這個過程而已,所以這裡程式碼看起來非常簡短,邏輯也很清晰:只要任務池不為空,那就不停的去處理當前第一個到的任務。完成後去設定平均時間然後列印。
private void FCFS() {
while (!tasks.isEmpty()) {
processFirstOne();
}
setAvgTime();
printAvg();
}
processFirstOne
處理第一個任務又包括找到第一個任務,設定當前時間,處理該任務,從任務池刪除該任務以及設定時間和列印時間。不難看到這個方法依然是很多小方法的呼叫。這裡筆者做了比較多的封裝來解耦,具體理由可以在後面的排程演算法中發現排程演算法基本都需要找到處理當前第一個到達的任務,這樣做有利於程式碼複用。
關於這裡設定當前時間,因為我們不知道當前任務完成後,下一個任務是緊接著就來了還是會隔一段時間才來(即任務與任務之間間隔了一段時間,cpu沒有處理任何任務)。所以需要判斷下一個任務的到達時間和當前時間哪個更大,如果任務早就到達了,那麼當前時間就保持不變即可;反之如果任務到達的比較慢則把當前時間調整為下一個任務的到達時間。
private void processFirstOne() {
if (!tasks.isEmpty()) {
Task chosenOne = findFirstOne();
this.timeAt = Math.max(this.timeAt, chosenOne.getArriveTime());
//執行作業
processingJob(chosenOne);
tasks.remove(chosenOne);
setAllTime(chosenOne);
printAllTime(chosenOne);
}
}
關於processingJob和findFirstOne方法其實沒有太多需要講解的地方,前者只是一串字串的輸出,而後者也不過是老生常談的Set遍歷。
SJF
SJF排程演算法也就是找出當前已到達任務中執行時間最短的並處理。
updateArrivedTasks()是更新已到達任務。即對比當前時間,將所有到達時間<當前時間的任務(也就是已經到達了的任務)放在已到達任務池裡。
這裡又對這部分邏輯做了封裝,理由也同樣是有利於程式碼複用,後面的HRN與RR排程都會用到。
只要任務池不為空,那麼首先更新已到達任務看看有沒有已經到達的任務,然後對已到達任務池進行遍歷,並找出執行時間最短的任務執行,執行完成後從任務池與已到達任務池中刪除,然後計算並設定該任務的週轉時間完成時間云云……如果已到達任務池沒有任何任務到達,那麼就去找當前時間第一個到達的任務去執行。
private void SJF() {
while (!tasks.isEmpty()) {
//將早於當前時間的所有任務加入已到達任務池,後續任務從已到達任務池中選取並執行,直到已到達任務池中沒有任務
updateArrivedTasks();
//找出已到達任務池中符合條件的作業並執行
while (!arrivedTasks.isEmpty()) {
Iterator<Task> iterator = arrivedTasks.iterator();
Task chosenOne = iterator.next();
while (iterator.hasNext()) {
Task t = iterator.next();
if (chosenOne.getRunTime() > t.getRunTime())
chosenOne = t;
}
//執行作業
processingJob(chosenOne);
//從已到達任務池和任務池中清除
arrivedTasks.remove(chosenOne);
tasks.remove(chosenOne);
setAllTime(chosenOne);
printAllTime(chosenOne);
}
//已到達任務池中沒有任務可執行時,找到一個當前時間下最先到達了的作業
processFirstOne();
}
setAvgTime();
printAvg();
}
HRN
對比SJF的程式碼來看,HRN的實現和SJF的結構幾乎沒有區別,唯一的區別在於判斷條件不再是找到一個執行時間最短的,而是找到一個響應比最高的任務。
這裡再複習一下最高響應比的公式:HRN=1+響應時間/執行時間
while (!tasks.isEmpty()) {
updateArrivedTasks();
while (!arrivedTasks.isEmpty()) {
Iterator<Task> iterator = arrivedTasks.iterator();
Task chosenOne = iterator.next();
while (iterator.hasNext()) {
Task t = iterator.next();
double chosenOneHRN = 1 + (this.timeAt - chosenOne.getArriveTime()) / chosenOne.getRunTime();
double tHRN = 1 + (this.timeAt - t.getArriveTime()) / t.getRunTime();
if (tHRN > chosenOneHRN)
chosenOne = t;
}
processingJob(chosenOne);
arrivedTasks.remove(chosenOne);
tasks.remove(chosenOne);
setAllTime(chosenOne);
printAllTime(chosenOne);
}
processFirstOne();
}
setAvgTime();
printAvg();
}
RR
時間片輪轉排程屬於比較好玩的,它不同於我前面搭設的架構,但是又能看出其擁有和前面相似的部分。它不同之處在於它不像作業排程每次送給cpu一個任務之後cpu一定會把這個任務執行完,cpu只會對已到達任務佇列的隊首任務處理一個時間片,然後將該任務出列並加到隊尾,然後繼續處理已經到達任務佇列的隊首任務一個時間片……直到佇列中的任務被執行完成才不再將該任務放入隊尾。隨著時間的流動,別的任務也會到達cpu,達到的任務都放在已到達任務佇列的隊尾。
不難發現其實從while開始,程式碼也基本一致,不過這裡多了一個更新已到達任務佇列方法,類似更新已到達任務池方法,不予贅述。和處理任務不同,RR排程只會處理一個時間片,所以筆者寫了處理程序方法讓任務被cpu處理一個時間片,其返回一個boolean告訴程式該任務是否執行完成。若完成了則把它移出任務池並設定時間即可,若該任務沒有完成,那麼就再把它放在隊尾,等待cpu對它繼續處理。
private void RR() {
Scanner in = new Scanner(System.in);
System.out.print("請輸入時間片長度(s):");
this.timeRobin = in.nextDouble();
while (!tasks.isEmpty()) {
updateArrivedTasks();
updateArrivedTasksQueue();
if (!arrivedTasksQueue.isEmpty()) {
Task chosenOne = arrivedTasksQueue.poll();
if (processingProcess(chosenOne)) {
tasks.remove(chosenOne);
setAllTime(chosenOne);
printAllTime(chosenOne);
} else
arrivedTasksQueue.offer(chosenOne);
}
if(arrivedTasksQueue.isEmpty()&&!tasks.isEmpty()) {
Task firstOne=findFirstOne();
this.timeAt=Math.max(this.timeAt,firstOne.getArriveTime());
}
}
setAvgTime();
printAvg();
}
實驗結果
測試用例
FCFS
排程演算法:1.FCFS 2.SJF 3.HRN 4.RR
請選擇排程演算法:1
請輸入任務數量(輸入0將以預設任務引數提交):0
Task1 is running…Finished in 2.0s!
Task1完成時間為10.000s
Task1週轉時間2.000s
Task1帶權週轉時間為1.000s
Task2 is running…Finished in 0.5s!
Task2完成時間為10.500s
Task2週轉時間2.000s
Task2帶權週轉時間為4.000s
Task3 is running…Finished in 0.1s!
Task3完成時間為10.600s
Task3週轉時間1.600s
Task3帶權週轉時間為16.000s
Task4 is running…Finished in 0.2s!
Task4完成時間為10.800s
Task4週轉時間1.300s
Task4帶權週轉時間為6.500s此演算法平均週轉時間為1.725s
此演算法平均帶權週轉時間為6.875sProcess finished with exit code 0
SJF
排程演算法:1.FCFS 2.SJF 3.HRN 4.RR
請選擇排程演算法:2
請輸入任務數量(輸入0將以預設任務引數提交):0
Task1 is running…Finished in 2.0s!
Task1完成時間為10.000s
Task1週轉時間2.000s
Task1帶權週轉時間為1.000s
Task3 is running…Finished in 0.1s!
Task3完成時間為10.100s
Task3週轉時間1.100s
Task3帶權週轉時間為11.000s
Task4 is running…Finished in 0.2s!
Task4完成時間為10.300s
Task4週轉時間0.800s
Task4帶權週轉時間為4.000s
Task2 is running…Finished in 0.5s!
Task2完成時間為10.800s
Task2週轉時間2.300s
Task2帶權週轉時間為4.600s此演算法平均週轉時間為1.550s
此演算法平均帶權週轉時間為5.150sProcess finished with exit code 0
HRN
排程演算法:1.FCFS 2.SJF 3.HRN 4.RR
請選擇排程演算法:3
請輸入任務數量(輸入0將以預設任務引數提交):0
Task1 is running…Finished in 2.0s!
Task1完成時間為10.000s
Task1週轉時間2.000s
Task1帶權週轉時間為1.000s
Task3 is running…Finished in 0.1s!
Task3完成時間為10.100s
Task3週轉時間1.100s
Task3帶權週轉時間為11.000s
Task2 is running…Finished in 0.5s!
Task2完成時間為10.600s
Task2週轉時間2.100s
Task2帶權週轉時間為4.200s
Task4 is running…Finished in 0.2s!
Task4完成時間為10.800s
Task4週轉時間1.300s
Task4帶權週轉時間為6.500s此演算法平均週轉時間為1.625s
此演算法平均帶權週轉時間為5.675sProcess finished with exit code 0
RR
排程演算法:1.FCFS 2.SJF 3.HRN 4.RR
請選擇排程演算法:4
請輸入任務數量(輸入0將以預設任務引數提交):0
請輸入時間片長度(s):0.2
Task1 is running…已執行0.200s,剩餘1.800s 當前時間8.200s
Task1 is running…已執行0.400s,剩餘1.600s 當前時間8.400s
Task1 is running…已執行0.600s,剩餘1.400s 當前時間8.600s
Task1 is running…已執行0.800s,剩餘1.200s 當前時間8.800s
Task2 is running…已執行0.200s,剩餘0.300s 當前時間9.000s
Task1 is running…已執行1.000s,剩餘1.000s 當前時間9.200s
Task2 is running…已執行0.400s,剩餘0.100s 當前時間9.400s
Task3 is running…Finished in0.100s!
Task3完成時間為9.500s
Task3週轉時間0.500s
Task3帶權週轉時間為5.000s
Task1 is running…已執行1.200s,剩餘0.800s 當前時間9.700s
Task2 is running…Finished in0.500s!
Task2完成時間為9.800s
Task2週轉時間1.300s
Task2帶權週轉時間為2.600s
Task4 is running…Finished in0.200s!
Task4完成時間為10.000s
Task4週轉時間0.500s
Task4帶權週轉時間為2.500s
Task1 is running…已執行1.400s,剩餘0.600s 當前時間10.200s
Task1 is running…已執行1.600s,剩餘0.400s 當前時間10.400s
Task1 is running…已執行1.800s,剩餘0.200s 當前時間10.600s
Task1 is running…已執行2.000s,剩餘0.000s 當前時間10.800s
Task1 is running…Finished in2.000s!
Task1完成時間為10.800s
Task1週轉時間2.800s
Task1帶權週轉時間為1.400s此演算法平均週轉時間為1.275s
此演算法平均帶權週轉時間為2.875sProcess finished with exit code 0
馬後炮
除錯過程
其實處理機排程我本打算寫成多執行緒實現。把每個任務都當作一個執行緒,cpu作為資源被每個程序搶佔,而具體搶佔資源的規則則根據不同調度算法制定。在我的github上,處理機排程這部分有兩個包,一個是easy即本文的原始碼,另一個包是multithread,只完成了FCFS和SJF演算法執行緒之間的競爭,能夠輸出正確的順序。但是問題出現在每個任務等待的時間無法精確獲得,也就是程序在wait()方法掛起後到被喚醒並被cpu處理這個過程的時間算不準。我程式碼上用的是getCurrentMills()方法,在wait()的前一句記錄時間,在被喚醒後的第一時間記錄時間,兩者相減的結果不盡人意(程式碼執行時有sleep模擬任務執行過程)。猜想是執行程式碼本身是需要時間的,同時執行緒競爭也花了時間等原因導致算不準,以後有空再來填這個坑。
在對浮點型型別數值運算的時候需要注意其表示問題,因為很多數字浮點型是無法準確表示的。比如在updateArrivedTasks()程式碼裡,比較當前時間>=任務到達時間時有一個小細節需要注意:
一開始的思路是:
this.timeAt>=t.getArriveTime()
正確做法是:
Math.abs(this.timeAt-t.getArriveTime())<0.000001||this.timeAt>t.getArriveTime()
理由很簡單,因為浮點型數值比較相等時會有精度失真問題,所以比較浮點型相等不能直接比較。舉例來說,二進位制無法表示8.5,在除錯過程中發現this.timeAt為8.49999999……,小於t.getArriveTime()的8.5,然而實際上我們是希望這兩者相等的。
解決方案也比較粗暴,在一定範圍內認為其相等即可,至於大於的情況那必然是大於的。
存在的問題
RR排程演算法中,老師提出當某個任務A處理的時間片到達的瞬間,另一個任務B正好到達,則應當是先把任務B放入已到達任務佇列的隊尾,然後才把任務A放入已到達任務佇列的隊尾,而在我的程式碼中是先把A放到隊尾,之後才更新已到達佇列並把B再放到隊尾。的確我更新已到達任務佇列的時機有些問題。
小結
構思流程並畫圖是很重要的。
其實筆者還沒有寫到HRN排程演算法的時候,單純的去構思這樣的資料結構時大概花了1個小時左右,這個過程說實話是異常艱辛的,因為我沒有去打草稿畫圖來幫助我理解處理機排程的過程。當時想著一個個寫完了事,於是我寫好FCFS只花了不到10分鐘就完成了,而且經過測試的確可以正常輸出。但是寫到後面就發現這部分程式碼可以封裝,那部分程式碼能複用好多次……這麼一來二去,邊寫邊改邊封裝的效率實在不高。因為腦子並不能把整個結構清晰的記住,往往在寫某個具體的程式碼之後就忘了巨集觀的思路,一開始就埋頭寫程式碼不利於巨集觀觀察問題。而在我把前兩個演算法邊封裝邊寫完成之後,再來看整個程式碼,思路就非常清晰了,HRN排程我甚至只花了15分鐘就完成了。後面的RR演算法由於有些許區別,但我也先畫好了思路再動手,事半功倍。