日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > windows >内容正文

windows

【操作系统】Nachos 多道程序设计

發布時間:2023/12/10 windows 26 豆豆
生活随笔 收集整理的這篇文章主要介紹了 【操作系统】Nachos 多道程序设计 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

2021SC@SDUSC

文章目錄

  • 題目要求
    • 題目 1
    • 題目 2
    • 題目 3
    • 題目 4
  • 具體實現
    • 關于交叉編譯
    • 基本配置
    • 題目 1
    • 題目 2
    • 題目 3
    • 題目 4
  • 源碼

題目要求

英文原版見官網。

Nachos 的第二個階段是支持多道程序設計。和第一次作業一樣,我們給你一些你需要的代碼;你的任務就是完成系統并加強它。到目前為止,你為 Nachos 編寫的所有代碼都是操作系統內核的一部分。在一個真正的操作系統中,內核不僅在內部使用它的程序,而且允許用戶級程序通過系統調用訪問它的一些程序。

第一步是閱讀和理解我們為你編寫的系統的一部分。內核文件在nachos.userprog包中,還有一些額外的機器模擬類會被使用:

  • Processor模擬了一個 MIPS 處理器;
  • SerialConsole模擬了一個串行控制臺(用于鍵盤輸入和文本輸出);
  • FileSystem是一個文件系統接口。要訪問文件,請使用Machine.stubFileSystem()返回的FileSystem。該文件系統可訪問test目錄中的文件。

這項任務的新內核文件包括:

  • UserKernel.java:一個多程序的內核;
  • UserProcess.java:一個用戶進程;管理地址空間,并將一個程序加載到虛擬內存;
  • UThread.java:一個能夠執行用戶 MIPS 代碼的線程;
  • SynchConsole.java:一個同步的控制臺;使得在多個線程之間共享機器的串行控制臺成為可能。

在這項作業中,我們給你一個模擬的 CPU,它模擬了一個真實的 CPU(MIPS R3000芯片)。通過模擬執行,我們可以完全控制一次執行多少條指令,地址轉換如何進行,以及如何處理中斷和異常(包括系統調用)。我們的模擬器可以運行由 C 語言編譯成 MIPS 指令集的正常程序。唯一需要注意的是,不支持浮點運算。

我們提供的代碼可以一次運行一個用戶級的 MIPS 程序,并且只支持一個系統調用:halt。halt所做的就是要求操作系統關閉機器。這個測試程序可以在test/halt.c中找到,它代表了最簡單的支持 MIPS 的程序。

我們在 Nachos 發行版的test目錄中提供了其他幾個 MIPS 示例程序。你可以使用這些程序來測試你的實現,或者你可以編寫新的程序。當然,在你實現適當的內核支持之前,你將無法運行利用 I/O 等功能的程序!這將是你在本項目中的任務。

test目錄包括 C 源文件(.c文件)和 Nachos 用戶程序二進制文件(.coff文件)。二進制文件可以在test目錄下通過運行gmake構建,也可以從proj2目錄下通過運行gmake test構建。

要運行halt程序,請到test目錄再gmake;然后到proj2目錄,gmake,并運行nachos -d ma。追蹤用戶程序被加載、運行和調用系統調用時發生的情況('m'調試標志啟用 MIPS 反匯編,'a'調試標志打印出進程加載信息)。

為了編譯測試程序,你需要一個 MIPS 交叉編譯器。在教學機上已經安裝了 mips-gcc(詳見測試目錄中的 Makefile)。如果你沒有使用教學機,你必須下載適當的交叉編譯器并相應地設置 ARCHDIR 環境變量。

