在本實驗中,你將要去實現 spawn
,它是一個載入和執行磁碟上可執行檔案的庫呼叫。然後,你接著要去充實你的核心和庫,以使作業系統能夠在控制台上執行一個 shell。而這些特性需要一個檔案系統,本實驗將引入一個可讀/寫的簡單檔案系統。
使用 Git 去獲取最新版的課程倉庫,然後建立一個命名為 lab5
的本地分支,去跟蹤遠端的 origin/lab5
分支:
athena% cd ~/6.828/labathena% add gitathena% git pullAlready up-to-date.athena% git checkout -b lab5 origin/lab5Branch lab5 set up to track remote branch refs/remotes/origin/lab5.Switched to a new branch "lab5"athena% git merge lab4Merge 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 中直接對映頁。
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
並輸入幾行來測試你的程式碼。在你輸入完成之後,系統將回顯你輸入的行。如果控制台和視窗都可以使用的話,嘗試在它們上面都做一下測試。
執行 make run-icode
或 make run-icode-nox
將執行你的核心並啟動 user/icode
。icode
又執行 init
,它將設定控制台作為檔案描述符 0 和 1(即:標準輸入 stdin 和標準輸出 stdout),然後增殖出環境 sh
,就是 shell。之後你應該能夠執行如下的命令了:
echo hello world | catcat lorem |catcat lorem |numcat lorem |num |num |num |num |numlsfd
注意使用者庫常規程式 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
去提交你的解決方案。