操作系統實驗之處理機調度

實驗要求

  • 選擇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.875s

Process 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.150s

Process 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.675s

Process 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.875s

Process 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算法由於有些許區別,但我也先畫好了思路再動手,事半功倍。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章