構建一個與 Nachos 兼容的 MIPS 二進制文件有多個階段(所有這些都由test Makefile處理):

  • 源文件(*.c)被mips-gcc編譯成對象文件(*.o);
  • 一些對象文件被鏈接到libnachos.a,即 Nachos 標準庫;
  • start.s被預處理并組裝成start.o。該文件包含初始化進程的匯編語言代碼。它還提供了系統調用的 “存根代碼”,使系統調用得以調用。這就利用了 MIPS 的特殊指令syscall,它可以向 Nachos 內核捕捉調用系統調用的信息;
  • 一個對象文件與libnachos.a鏈接,產生一個與 Nachos 兼容的 MIPS 二進制文件,其擴展名為*.coff。(COFF 代表通用對象文件格式,是一種行業標準的二進制格式,Nachos 內核可以理解這種二進制格式)。
  • 你可以通過運行以下程序來運行其他測試程序:

    nachos -x PROGNAME.coff

    其中PROGNAME.coff是test目錄中 MIPS 程序二進制的名稱。請隨意編寫你自己的 C 測試程序 – 事實上,你需要這樣做來測試你自己的代碼!

    與實驗一相同,你應該在項目中提交并記錄你的測試用例(包括你的代碼的 Java 和 C 部分)。在這個項目中,大多數測試用例將以 C 語言程序的形式實現,測試你的系統調用,但也有可能在 Java 中進行一些“內部”測試。

    題目 1

    實現文件系統調用(記錄在syscall.h中的creat、open、read、write、close和unlink)。你將在UserProcess.java中看到halt的代碼;你最好也把你的新系統調用放在這里。注意,你不是在實現一個文件系統;相反,你只是讓用戶進程有能力訪問我們已經為你實現的文件系統。

    • 我們已經為你提供了從用戶程序中調用系統調用所需的匯編代碼(見start.s,SYSCALLSTUB宏為每個系統調用生成了匯編代碼);
    • 你需要保護 Nachos 內核不受用戶程序錯誤的影響;用戶程序不應該做任何事情來破壞操作系統(除了明確調用halt()系統調用)。換句話說,你必須確保用戶程序不會向內核傳遞假的參數,從而導致內核破壞其內部狀態或其他進程的狀態。另外,你必須采取措施確保如果一個用戶進程做了任何非法的事情,比如試圖訪問未映射的內存或跳轉到一個錯誤的地址,該進程將被干凈地殺死并釋放其資源;
    • 你應該使halt()系統調用只能由“根”進程調用,也就是系統中的第一個進程。如果其他進程試圖調用 halt(),系統調用應該被忽略并立即返回;
    • 由于作為參數傳遞給系統調用的內存地址是虛擬地址,你需要使用UserProcess.readVirtualMemory和UserProcess.writeVirtualMemory來在用戶進程和內核之間傳輸內存;
    • 用戶進程在其虛擬地址空間中將文件名和其他字符串參數存儲為以null結尾的字符串。作為參數傳遞給系統調用的字符串的最大長度是 256 字節;
    • 當一個系統調用希望向用戶表明一個錯誤情況時,它應該返回 -1(而不是在內核中拋出一個異常)。否則,系統調用應該返回適當的值,如test/syscall.h中記載的;
    • 當任何進程被啟動時,它的文件描述符 0 和 1 必須是指標準輸入和標準輸出。使用UserKernel.console.openForReading()和UserKernel.console.openForWriting()來使之更容易。用戶進程被允許關閉這些描述符,就像open()所返回的描述符一樣;
    • 已經為你提供了一個 UNIX 文件系統的存根文件系統接口;該接口由machine/FileSystem.java類給出。你可以通過靜態字段ThreadedKernel.fileSystem訪問這個存根文件系統。(注意,由于UserKernel擴展了ThreadedKernel,你仍然可以訪問這個字段)。這個文件系統能夠訪問你的 Nachos 發行版中的test目錄,當你想支持exec系統調用(見下文)時,這將非常有用。你不需要實現任何文件系統的功能。您應仔細研究FileSystem和StubFileSystem的規范,以確定您應在系統調用中提供哪些功能,以及哪些功能由文件系統處理;
    • 不要實現任何類型的文件鎖定;這是文件系統的責任。如果ThreadedKernel.fileSystem.open()返回一個非空的OpenFile,那么用戶進程就被允許訪問給定的文件;否則,你應該發出錯誤信號。同樣,你也不需要擔心如果多個進程同時試圖訪問同一個文件會發生什么細節;存根文件系統會為你處理這些細節;
    • 你的實現應該支持每個進程至少有 16 個并發打開的文件。一個進程所打開的每個文件都應該有一個唯一的文件描述符與之相關聯(詳見syscall.h)。文件描述符應該是一個非負的整數,它只是用來索引該進程當前打開的文件的表格。注意,如果與之相關的文件被關閉,一個給定的文件描述符可以被重復使用,不同的進程可以使用同一個文件描述符(即整數)來指代不同的文件。

    題目 2

    實現對多道程序的支持。我們給你的代碼只限于一次運行一個用戶進程;你的工作是使它對多個用戶進程有效。

    • 想出一個分配機器物理內存的方法,使不同的進程在內存使用上不發生重疊。請注意,用戶程序不使用malloc()或free(),這意味著用戶程序實際上沒有動態內存分配需求(因此,沒有堆)。這意味著,當一個進程被創建時,你就知道它的完整內存需求。你可以為進程的堆棧分配一個固定數量的頁面;8 個頁面應該足夠了。
      我們建議維護一個空閑物理頁的全局鏈接列表(也許是作為UserKernel類的一部分)。在訪問這個列表時,請確保在必要時使用同步化。你的解決方案必須盡可能為新進程分配頁面,從而有效地利用內存。這意味著只在一個連續的塊中分配頁是不能接受的;你的解決方案必須能夠利用空閑內存池中的“空隙”。
      還要確保一個進程的所有內存在退出時被釋放(無論它是通過系統調用exit()正常退出,還是由于非法操作而異常退出)。
    • 修改UserProcess.readVirtualMemory和UserProcess.writeVirtualMemory,它們在內核和用戶的虛擬地址空間之間復制數據,以便在多個用戶進程中工作。
      MIPS 機器的物理內存是通過Machine.processor().getMemory()方法訪問的;物理頁的總數是Machine.processor().getNumPhysPages()。你應該為每個用戶進程維護pageTable,它將用戶的虛擬地址映射到物理地址。TranslationEntry類表示一個單一的虛擬到物理頁的轉換。
      如果頁面來自被標記為只讀的 COFF 部分,TranslationEntry.readOnly字段應被設置為true。你可以使用CoffSection.isReadOnly()方法確定這一點。
      請注意,這些方法在失敗時不應該拋出異常;相反,它們必須總是返回傳輸的字節數(即使這個數字是零)。
    • 修改UserProcess.loadSections(),使其使用你上面決定的分配策略,分配它所需要的頁數(也就是基于用戶程序的大小)。這個方法還應該為進程設置pageTable結構,以便進程被加載到正確的物理內存頁中。如果新的用戶進程不能裝入物理內存,exec()應該返回一個錯誤。
      請注意,用戶線程(見UThread類)已經在上下文切換時保存和恢復用戶機器狀態以及進程狀態。所以,你不需要對這些細節負責。

    題目 3

    實現系統調用(記錄在syscall.h中exec、join和exit)。

    • 同樣,所有在寄存器中傳遞給exec和join的地址都是虛擬地址。你應該使用readVirtualMemory和readVirtualMemoryString來在內核和用戶進程之間傳輸內存;
    • 同樣,你必須保護這些系統調用;
    • 注意,子進程的狀態對這個進程來說是完全私有的。這意味著父進程和子進程不直接共享內存或文件描述符。注意,兩個進程當然可以打開同一個文件;例如,所有進程都應該有文件描述符 0 和 1 映射到系統控制臺,如上所述;
    • 與KThread.join()不同的是,只有一個進程的父進程可以加入(指 join)到它。例如,如果 A 執行 B,B 執行 C,A 不允許加入 C,但 B 允許加入 C;
    • join需要一個進程 ID 作為參數,用于唯一地識別父進程希望加入的子進程。進程 ID 應該是一個全局唯一的正整數,在每個進程被創建時分配給它(盡管在這個項目中,進程 ID 的唯一用途是join,但對于以后的項目階段,進程 ID 在系統中的所有運行進程中是唯一的,這一點很重要)。實現這一目標的最簡單方法是維護一個靜態計數器,該計數器指示要分配的下一個進程 ID。由于進程 ID 是一個int,如果系統中有許多進程,那么這個值就有可能溢出。在這個項目中,你不需要處理這種情況;也就是說,假設進程 ID 計數器不會溢出;
    • 當一個進程調用exit()時,其線程應立即終止,并且該進程應清理與其相關的任何狀態(即釋放內存,關閉打開的文件等)。如果一個進程非正常退出,也要進行同樣的清理工作;
    • 在父進程調用join系統調用的情況下,退出的進程的退出狀態應該被轉移到父進程中。非正常退出的進程的退出狀態由你決定。就join而言,如果子進程以任何狀態調用exit系統調用,它就會正常退出;如果內核將其殺死(例如由于未處理的異常),它就會非正常退出;
    • 最后一個調用exit()的進程應該通過調用Kernel.kernel.terminate()使機器停止運行。(注意,只有根進程可以調用halt()系統調用,但最后一個退出的進程應該直接調用Kernel.kernel.terminate()。

    題目 4

    實現一個彩票調度器(放在threads/LotteryScheduler.java中)。注意,這個類擴展了PriorityScheduler,你應該可以重用該類的大部分功能;彩票調度不應該是大量的額外代碼。唯一的主要區別是用于從隊列中挑選線程的機制:進行抽獎,而不是僅僅挑選優先級最高的線程。你的彩票調度應該實現優先級捐贈(注意,由于這是一個彩票調度,優先級倒置實際上不能導致饑餓!然而,你的調度器無論如何都必須做優先級捐贈)。

    • 在彩票調度中,等待的線程不是捐贈優先權,而是將票據轉給他們所等待的線程。與標準的優先級調度器不同,一個等待的線程總是把它的票數加到隊列所有者的票數上;也就是說,所有者的票數是它自己的票數和所有等待者的票數之和,而不是最大值。請確保正確實現這一點;
    • 即使系統中有數十億張票據,你的解決方案也應該是有效的(也就是說,不要保留一個包含每張票據條目的數組);
    • 當調用LotteryScheduler.encreatePriority()時,一個進程所持有的彩票數量應該增加 1。同樣,對于decreasePriority()來說,這個數字應該減去 1;
    • 系統中的(實際)門票總數保證不超過Integer.MAX_VALUE。最大的個人優先級現在也是Integer.MAX_VALUE,而不是 7(PriorityScheduler.priorityMaximum)。如果你愿意,你也可以假設最小優先級增加到 1(從 0)。

    具體實現

    關于交叉編譯

    編譯的意思是指,把程序員能看懂的代碼翻譯成機器能看懂的代碼。

    在 Windows 系統中,可執行文件是以.exe結尾的文件;在 Linux 中,我們可以運行具有執行權限的文件。同理,對于 Nachos 系統來說,它把.coff文件當做它的可執行文件。

    正常來說,我們可以在 Windows 系統上把.cpp文件編譯成.exe文件,所以理論上講,我們也可以在 Nachos 系統把這個.cpp文件編譯成.coff文件。但問題是,我們沒有 Nachos 上的編譯器,甚至我們的 Nachos 還沒實現呢!

    交叉編譯器的作用就是,把一個系統上的源文件編譯成另一個系統上的可執行文件。交叉編譯多見于嵌入式開發,因為某些小系統難以支持編譯。

    所以,我們的實驗需要的并不是在自己電腦上安裝這個交叉編譯器,而是交叉編譯后所得到的這個.coff結尾的文件。如果我們想在 Nachos 上運行我們自己寫的程序,但是自己并沒有配置好交叉編譯器,那用別人的就好了。

    在nachos/test目錄下面已經有一些可執行文件了,比如cat.coff、echo.coff、sh.coff、halt.coff等等,我們在進行實驗的時候,可能會用到它們。

    基本配置

    首先,與nachos目錄同層的nachos.conf文件中,還保存著前一個實驗的配置,我們可以進入到nachos/proj2/nachos.conf中,把里面的內容復制過來。

    Machine.stubFileSystem = true Machine.processor = true Machine.console = true Machine.disk = false Machine.bank = false Machine.networkLink = false Processor.usingTLB = false Processor.numPhysPages = 64 ElevatorBank.allowElevatorGUI = false NachosSecurityManager.fullySecure = false ThreadedKernel.scheduler = nachos.threads.RoundRobinScheduler #nachos.threads.LotteryScheduler Kernel.shellProgram = halt.coff #sh.coff Kernel.processClassName = nachos.userprog.UserProcess Kernel.kernel = nachos.userprog.UserKernel

    注意到,上面的配置項中有兩行帶了注釋(#后面的)。ThreadedKernel.scheduler表示的是采用的線程調度方式,當前的值為循環調度,注釋后面是彩票調度,這個在題目 4 中會進行修改。Kernel.shellProgram表示的啟動機器時首先要運行的可執行文件,halt.coff的唯一作用就是關閉這個操作系統,sh.coff運行后,我們就像是在操作 Linux 系統一樣,輸入一行指令,系統就是執行相應的功能。

    在nachos/machine/Machine.java的main方法中,我們可以看到系統似乎在尋找test目錄:

    public static void main(final String[] args) {...String testDirectoryName = Config.getString("FileSystem.testDirectory");if (testDirectoryName != null) {testDirectory = new File(testDirectoryName);} else {testDirectory = new File(baseDirectory.getParentFile(), "test");}... }

    但是,之前的nachos.conf中壓根就沒有FileSystem.testDirectory這一項,而且baseDirectory指的是nachos-java,它的父目錄下面也沒有test文件夾。要使系統能找到test,要么我們修改配置文件,要么我們移動test文件夾,要么我們就修改上面的代碼(雖然這個源文件好像不讓學生修改),我選擇第三者:

    public static void main(final String[] args) {...testDirectory = new File(nachosDirectory, "test");... }

    還有之前我們做實驗一時在nachos/threads/KThread.java的selfTest()中添加的代碼,為了避免多余的內容對我們產生影響,我們可以把這個方法所調用的測試程序進行注釋。

    在nachos/userprog/UserKernel.java中也有一個selfTest方法,內容如下:

    public void selfTest() {super.selfTest();System.out.println("Testing the console device. Typed characters");System.out.println("will be echoed until q is typed.");char c;do {c = (char) console.readByte(true);console.writeByte(c);}while (c != 'q');System.out.println(""); }

    selfTest是控制臺的測試,檢查是否能正常輸入輸出。它會打印我們向控制臺輸入的內容,直到我們輸入q表示結束。而對于我們的實驗來說,這個方法并沒有什么作用,除了第一行調用父級的selfTest方法之外,其余的也都注釋掉。

    題目 1

    該題目讓我們實現一個文件系統。

    進入nachos/userprog/UserProcess.java,聲明作為參數傳遞給系統調用的字符串的最大長度:

    private static int maxFilenameLength = 256;

    聲明存放打開文件的數組,并在構造方法中進行初始化,同時讓文件描述符 0 和 1 引用標準輸入和標準輸出。

    private OpenFile[] openFiles;public UserProcess() {int numPhysPages = Machine.processor().getNumPhysPages();pageTable = new TranslationEntry[numPhysPages];for (int i = 0; i < numPhysPages; i++)pageTable[i] = new TranslationEntry(i, i, true, false, false, false);this.openFiles = new OpenFile[16];this.openFiles[0] = UserKernel.console.openForReading();this.openFiles[1] = UserKernel.console.openForWriting(); }

    文件調用的處理方法的實現思路可以查看nachos/test/syscall.h,具體實現如下:

    /* 文件管理系統調用: creat, open, read, write, close, unlink** 文件描述符是一個小的非負整數,它引用磁盤上的文件或流(例如控制臺輸入、控制臺輸出和網絡連接)* 可以將文件描述符傳遞給 read() 和 write(),以讀取/寫入相應的文件/流* 還可以將文件描述符傳遞給 close(),以釋放文件描述符和任何相關資源*//*** 嘗試打開給定名稱的磁盤文件,如果該文件不存在,則創建該文件,并返回可用于訪問該文件的文件描述符* <p>* 注意 creat() 只能用于在磁盤上創建文件,永遠不會返回引用流的文件描述符** @param fileAddress 目標文件地址* @return 新的文件描述符,如果發生錯誤,則為 -1*/ private int handleCreate(int fileAddress) {// 從該進程的虛擬內存中讀取字符串String fileName = readVirtualMemoryString(fileAddress, maxFilenameLength);// 非空檢驗if (fileName == null || fileName.length() == 0) {return -1;}// 與該文件相關聯的文件描述符int fileDescriptor = -1;// 遍歷 openFilesfor (int i = 0; i < this.openFiles.length; i++) {// 找出一個暫時未與進程打開文件相關聯的文件描述符if (this.openFiles[i] == null) {// 此時該文件描述符未進行關聯if (fileDescriptor == -1) {// 設置關聯fileDescriptor = i;}continue;}// 檢查該文件是否已經打開if (this.openFiles[i].getName().equals(fileName)) {return i;}}// 暫時沒有空閑的文件描述符if (fileDescriptor == -1) {return -1;}// 打開該文件,如果文件不存在,則創建一個文件OpenFile openFile = ThreadedKernel.fileSystem.open(fileName, true);// 使空閑的文件描述符與該文件相關聯this.openFiles[fileDescriptor] = openFile;return fileDescriptor; }/*** 嘗試打開指定名稱的文件并返回文件描述符* <p>* 注意 open() 只能用于打開磁盤上的文件,永遠不會返回引用流的文件描述符** @param fileAddress 目標文件地址* @return 新的文件描述符,如果發生錯誤,則為 -1*/ private int handleOpen(int fileAddress) {// 從該進程的虛擬內存中讀取以 null 結尾的字符串String fileName = readVirtualMemoryString(fileAddress, maxFilenameLength);// 非空檢驗if (fileName == null || fileName.length() == 0) {return -1;}// 與該文件相關聯的文件描述符int fileDescriptor = -1;// 遍歷 openFilesfor (int i = 0; i < this.openFiles.length; i++) {// 找出一個暫時未與進程打開文件相關聯的文件描述符if (this.openFiles[i] == null) {// 此時該文件描述符未進行關聯if (fileDescriptor == -1) {// 設置關聯fileDescriptor = i;}continue;}// 檢查該文件是否已經打開if (this.openFiles[i].getName().equals(fileName)) {return i;}}// 暫時沒有空閑的文件描述符if (fileDescriptor == -1) {return -1;}// 打開該文件,如果文件不存在,則返回 nullOpenFile openFile = ThreadedKernel.fileSystem.open(fileName, false);if (openFile == null) {return -1;}// 使空閑的文件描述符與該文件相關聯this.openFiles[fileDescriptor] = openFile;return fileDescriptor; }/*** 嘗試從 fileDescriptor 指向的文件或流中讀取數個字節到緩沖區* <p>* 成功時,返回讀取的字節數* 如果文件描述符引用磁盤上的文件,則文件地址將按此數字前進* <p>* 如果此數字小于請求的字節數,則不一定是錯誤* 如果文件描述符引用磁盤上的文件,則表示已到達文件末尾* 如果文件描述符引用一個流,這表示現在實際可用的字節比請求的字節少,但將來可能會有更多的字節可用* 注意 read() 從不等待流有更多數據,它總是盡可能立即返回* <p>* 出現錯誤時,返回 -1,新文件地址為未定義,發生這種情況的原因可能是:* fileDescriptor 無效、緩沖區的一部分為只讀或無效、網絡流已被遠程主機終止且沒有更多可用數據** @param fileDescriptor 文件描述符* @param vaddr 虛擬內存地址* @param length 讀取內容長度* @return 成功讀取的字節數,如果失敗,則為 -1*/ private int handleRead(int fileDescriptor, int vaddr, int length) {// 檢查文件描述符是否有效if (openFiles[fileDescriptor] == null) {return -1;}// 獲取該文件OpenFile openFile = openFiles[fileDescriptor];// 用于存儲讀取內容的緩沖區byte[] buf = new byte[length];// 將內容讀取到 buf,并獲得成功讀取的字節數int successRead = openFile.read(buf, 0, length);// 將數據從緩沖區寫入到該進程的虛擬內存,并獲得成功寫入的字節數int successWrite = writeVirtualMemory(vaddr, buf, 0, successRead);// 檢查傳輸的完整性if (successRead != successWrite) {return -1;}return successRead; }/*** 嘗試將緩沖區中的數個字節寫入到 fileDescriptor 所引用的文件或流* write() 可以在字節實際流動到文件或流之前返回* 但是,如果內核隊列暫時已滿,則對流的寫入可能會阻塞* <p>* 成功時,將返回寫入的字節數( 表示未寫入任何內容),文件位置將按此數字前進* 如果此數字小于請求的字節數,則為錯誤* 對于磁盤文件,這表示磁盤已滿* 對于流,這表示在傳輸所有數據之前,遠程主機終止了流* <p>* 出現錯誤時,返回 -1,新文件地址為未定義,發生這種情況的原因可能是:* fileDescriptor 無效、緩沖區的一部分為只讀或無效、網絡流已被遠程主機終止** @param fileDescriptor 文件描述符* @param vaddr 虛擬內存地址* @param length 寫入內容長度* @return 成功讀取的字節數,如果失敗,則為 -1*/ private int handleWrite(int fileDescriptor, int vaddr, int length) {// 檢查文件描述符是否有效if (openFiles[fileDescriptor] == null) {return -1;}// 獲取該文件OpenFile openFile = openFiles[fileDescriptor];// 用于存儲讀取內容的緩沖區byte[] buf = new byte[length];// 將數據從該進程的虛擬內存讀取到緩沖區,并獲得成功讀取的字節數int successRead = readVirtualMemory(vaddr, buf);// 將內容寫入到該文件,并獲得成功寫入的字節數int successWrite = openFile.write(buf, 0, successRead);// 檢查傳輸的完整性if (successRead != successWrite) {return -1;}return successRead; }/*** 關閉文件描述符,使其不再引用任何文件或流,并且可以重用** 如果文件描述符引用一個文件,則 write() 寫入的所有數據將在 close() 返回之前轉移到磁盤* 如果文件描述符引用流,則 write() 寫入的所有數據最終都將轉移(除非流被遠程終止),但不一定在 close() 返回之前** 與文件描述符關聯的資源將被釋放* 如果描述符是使用 unlink 刪除的磁盤文件的最后一個引用,則該文件將被刪除(此詳細信息由文件系統實現處理)** @param fileDescriptor 文件描述符* @return 成功時為 0,錯誤發生時為 -1*/ private int handleClose(int fileDescriptor) {// 檢查文件描述符是否有效if (openFiles[fileDescriptor] == null) {return -1;}// 獲取該文件OpenFile openFile = openFiles[fileDescriptor];// 取消文件描述符與該文件的關聯openFiles[fileDescriptor] = null;// 關閉此文件并釋放所有相關的系統資源openFile.close();return 0; }/*** 從文件系統中刪除文件* 如果沒有進程打開該文件,則會立即刪除該文件,并使其使用的空間可供重用** 如果任何進程仍然打開該文件,則該文件將一直存在,直到引用它的最后一個文件描述符關閉為止* 但是,在刪除該文件之前,creat() 和 open() 將無法返回新的文件描述符** @param fileAddress 文件地址* @return 成功時為 0,失敗時為 -1*/ private int handleUnlink(int fileAddress) {// 從該進程的虛擬內存中讀取以 null 結尾的字符串String fileName = readVirtualMemoryString(fileAddress, maxFilenameLength);// 非空檢驗if (fileName == null || fileName.length() == 0) {return -1;}for (int i = 0; i < openFiles.length; i++) {if (openFiles[i] != null && openFiles[i].getName().equals(fileName)) {openFiles[i] = null;break;}}// 移除文件boolean removeSuccess = ThreadedKernel.fileSystem.remove(fileName);// 檢測移除是否成功if (!removeSuccess) {return -1;}return 0; }

    將上述處理函數添加到handleSyscall中:

    public int handleSyscall(int syscall, int a0, int a1, int a2, int a3) {switch (syscall) {case syscallHalt:return handleHalt();case syscallCreate:return handleCreate(a0);case syscallOpen:return handleOpen(a0);case syscallRead:return handleRead(a0, a1, a2);case syscallWrite:return handleWrite(a0, a1, a2);case syscallClose:return handleClose(a0);case syscallUnlink:return handleUnlink(a0);default:Lib.debug(dbgProcess, "Unknown syscall " + syscall);Lib.assertNotReached("Unknown system call!");}return 0; }

    題目 2

    在UserProcess的構造方法,我們看到進程在獲取到物理頁數之后,直接創建了一個以這個頁數為長度的頁表。換句話說,這個進程把所有的頁都給占用了,這是在進行多道程序設計時無法接受的情況,我們需要換一種分配機制,所以把下面方法中所展示的代碼刪掉。

    public UserProcess() {int numPhysPages = Machine.processor().getNumPhysPages();pageTable = new TranslationEntry[numPhysPages];for (int i = 0; i < numPhysPages; i++)pageTable[i] = new TranslationEntry(i, i, true, false, false, false);... }

    我們希望每個進程只獲取它所需要的頁,同時,這些頁的物理地址最好允許不連續。因為要保證頁連續的話,系統長時間運轉時,隨著進程的創建與銷毀,空閑的頁會變得破碎,不利于利用。

    在nachos/userprog/UserKernel.java中,我們創建一個鏈表,來存放未被進程占用的頁號:

    private static LinkedList<Integer> AllFreePageNums;public UserKernel() {super();// 初始化內存列表AllFreePageNums = new LinkedList<>();// 獲取物理頁數int numPhysPages = Machine.processor().getNumPhysPages();// 為空閑頁編號for (int i = 0; i < numPhysPages; i++) {AllFreePageNums.add(i);} }

    我們還需要給用戶進程提供獲取和歸還這些頁的方法:

    public static LinkedList<Integer> getFreePageNums(int numPages) {// 聲明并初始化一個空閑頁號鏈表LinkedList<Integer> freePageNums = new LinkedList<>();// 如果空閑頁足夠if (AllFreePageNums.size() >= numPages) {// 從空閑頁中取出指定數量的頁號,并添加到 freePages 中for (int i = 0; i < numPages; i++) {freePageNums.add(AllFreePageNums.removeFirst());}}return freePageNums; }// 歸還空閑頁 public static void releaseOwnPageNums(LinkedList<Integer> ownPageNums){// 如果進程沒有占有頁,直接返回if (ownPageNums == null || ownPageNums.isEmpty()) {return;}// 將進程中頁號轉換成空閑頁號for (int i = 0; i < ownPageNums.size(); i ++) {AllFreePageNums.add(ownPageNums.removeFirst());} }

    回到nachos/userprog/UserProcess.java,用戶進程也需要一個鏈表來存儲這些頁號,注意,這些頁號是物理頁號。在頁表元素TranslationEntry初始化的過程中,第一個參數是連續的虛擬頁號,第二個參數則是物理頁號,就是在這里實現的實際地址與邏輯地址之間的映射。從用戶進程的角度看,它邏輯地址是從 0 開始的,是連續的,就仿佛它在使用整個內存。

    private LinkedList<Integer> ownPageNums;protected boolean loadSections() {// 保證程序的頁數不超過物理頁范圍if (numPages > Machine.processor().getNumPhysPages()) {coff.close();Lib.debug(dbgProcess, "\tinsufficient physical memory");return false;}// 獲取空閑頁號ownPageNums = UserKernel.getFreePageNums(numPages);// 檢查空閑頁是否充足if (ownPageNums.isEmpty()) {return false;}// 頁表數組初始化pageTable = new TranslationEntry[numPages];// 將數組中的頁表初始化for (int i = 0; i < numPages; i++) {pageTable[i] = new TranslationEntry(i, ownPageNums.get(i), true, false, false, false);}// 加載用戶程序到內存for (int s = 0; s < coff.getNumSections(); s++) {CoffSection section = coff.getSection(s);Lib.debug(dbgProcess, "\tinitializing " + section.getName()+ " section (" + section.getLength() + " pages)");for (int i = 0; i < section.getLength(); i++) {int vpn = section.getFirstVPN() + i;// 裝入頁section.loadPage(i, pageTable[vpn].ppn);}}return true; }protected void unloadSections() {// 關閉當前進程正在執行的文件coff.close();// 將該進程擁有的頁轉換為空閑頁UserKernel.releaseOwnPageNums(ownPageNums); }

    由于現在的虛擬地址不等于物理地址了,那對于虛擬地址的讀寫方法也得跟著改變:

    public int readVirtualMemory(int vaddr, byte[] data, int offset, int length) {// 偏移量和長度非負,且不能越界Lib.assertTrue(offset >= 0 && length >= 0 && offset + length <= data.length);// 獲取物理內存byte[] memory = Machine.processor().getMemory();// 傳輸的字節數過多,會導致虛擬內存越界if (length > pageSize * numPages - vaddr) {// 截取不超過越界的部分length = pageSize * numPages - vaddr;}// 不斷讀取虛擬內存,直到讀完指定長度的數據int successRead = 0;while (successRead < length) {// 計算頁號int pageNum = Processor.pageFromAddress(vaddr + successRead);// 檢查是否越界if (pageNum < 0 || pageNum >= pageTable.length) {return successRead;}// 計算頁偏移量int pageOffset = Processor.offsetFromAddress(vaddr + successRead);// 計算當頁剩余容量int pageRemain = pageSize - pageOffset;// 比較未讀取的內容與當頁未使用的空間,取較小值用于數據轉移int amount = Math.min(length - successRead, pageRemain);// 計算真實地址int realAddress = pageTable[pageNum].ppn * pageSize + pageOffset;// 將數據從內存復制到指定數組System.arraycopy(memory, realAddress, data, offset + successRead, amount);// 成功讀取的數據量successRead = successRead + amount;}return successRead; }public int writeVirtualMemory(int vaddr, byte[] data, int offset, int length) {// 偏移量和長度非負,且不能越界Lib.assertTrue(offset >= 0 && length >= 0 && offset + length <= data.length);// 獲取物理內存byte[] memory = Machine.processor().getMemory();// 傳輸的字節數過多,會導致虛擬內存越界if (length > pageSize * numPages - vaddr) {// 截取不超過越界的部分length = pageSize * numPages - vaddr;}// 不斷寫入虛擬內存,直到寫完指定長度的數據int successWrite = 0;while (successWrite < length) {// 計算頁號int pageNum = Processor.pageFromAddress(vaddr + successWrite);// 檢查是否越界if (pageNum < 0 || pageNum >= pageTable.length) {return successWrite;}// 計算頁偏移量int pageOffset = Processor.offsetFromAddress(vaddr + successWrite);// 計算當頁剩余容量int pageRemain = pageSize - pageOffset;// 比較未讀取的內容與當頁未使用的空間,取較小值用于數據轉移int amount = Math.min(length - successWrite, pageRemain);// 計算真實地址int realAddress = pageTable[pageNum].ppn * pageSize + pageOffset;// 如果當前頁為只讀狀態,終止數據轉移if (pageTable[pageNum].readOnly) {return successWrite;}// 將數據從內存復制到指定數組System.arraycopy(data, offset + successWrite, memory, realAddress, amount);// 成功讀取的數據量successWrite = successWrite + amount;}return successWrite; }

    題目 3

    這個題目是要我們實現進程管理系統調用,與題目 1 一樣,我們也是要根據nachos/test/syscall.h的要求,實現我們的方法。

    // 進程運行的狀態 private int status;// 進程 id 計數器(需要正整數) private static int processIdCounter = 1;// 當前進程的 id private int processId;// 進程 id 與進程的映射表 private static Map<Integer, UserProcess> processMap = new HashMap<>();// 父子進程 id private int parentProcessId; private LinkedList<Integer> childrenProcessId;// join 中需要用到的鎖和條件變量 private Lock joinLock; private Condition joinCondition;// 是否正常退出 private boolean normalExit = false;public UserProcess() {...// 設置當前進程 idthis.processId = processIdCounter;// 進程 id 計數器自增processIdCounter++;// 將該進程添加到進程映射表中processMap.put(this.processId, this);// -1 表示無父進程parentProcessId = -1;// 子進程 id 鏈表初始化childrenProcessId = new LinkedList<>();// 初始化 join 用到的鎖和條件變量joinLock = new Lock();joinCondition = new Condition(joinLock); }/* 進程管理系統調用: exit, exec, join *//*** 立即終止當前進程* 屬于該進程的任何打開的文件描述符都將關閉* 進程的任何子進程都不再具有父進程** status 作為此進程的退出狀態返回給父進程,并且可以使用 join 系統調用收集* 正常退出的進程應(但不要求)將狀態設置為 0** exit() 永不返回** @param status 退出狀態* @return 不返回*/ private int handleExit(int status) {// 設置進程運行狀態this.status = status;// 關閉可執行文件coff.close();// 關閉所有打開的文件for (int i = 0; i < openFiles.length; i++) {// 如果打開文件非空if (openFiles[i] != null) {// 關閉該文件openFiles[i].close();}}// 釋放內存unloadSections();// 如果這是最后一個進程if (processMap.size() == 1) {// 終止內核Kernel.kernel.terminate();return 0;}// 如果該進程存在父進程if (this.parentProcessId != -1) {// 獲取父進程UserProcess parentProcess = processMap.get(this.parentProcessId);// 將該進程從父進程的子進程列表中移除(注意:不要直接傳數字,否則會被視為索引)parentProcess.childrenProcessId.remove(new Integer(this.processId));}// 遍歷該進程的子進程for (int childProcessId : childrenProcessId) {// 將子進程的父進程設置為無processMap.get(childProcessId).parentProcessId = -1;}// 將該進程從映射表中移除processMap.remove(this.processId);// 設置正常退出狀態this.normalExit = true;// 獲得鎖joinLock.acquire();// 喚醒在這個條件變量上等待的線程(可能是父進程的線程)joinCondition.wake();// 釋放鎖joinLock.release();// 終止線程KThread.finish();return 0; }/*** 在新的子進程中使用指定的參數執行存儲在指定文件中的程序* 子進程有一個新的唯一進程 ID,以標準輸入作為文件描述符 0 打開,標準輸出作為文件描述符 1 打開開始** file 是以 null 結尾的字符串,指定包含可執行文件的文件名* 請注意,此字符串必須包含 .coff 擴展名** argc 指定要傳遞給子進程的參數的數量* 此數字必須為非負數** argv 是指向以 null 結尾的字符串的指針數組,這些字符串表示要傳遞給子進程的參數* argv[0] 指向第一個參數,argv[argc-1] 指向最后一個參數** exec() 返回子進程的進程 ID,該 ID 可以傳遞給 join()* 出現錯誤時,返回 -1** @param fileAddress 文件地址* @param argc 要傳遞給子進程的參數的數量* @param argvAddress 要傳遞給子進程的參數的地址* @return 子進程的 id,出現錯誤時為 -1*/ private int handleExec(int fileAddress, int argc, int argvAddress) {// 從該進程的虛擬內存中讀取以 null 結尾的字符串String fileName = readVirtualMemoryString(fileAddress, maxFilenameLength);// 非空檢驗if (fileName == null || fileName.length() == 0) {return -1;}// 讀取子進程的參數String[] args = new String[argc];for (int i = 0; i < argc; i++) {byte[] argsAddress = new byte[4];// 從虛擬內存中讀取參數地址if (readVirtualMemory(argvAddress + i * 4, argsAddress) > 0) {// 根據讀取到的地址找相應的字符串args[i] = readVirtualMemoryString(Lib.bytesToInt(argsAddress, 0), 256);}}// 創建子進程UserProcess newUserProcess = UserProcess.newUserProcess();// 執行子進程boolean executeSuccess = newUserProcess.execute(fileName, args);// 檢測是否執行成功if (!executeSuccess) {return -1;}// 將新進程的父進程設置為該進程newUserProcess.parentProcessId = this.processId;// 將新進程添加到該進程的子進程中this.childrenProcessId.add(new Integer(newUserProcess.processId));// 返回新進程 idreturn newUserProcess.processId; }/*** 暫停當前進程的執行,直到 processID 參數指定的子進程退出* 如果在調用時孩子已經退出,則立即返回* 當該進程恢復時,它會斷開子進程的連接,因此 join() 不能再次用于該進程** processID 是子進程的進程 ID,由 exec() 返回** statusAddress 指向一個整數,子進程的退出狀態將存儲在該整數中* 這是子進程傳遞給 exit() 的值* 如果子進程由于未處理的異常而退出,則存儲的值為未定義** 如果子進程正常退出,則返回 1* 如果子進程由于未處理的異常而退出,則返回 0* 如果 processID 未引用當前進程的子進程,則返回 -1** @param processID 子進程 id* @param statusAddress 子進程退出狀態的地址* @return 1(正常退出),0(子進程異常退出),-1(processID 引用錯誤)*/ private int handleJoin(int processID, int statusAddress) {// 獲取子進程UserProcess childProcess = processMap.get(processID);// 只有一個進程的父進程才能 join 到它if (!(childProcess.parentProcessId == this.processId)) {return -1;}// 父進程持有子進程的鎖childProcess.joinLock.acquire();// 該進程在該鎖的條件變量上等待,直到子進程退出childProcess.joinCondition.sleep();// 把鎖釋放掉childProcess.joinLock.release();// 獲取子進程的運行狀態byte[] childstatus = Lib.bytesFromInt(childProcess.status);// 將子進程的狀態寫入內存中int successWrite = writeVirtualMemory(statusAddress, childstatus);// 判斷子進程是否正常結束if (childProcess.normalExit && successWrite == 4) {return 1;}return 0; }public int handleSyscall(int syscall, int a0, int a1, int a2, int a3) {switch (syscall) {case syscallHalt:return handleHalt();case syscallExit:return handleExit(a0);case syscallExec:return handleExec(a0, a1, a2);case syscallJoin:return handleJoin(a0, a1);....}return 0; }

    到現在,我們的系統就可以正常運行了,程序入口依然是nachos/machine/Machine.java中的main函數,直接運行。我們依次輸入echo hello、halt,運行結果如下:

    nachos 5.0j initializing...config interrupt timer processor console user-check grader nachos% echo hello echo hello 2 arguments arg 0: echo arg 1: hello[2] Done (0) nachos% halt halt Machine halting!Ticks: total 106513374, kernel 56075410, user 50437964 Disk I/O: reads 0, writes 0 Console I/O: reads 17, writes 92 Paging: page faults 0, TLB misses 0 Network I/O: received 0, sent 0Process finished with exit code 0

    如果控制臺打印了如下信息,不用管:

    Lacked permission: ("java.lang.RuntimePermission" "createClassLoader")

    題目 4

    先修改系統選用的調度程序,進入nachos.conf,修改ThreadedKernel.scheduler。

    ThreadedKernel.scheduler = nachos.threads.LotteryScheduler

    進入nachos/threads/PriorityScheduler.java,把PriorityQueue中的list的訪問權限由private改成protected,以便子類利用。

    protected LinkedList<ThreadState> list = new LinkedList<>();

    進入同目錄下的LotteryScheduler.java,因為LotteryScheduler繼承了PriorityScheduler,所以我們可以復用其中的大部分方法。

    聲明一個內部類LotteryThreadState,讓它繼承優先級調度中的ThreadState,并重寫部分方法:

    protected class LotteryThreadState extends ThreadState {public LotteryThreadState(KThread thread) {super(thread);}public int getEffectivePriority() {// 嘗試使用之前保存的數據if (KThread.getPriorityStatus()) {return effectivePriority;}// 重新計算有效優先級effectivePriority = priority;// 遍歷該線程的等待線程列表for (KThread waitThread : thread.getWaitThreadList()) {// 等待線程的有效優先級effectivePriority += getThreadState(waitThread).getEffectivePriority();}return effectivePriority;} }

    再聲明一個LotteryQueue,讓它繼承優先級調度中的PriorityQueue,并重寫部分方法:

    protected class LotteryQueue extends PriorityQueue {LotteryQueue(boolean transferPriority) {super(transferPriority);}protected ThreadState pickNextThread() {// 計算彩票總數int lotterySum = 0;for (ThreadState lotteryThreadState : list) {if (transferPriority) {lotterySum += lotteryThreadState.getEffectivePriority();} else {lotterySum += lotteryThreadState.getPriority();}}// 當前存在可運行的線程if (lotterySum != 0) {// 指定獲勝彩票int winLottery = Lib.random(lotterySum) + 1;// 當前彩票計數int currentLotteryNum = 0;// 遍歷所有線程,直到找到持有中獎彩票的線程for (ThreadState lotteryThreadState: list) {if (transferPriority) {currentLotteryNum += lotteryThreadState.getEffectivePriority();} else {currentLotteryNum += lotteryThreadState.getPriority();}// 找到獲獎彩票if (currentLotteryNum >= winLottery) {return lotteryThreadState;}}}return null;} }

    然后,把進程的選擇與進程狀態的綁定也修改一下:

    public ThreadQueue newThreadQueue(boolean transferPriority) {return new LotteryQueue(transferPriority); }protected ThreadState getThreadState(KThread thread) {if (thread.schedulingState == null)thread.schedulingState = new LotteryThreadState(thread);return (ThreadState) thread.schedulingState; }

    這樣,我們的彩票調度就完成了,測試用例可以沿用優先級調度題目的測試用例,也可以在此基礎上進行修改和補充。修改部分配置后,運行,控制臺打印如下:

    nachos 5.0j initializing...config interrupt timer user-check grader<--- 題目 5 開始測試 --->B 線程開始運行,初始優先級為 4 B 線程嘗試讓出 CPU B 線程重新使用 CPU B 線程結束運行 C 線程開始運行,初始優先級為 6 C 線程嘗試讓出 CPU C 線程重新使用 CPU C 線程等待 A 線程 A 線程開始運行,初始優先級為 2 A 線程嘗試讓出 CPU A 線程重新使用 CPU A 線程結束運行 C 線程重新使用 CPU C 線程結束運行<--- 題目 5 結束測試 --->Machine halting!Ticks: total 2070, kernel 2070, user 0 Disk I/O: reads 0, writes 0 Console I/O: reads 0, writes 0 Paging: page faults 0, TLB misses 0 Network I/O: received 0, sent 0Process finished with exit code 0

    源碼

    項目地址:https://gitee.com/GongY1Y/nachos-study

    總結

    以上是生活随笔為你收集整理的【操作系统】Nachos 多道程序设计的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。