Caffeinated 6.828:實驗 5:文件系統、Spawn 和 Shell
簡介
在本實驗中,你將要去實現 spawn
,它是一個載入和運行磁碟上可運行文件的庫調用。然後,你接著要去充實你的內核和庫,以使操作系統能夠在控制台上運行一個 shell。而這些特性需要一個文件系統,本實驗將引入一個可讀/寫的簡單文件系統。
預備知識
使用 Git 去獲取最新版的課程倉庫,然後創建一個命名為 lab5
的本地分支,去跟蹤遠程的 origin/lab5
分支:
athena% cd ~/6.828/lab
athena% add git
athena% git pull
Already up-to-date.
athena% git checkout -b lab5 origin/lab5
Branch lab5 set up to track remote branch refs/remotes/origin/lab5.
Switched to a new branch "lab5"
athena% git merge lab4
Merge made by recursive.
.....
athena%
在實驗中這一部分的主要新組件是文件系統環境,它位於新的 fs
目錄下。通過檢查這個目錄中的所有文件,我們來看一下新的文件都有什麼。另外,在 user
和 lib
目錄下還有一些文件系統相關的源文件。
fs/fs.c
維護文件系統在磁碟上結構的代碼fs/bc.c
構建在我們的用戶級頁故障處理功能之上的一個簡單的塊緩存fs/ide.c
極簡的基於 PIO(非中斷驅動的)IDE 驅動程序代碼fs/serv.c
使用文件系統 IPC 與客戶端環境交互的文件系統伺服器lib/fd.c
實現一個常見的類 UNIX 的文件描述符介面的代碼lib/file.c
磁碟上文件類型的驅動,實現為一個文件系統 IPC 客戶端lib/console.c
控制台輸入/輸出文件類型的驅動lib/spawn.c
spawn 庫調用的框架代碼
你應該再次去運行 pingpong
、primes
和 forktree
,測試實驗 4 完成後合併到新的實驗 5 中的代碼能否正確運行。你還需要在 kern/init.c
中注釋掉 ENV_CREATE(fs_fs)
行,因為 fs/fs.c
將嘗試去做一些 I/O,而 JOS 到目前為止還不具備該功能。同樣,在 lib/exit.c
中臨時注釋掉對 close_all()
的調用;這個函數將調用你在本實驗後面部分去實現的子程序,如果現在去調用,它將導致 JOS 內核崩潰。如果你的實驗 4 的代碼沒有任何 bug,將很完美地通過這個測試。在它們都能正常工作之前是不能繼續後續實驗的。在你開始做練習 1 時,不要忘記去取消這些行上的注釋。
如果它們不能正常工作,使用 git diff lab4
去重新評估所有的變更,確保你在實驗 4(及以前)所寫的代碼在本實驗中沒有丟失。確保實驗 4 仍然能正常工作。
實驗要求
和以前一樣,你需要做本實驗中所描述的所有常規練習和至少一個挑戰問題。另外,你需要寫出你在本實驗中問題的詳細答案,和你是如何解決挑戰問題的一個簡短(即:用一到兩個段落)的描述。如果你實現了多個挑戰問題,你只需要寫出其中一個即可,當然,我們歡迎你做的越多越好。在你動手實驗之前,將你的問題答案寫入到你的 lab5
根目錄下的一個名為 answers-lab5.txt
的文件中。
文件系統的雛形
你將要使用的文件系統比起大多數「真正的」文件系統(包括 xv6 UNIX 的文件系統)要簡單的多,但它也是很強大的,足夠去提供基本的特性:創建、讀取、寫入和刪除組織在層次目錄結構中的文件。
到目前為止,我們開發的是一個單用戶操作系統,它提供足夠的保護並能去捕獲 bug,但它還不能在多個不可信用戶之間提供保護。因此,我們的文件系統還不支持 UNIX 的所有者或許可權的概念。我們的文件系統目前也不支持硬鏈接、時間戳、或像大多數 UNIX 文件系統實現的那些特殊的設備文件。
磁碟上的文件系統結構
主流的 UNIX 文件系統將可用磁碟空間分為兩種主要的區域類型:節點區域和數據區域。UNIX 文件系統在文件系統中為每個文件分配一個節點;一個文件的節點保存了這個文件重要的元數據,比如它的 stat
屬性和指向數據塊的指針。數據區域被分為更大的(一般是 8 KB 或更大)數據塊,它在文件系統中存儲文件數據和目錄元數據。目錄條目包含文件名字和指向到節點的指針;如果文件系統中的多個目錄條目指向到那個文件的節點上,則稱該文件是硬鏈接的。由於我們的文件系統不支持硬鏈接,所以我們不需要這種間接的級別,並且因此可以更方便簡化:我們的文件系統將壓根就不使用節點,而是簡單地將文件的(或子目錄的)所有元數據保存在描述那個文件的(唯一的)目錄條目中。
文件和目錄邏輯上都是由一系列的數據塊組成,它或許是很稀疏地分散到磁碟上,就像一個環境的虛擬地址空間上的頁,能夠稀疏地分散在物理內存中一樣。文件系統環境隱藏了塊布局的細節,只提供文件中任意偏移位置讀寫位元組序列的介面。作為像文件創建和刪除操作的一部分,文件系統環境服務程序在目錄內部完成所有的修改。我們的文件系統允許用戶環境去直接讀取目錄元數據(即:使用 read
),這意味著用戶環境自己就能夠執行目錄掃描操作(即:實現 ls
程序),而不用另外依賴對文件系統的特定調用。用這種方法做目錄掃描的缺點是,(也是大多數現代 UNIX 操作系統變體摒棄它的原因)使得應用程序依賴目錄元數據的格式,如果不改變或至少要重編譯應用程序的前提下,去改變文件系統的內部布局將變得很困難。
扇區和塊
大多數磁碟都不能執行以位元組為粒度的讀寫操作,而是以扇區為單位執行讀寫。在 JOS 中,每個扇區是 512 位元組。文件系統實際上是以塊為單位來分配和使用磁碟存儲的。要注意這兩個術語之間的區別:扇區大小是硬碟硬體的屬性,而塊大小是使用這個磁碟的操作系統上的術語。一個文件系統的塊大小必須是底層磁碟的扇區大小的倍數。
UNIX xv6 文件系統使用 512 位元組大小的塊,與它底層磁碟的扇區大小一樣。而大多數現代文件系統使用更大尺寸的塊,因為現在存儲空間變得很廉價了,而使用更大的粒度在存儲管理上更高效。我們的文件系統將使用 4096 位元組的塊,以更方便地去匹配處理器上頁的大小。
超級塊
文件系統一般在磁碟上的「易於查找」的位置(比如磁碟開始或結束的位置)保留一些磁碟塊,用於保存描述整個文件系統屬性的元數據,比如塊大小、磁碟大小、用於查找根目錄的任何元數據、文件系統最後一次掛載的時間、文件系統最後一次錯誤檢查的時間等等。這些特定的塊被稱為超級塊。
我們的文件系統只有一個超級塊,它固定為磁碟的 1 號塊。它的布局定義在 inc/fs.h
文件里的 struct Super
中。而 0 號塊一般是保留的,用於去保存引導載入程序和分區表,因此文件系統一般不會去使用磁碟上比較靠前的塊。許多「真實的」文件系統都維護多個超級塊,並將它們複製到間隔較大的幾個區域中,這樣即便其中一個超級塊壞了或超級塊所在的那個區域產生了介質錯誤,其它的超級塊仍然能夠被找到並用於去訪問文件系統。
文件元數據
元數據的布局是描述在我們的文件系統中的一個文件中,這個文件就是 inc/fs.h
中的 struct File
。元數據包含文件的名字、大小、類型(普通文件還是目錄)、指向構成這個文件的塊的指針。正如前面所提到的,我們的文件系統中並沒有節點,因此元數據是保存在磁碟上的一個目錄條目中,而不是像大多數「真正的」文件系統那樣保存在節點中。為簡單起見,我們將使用 File
這一個結構去表示文件元數據,因為它要同時出現在磁碟上和內存中。
在 struct File
中的數組 f_direct
包含一個保存文件的前 10 個塊(NDIRECT
)的塊編號的空間,我們稱之為文件的直接塊。對於最大 10*4096 = 40KB
的小文件,這意味著這個文件的所有塊的塊編號將全部直接保存在結構 File
中,但是,對於超過 40 KB 大小的文件,我們需要一個地方去保存文件剩餘的塊編號。所以我們分配一個額外的磁碟塊,我們稱之為文件的間接塊,由它去保存最多 4096/4 = 1024 個額外的塊編號。因此,我們的文件系統最多允許有 1034 個塊,或者說不能超過 4MB 大小。為支持大文件,「真正的」文件系統一般都支持兩個或三個間接塊。
目錄與普通文件
我們的文件系統中的結構 File
既能夠表示一個普通文件,也能夠表示一個目錄;這兩種「文件」類型是由 File
結構中的 type
欄位來區分的。除了文件系統根本就不需要解釋的、分配給普通文件的數據塊的內容之外,它使用完全相同的方式來管理普通文件和目錄「文件」,文件系統將目錄「文件」的內容解釋為包含在目錄中的一系列的由 File
結構所描述的文件和子目錄。
在我們文件系統中的超級塊包含一個結構 File
(在 struct Super
中的 root
欄位中),它用於保存文件系統的根目錄的元數據。這個目錄「文件」的內容是一系列的 File
結構所描述的、位於文件系統根目錄中的文件和目錄。在根目錄中的任何子目錄轉而可以包含更多的 File
結構所表示的子目錄,依此類推。
文件系統
本實驗的目標並不是讓你去實現完整的文件系統,你只需要去實現幾個重要的組件即可。實踐中,你將負責把塊讀入到塊緩存中,並且刷新臟塊到磁碟上;分配磁碟塊;映射文件偏移量到磁碟塊;以及實現讀取、寫入、和在 IPC 介面中打開。因為你並不去實現完整的文件系統,熟悉提供給你的代碼和各種文件系統介面是非常重要的。
磁碟訪問
我們的操作系統的文件系統環境需要能訪問磁碟,但是我們在內核中並沒有實現任何磁碟訪問的功能。與傳統的在內核中添加了 IDE 磁碟驅動程序、以及允許文件系統去訪問它所必需的系統調用的「大一統」策略不同,我們將 IDE 磁碟驅動實現為用戶級文件系統環境的一部分。我們仍然需要對內核做稍微的修改,是為了能夠設置一些東西,以便於文件系統環境擁有實現磁碟訪問本身所需的許可權。
只要我們依賴輪詢、基於 「編程的 I/O」(PIO)的磁碟訪問,並且不使用磁碟中斷,那麼在用戶空間中實現磁碟訪問還是很容易的。也可以去實現由中斷驅動的設備驅動程序(比如像 L3 和 L4 內核就是這麼做的),但這樣做的話,內核必須接收設備中斷並將它派發到相應的用戶模式環境上,這樣實現的難度會更大。
x86 處理器在 EFLAGS 寄存器中使用 IOPL 位去確定保護模式中的代碼是否允許執行特定的設備 I/O 指令,比如 IN
和 OUT
指令。由於我們需要的所有 IDE 磁碟寄存器都位於 x86 的 I/O 空間中而不是映射在內存中,所以,為了允許文件系統去訪問這些寄存器,我們需要做的唯一的事情便是授予文件系統環境「I/O 許可權」。實際上,在 EFLAGS 寄存器的 IOPL 位上規定,內核使用一個簡單的「要麼全都能訪問、要麼全都不能訪問」的方法來控制用戶模式中的代碼能否訪問 I/O 空間。在我們的案例中,我們希望文件系統環境能夠去訪問 I/O 空間,但我們又希望任何其它的環境完全不能訪問 I/O 空間。
練習 1、
i386_init
通過將類型ENV_TYPE_FS
傳遞給你的環境創建函數env_create
來識別文件系統。修改env.c
中的env_create
,以便於它只授予文件系統環境 I/O 的許可權,而不授予任何其它環境 I/O 的許可權。確保你能啟動這個文件系統環境,而不會產生一般保護故障。你應該要通過在
make grade
中的 fs i/o 測試。
.
問題 1、當你從一個環境切換到另一個環境時,你是否需要做一些操作來確保 I/O 許可權設置能被保存和正確地恢復?為什麼?
注意本實驗中的 GNUmakefile
文件,它用於設置 QEMU 去使用文件 obj/kern/kernel.img
作為磁碟 0 的鏡像(一般情況下表示 DOS 或 Windows 中的 「C 盤」),以及使用(新)文件 obj/fs/fs.img
作為磁碟 1 的鏡像(」D 盤「)。在本實驗中,我們的文件系統應該僅與磁碟 1 有交互;而磁碟 0 僅用於去引導內核。如果你想去恢復其中一個有某些錯誤的磁碟鏡像,你可以通過輸入如下的命令,去重置它們到最初的、」嶄新的「版本:
$ rm obj/kern/kernel.img obj/fs/fs.img
$ make
或者:
$ make clean
$ make
小挑戰!實現中斷驅動的 IDE 磁碟訪問,既可以使用也可以不使用 DMA 模式。由你來決定是否將設備驅動移植進內核中、還是與文件系統一樣保留在用戶空間中、甚至是將它移植到一個它自己的的單獨的環境中(如果你真的想了解微內核的本質的話)。
塊緩存
在我們的文件系統中,我們將在處理器虛擬內存系統的幫助下,實現一個簡單的」緩衝區「(實際上就是一個塊緩衝區)。塊緩存的代碼在 fs/bc.c
文件中。
我們的文件系統將被限制為僅能處理 3GB 或更小的磁碟。我們保留一個大的、尺寸固定為 3GB 的文件系統環境的地址空間區域,從 0x10000000(DISKMAP
)到 0xD0000000(DISKMAP+DISKMAX
)作為一個磁碟的」內存映射版「。比如,磁碟的 0 號塊被映射到虛擬地址 0x10000000 處,磁碟的 1 號塊被映射到虛擬地址 0x10001000 處,依此類推。在 fs/bc.c
中的 diskaddr
函數實現從磁碟塊編號到虛擬地址的轉換(以及一些完整性檢查)。
由於我們的文件系統環境在系統中有獨立於所有其它環境的虛擬地址空間之外的、它自己的虛擬地址空間,並且文件系統環境僅需要做的事情就是實現文件訪問,以這種方式去保留大多數文件系統環境的地址空間是很明智的。如果在一台 32 位機器上的」真實的「文件系統上這麼做是很不方便的,因為現在的磁碟都遠大於 3 GB。而在一台有 64 位地址空間的機器上,這樣的緩存管理方法仍然是明智的。
當然,將整個磁碟讀入到內存中需要很長時間,因此,我們將它實現成」按需「分頁的形式,那樣我們只在磁碟映射區域中分配頁,並且當在這個區域中產生頁故障時,從磁碟讀取相關的塊去響應這個頁故障。通過這種方式,我們能夠假裝將整個磁碟裝進了內存中。
練習 2、在
fs/bc.c
中實現bc_pgfault
和flush_block
函數。bc_pgfault
函數是一個頁故障服務程序,就像你在前一個實驗中編寫的寫時複製 fork 一樣,只不過它的任務是從磁碟中載入頁去響應一個頁故障。在你編寫它時,記住: (1)addr
可能並不會做邊界對齊,並且 (2) 在扇區中的ide_read
操作並不是以塊為單位的。(如果需要的話)函數
flush_block
應該會將一個塊寫入到磁碟上。如果在塊緩存中沒有塊(也就是說,頁沒有映射)或者它不是一個臟塊,那麼flush_block
將什麼都不做。我們將使用虛擬內存硬體去跟蹤,磁碟塊自最後一次從磁碟讀取或寫入到磁碟之後是否被修改過。查看一個塊是否需要寫入時,我們只需要去查看uvpt
條目中的PTE_D
的 」dirty「 位即可。(PTE_D
位由處理器設置,用於表示那個頁被寫入;具體細節可以查看 x386 參考手冊的 第 5 章 的 5.2.4.3 節)塊被寫入到磁碟上之後,flush_block
函數將使用sys_page_map
去清除PTE_D
位。使用
make grade
去測試你的代碼。你的代碼應該能夠通過 check_bc、check_super、和 check_bitmap 的測試。
在 fs/fs.c
中的函數 fs_init
是塊緩存使用的一個很好的示例。在初始化塊緩存之後,它簡單地在全局變數 super
中保存指針到磁碟映射區。在這之後,如果塊在內存中,或我們的頁故障服務程序按需將它們從磁碟上讀入後,我們就能夠簡單地從 super
結構中讀取塊了。
.
小挑戰!到現在為止,塊緩存還沒有清除策略。一旦某個塊因為頁故障被讀入到緩存中之後,它將一直不會被清除,並且永遠保留在內存中。給塊緩存增加一個清除策略。在頁表中使用
PTE_A
的 accessed 位來實現,任何環境訪問一個頁時,硬體就會設置這個位,你可以通過它來跟蹤磁碟塊的大致使用情況,而不需要修改訪問磁碟映射區域的任何代碼。使用臟塊要小心。
塊點陣圖
在 fs_init
設置了 bitmap
指針之後,我們可以認為 bitmap
是一個裝滿比特位的數組,磁碟上的每個塊就是數組中的其中一個比特位。比如 block_is_free
,它只是簡單地在點陣圖中檢查給定的塊是否被標記為空閑。
練習 3、使用
free_block
作為實現fs/fs.c
中的alloc_block
的一個模型,它將在點陣圖中去查找一個空閑的磁碟塊,並將它標記為已使用,然後返回塊編號。當你分配一個塊時,你應該立即使用flush_block
將已改變的點陣圖塊刷新到磁碟上,以確保文件系統的一致性。使用
make grade
去測試你的代碼。現在,你的代碼應該要通過 alloc_block 的測試。
文件操作
在 fs/fs.c
中,我們提供一系列的函數去實現基本的功能,比如,你將需要去理解和管理結構 File
、掃描和管理目錄」文件「的條目、 以及從根目錄開始遍歷文件系統以解析一個絕對路徑名。閱讀 fs/fs.c
中的所有代碼,並在你開始實驗之前,確保你理解了每個函數的功能。
練習 4、實現
file_block_walk
和file_get_block
。file_block_walk
從一個文件中的塊偏移量映射到struct File
中那個塊的指針上或間接塊上,它非常類似於pgdir_walk
在頁表上所做的事。file_get_block
將更進一步,將去映射一個真實的磁碟塊,如果需要的話,去分配一個新的磁碟塊。使用
make grade
去測試你的代碼。你的代碼應該要通過 file_open、filegetblock、以及 fileflush/filetruncated/file rewrite、和 testfile 的測試。
file_block_walk
和 file_get_block
是文件系統中的」勞動模範「。比如,file_read
和 file_write
或多或少都在 file_get_block
上做必需的登記工作,然後在分散的塊和連續的緩存之間複製位元組。
.
小挑戰!如果操作在中途實然被打斷(比如,突然崩潰或重啟),文件系統很可能會產生錯誤。實現軟體更新或日誌處理的方式讓文件系統的」崩潰可靠性「更好,並且演示一下舊的文件系統可能會崩潰,而你的更新後的文件系統不會崩潰的情況。
文件系統介面
現在,我們已經有了文件系統環境自身所需的功能了,我們必須讓其它希望使用文件系統的環境能夠訪問它。由於其它環境並不能直接調用文件系統環境中的函數,我們必須通過一個遠程過程調用或 RPC、構建在 JOS 的 IPC 機制之上的抽象化來暴露對文件系統的訪問。如下圖所示,下圖是對文件系統服務調用(比如:讀取)的樣子:
Regular env FS env
+---------------+ +---------------+
| read | | file_read |
| (lib/fd.c) | | (fs/fs.c) |
...|.......|.......|...|.......^.......|...............
| v | | | | RPC mechanism
| devfile_read | | serve_read |
| (lib/file.c) | | (fs/serv.c) |
| | | | ^ |
| v | | | |
| fsipc | | serve |
| (lib/file.c) | | (fs/serv.c) |
| | | | ^ |
| v | | | |
| ipc_send | | ipc_recv |
| | | | ^ |
+-------|-------+ +-------|-------+
| |
+-------------------+
圓點虛線下面的過程是一個普通的環境對文件系統環境請求進行讀取的簡單機制。從(我們提供的)在任何文件描述符上的 read
工作開始,並簡單地派發到相關的設備讀取函數上,在我們的案例中是 devfile_read
(我們還有更多的設備類型,比如管道)。devfile_read
實現了對磁碟上文件指定的 read
。它和 lib/file.c
中的其它的 devfile_*
函數實現了客戶端側的文件系統操作,並且所有的工作大致都是以相同的方式來完成的,把參數打包進一個請求結構中,調用 fsipc
去發送 IPC 請求以及解包並返回結果。fsipc
函數把發送請求到伺服器和接收來自伺服器的回復的普通細節做了簡化處理。
在 fs/serv.c
中可以找到文件系統伺服器代碼。它是一個 serve
函數的循環,無休止地接收基於 IPC 的請求,並派發請求到相關的服務函數,並通過 IPC 來回送結果。在讀取示例中,serve
將派發到 serve_read
函數上,它將去處理讀取請求的 IPC 細節,比如,解包請求結構並最終調用 file_read
去執行實際的文件讀取動作。
回顧一下 JOS 的 IPC 機制,它讓一個環境發送一個單個的 32 位數字和可選的共享頁。從一個客戶端向伺服器發送一個請求,我們為請求類型使用 32 位的數字(文件系統伺服器 RPC 是有編號的,就像系統調用那樣的編號),然後通過 IPC 在共享頁上的一個 union Fsipc
中存儲請求參數。在客戶端側,我們已經在 fsipcbuf
處共享了頁;在服務端,我們在 fsreq
(0x0ffff000
)處映射入站請求頁。
伺服器也通過 IPC 來發送響應。我們為函數的返回代碼使用 32 位的數字。對於大多數 RPC,這已經涵蓋了它們全部的返回代碼。FSREQ_READ
和 FSREQ_STAT
也返回數據,它們只是被簡單地寫入到客戶端發送它的請求時的頁上。在 IPC 的響應中並不需要去發送這個頁,因為這個頁是文件系統伺服器和客戶端從一開始就共享的頁。另外,在它的響應中,FSREQ_OPEN
與客戶端共享一個新的 「Fd page」。我們將快捷地返回到文件描述符頁上。
練習 5、實現
fs/serv.c
中的serve_read
。
serve_read
的重任將由已經在fs/fs.c
中實現的file_read
來承擔(它實際上不過是對file_get_block
的一連串調用)。對於文件讀取,serve_read
只能提供 RPC 介面。查看serve_set_size
中的注釋和代碼,去大體上了解伺服器函數的結構。使用
make grade
去測試你的代碼。你的代碼通過 serveopen/filestat/file_close 和 file_read 的測試後,你得分應該是 70(總分為 150)。
.
練習 6、實現
fs/serv.c
中的serve_write
和lib/file.c
中的devfile_write
。使用
make grade
去測試你的代碼。你的代碼通過 file_write、fileread after filewrite、open、和 large file 的測試後,得分應該是 90(總分為150)。
進程增殖
我們給你提供了 spawn
的代碼(查看 lib/spawn.c
文件),它用於創建一個新環境、從文件系統中載入一個程序鏡像並啟動子環境來運行這個程序。然後這個父進程獨立於子環境持續運行。spawn
函數的行為,在效果上類似於UNIX 中的 fork
,然後同時緊跟著 fork
之後在子進程中立即啟動執行一個 exec
。
我們實現的是 spawn
,而不是一個類 UNIX 的 exec
,因為 spawn
是很容易從用戶空間中、以」外內核式「 實現的,它無需來自內核的特別幫助。為了在用戶空間中實現 exec
,想一想你應該做什麼?確保你理解了它為什麼很難。
練習 7、
spawn
依賴新的系統調用sys_env_set_trapframe
去初始化新創建的環境的狀態。實現kern/syscall.c
中的sys_env_set_trapframe
。(不要忘記在syscall()
中派發新系統調用)運行來自
kern/init.c
中的user/spawnhello
程序來測試你的代碼kern/init.c
,它將嘗試從文件系統中增殖/hello
。使用
make grade
去測試你的代碼。
.
小挑戰!實現 Unix 式的
exec
。
.
小挑戰!實現
mmap
式的文件內存映射,並如果可能的話,修改spawn
從 ELF 中直接映射頁。
跨 fork 和 spawn 共享庫狀態
UNIX 文件描述符是一個通稱的概念,它還包括管道、控制台 I/O 等等。在 JOS 中,每個這類設備都有一個相應的 struct Dev
,使用指針去指向到實現讀取/寫入/等等的函數上。對於那個設備類型,lib/fd.c
在其上實現了類 UNIX 的文件描述符介面。每個 struct Fd
表示它的設備類型,並且大多數 lib/fd.c
中的函數只是簡單地派發操作到 struct Dev
中相應函數上。
lib/fd.c
也在每個應用程序環境的地址空間中維護一個文件描述符表區域,開始位置在 FDTABLE
處。這個區域為應該程序能夠一次最多打開 MAXFD
(當前為 32)個文件描述符而保留頁的地址空間值(4KB)。在任意給定的時刻,當且僅當相應的文件描述符處於使用中時,一個特定的文件描述符表才會被映射。在區域的 FILEDATA
處開始的位置,每個文件描述符表也有一個可選的」數據頁「,如果它們被選定,相應的設備就能使用它。
我們想跨 fork
和 spawn
共享文件描述符狀態,但是文件描述符狀態是保存在用戶空間的內存中。而現在,在 fork
中,內存是標記為寫時複製的,因此狀態將被複制而不是共享。(這意味著環境不能在它們自己無法打開的文件中去搜索,並且管道不能跨一個 fork
去工作)在 spawn
上,內存將被保留,壓根不會去複製。(事實上,增殖的環境從使用一個不打開的文件描述符去開始。)
我們將要修改 fork
,以讓它知道某些被」庫管理的系統「所使用的、和總是被共享的內存區域。而不是去」硬編碼「一個某些區域的列表,我們將在頁表條目中設置一個」這些不使用「的位(就像我們在 fork
中使用的 PTE_COW
位一樣)。
我們在 inc/lib.h
中定義了一個新的 PTE_SHARE
位,在 Intel 和 AMD 的手冊中,這個位是被標記為」軟體可使用的「的三個 PTE 位之一。我們將創建一個約定,如果一個頁表條目中這個位被設置,那麼在 fork
和 spawn
中應該直接從父環境中複製 PTE 到子環境中。注意它與標記為寫時複製的差別:正如在第一段中所描述的,我們希望確保能共享頁更新。
練習 8、修改
lib/fork.c
中的duppage
,以遵循最新約定。如果頁表條目設置了PTE_SHARE
位,僅直接複製映射。(你應該去使用PTE_SYSCALL
,而不是0xfff
,去從頁表條目中掩掉有關的位。0xfff
僅選出可訪問的位和臟位。)同樣的,在
lib/spawn.c
中實現copy_shared_pages
。它應該循環遍歷當前進程中所有的頁表條目(就像fork
那樣),複製任何設置了PTE_SHARE
位的頁映射到子進程中。
使用 make run-testpteshare
去檢查你的代碼行為是否正確。正確的情況下,你應該會看到像 fork handles PTE_SHARE right
和 」spawn handles PTE_SHARE right
」 這樣的輸出行。
使用 make run-testfdsharing
去檢查文件描述符是否正確共享。正確的情況下,你應該會看到 read in child succeeded
和 「read in parent succeeded
」 這樣的輸出行。
鍵盤介面
為了能讓 shell 工作,我們需要一些方式去輸入。QEMU 可以顯示輸出,我們將它的輸出寫入到 CGA 顯示器上和串列埠上,但到目前為止,我們僅能夠在內核監視器中接收輸入。在 QEMU 中,我們從圖形化窗口中的輸入作為從鍵盤到 JOS 的輸入,雖然鍵入到控制台的輸入作為出現在串列埠上的字元的方式顯現。在 kern/console.c
中已經包含了由我們自實驗 1 以來的內核監視器所使用的鍵盤和串列埠的驅動程序,但現在你需要去把這些增加到系統中。
練習 9、在你的
kern/trap.c
中,調用kbd_intr
去處理捕獲IRQ_OFFSET+IRQ_KBD
和serial_intr
,用它們去處理捕獲IRQ_OFFSET+IRQ_SERIAL
。
在 lib/console.c
中,我們為你實現了文件的控制台輸入/輸出。kbd_intr
和 serial_intr
將使用從最新讀取到的輸入來填充緩衝區,而控制台文件類型去排空緩衝區(默認情況下,控制台文件類型為 stdin/stdout,除非用戶重定向它們)。
運行 make run-testkbd
並輸入幾行來測試你的代碼。在你輸入完成之後,系統將回顯你輸入的行。如果控制台和窗口都可以使用的話,嘗試在它們上面都做一下測試。
Shell
運行 make run-icode
或 make run-icode-nox
將運行你的內核並啟動 user/icode
。icode
又運行 init
,它將設置控制台作為文件描述符 0 和 1(即:標準輸入 stdin 和標準輸出 stdout),然後增殖出環境 sh
,就是 shell。之後你應該能夠運行如下的命令了:
echo hello world | cat
cat lorem |cat
cat lorem |num
cat lorem |num |num |num |num |num
lsfd
注意用戶庫常規程序 cprintf
將直接輸出到控制台,而不會使用文件描述符代碼。這對調試非常有用,但是對管道連接其它程序卻很不利。為將輸出列印到特定的文件描述符(比如 1,它是標準輸出 stdout),需要使用 fprintf(1, "...", ...)
。printf("...", ...)
是將輸出列印到文件描述符 1(標準輸出 stdout) 的快捷方式。查看 user/lsfd.c
了解更多示例。
練習 10、這個 shell 不支持 I/O 重定向。如果能夠運行
run sh <script
就更完美了,就不用將所有的命令手動去放入一個腳本中,就像上面那樣。為<
在user/sh.c
中添加重定向的功能。通過在你的 shell 中輸入
sh <script
來測試你實現的重定向功能。運行
make run-testshell
去測試你的 shell。testshell
只是簡單地給 shell 」喂「上面的命令(也可以在fs/testshell.sh
中找到),然後檢查它的輸出是否與fs/testshell.key
一樣。
.
小挑戰!給你的 shell 添加更多的特性。最好包括以下的特性(其中一些可能會要求修改文件系統):
- 後台命令 (
ls &
)- 一行中運行多個命令 (
ls; echo hi
)- 命令組 (
(ls; echo hi) | cat > out
)- 擴展環境變數 (
echo $hello
)- 引號 (
echo "a | b"
)- 命令行歷史和/或編輯功能
- tab 命令補全
- 為命令行查找目錄、cd 和路徑
- 文件創建
- 用快捷鍵
ctl-c
去殺死一個運行中的環境可做的事情還有很多,並不僅限於以上列表。
到目前為止,你的代碼應該要通過所有的測試。和以前一樣,你可以使用 make grade
去評級你的提交,並且使用 make handin
上交你的實驗。
本實驗到此結束。 和以前一樣,不要忘了運行 make grade
去做評級測試,並將你的練習答案和挑戰問題的解決方案寫下來。在動手實驗之前,使用 git status
和 git diff
去檢查你的變更,並不要忘記使用 git add answers-lab5.txt
去提交你的答案。完成之後,使用 git commit -am 'my solutions to lab 5』
去提交你的變更,然後使用 make handin
去提交你的解決方案。
via: https://pdos.csail.mit.edu/6.828/2018/labs/lab5/
作者:csail.mit 選題:lujun9972 譯者:qhwdw 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive