1. 程式人生 > 其它 >BUAA_OS lab5實驗報告

BUAA_OS lab5實驗報告

一、思考題

1. 思考5.1

查閱資料,瞭解 Linux/Unix 的 /proc 檔案系統是什麼?有什麼作用? Windows 作業系統又是如何實現這些功能的?proc 檔案系統這樣的設計有什麼好處和可以改進的地方?

Linux系統上的/proc是一種偽檔案系統(虛擬檔案系統),儲存的是當前核心執行狀態的一系列特殊檔案。

使用者可以通過這些檔案檢視有關係統硬體及當前正在執行程序的資訊,甚至可以通過更改其中某些檔案來改變核心的執行狀態。因為系統資訊如程序是動態改變的,所以使用者或應用程式讀取proc檔案時,proc檔案系統是動態從系統核心讀出所需資訊並提交的。

Windows通過呼叫Win32 API函式實現與核心模式的互動。

proc檔案系統的設計將核心互動變成了簡單的修改檔案,大大簡化了互動的過程。另一方面,Windows的多個磁碟意味著有多個根目錄,而proc檔案系統只有一個根目錄,這是一個相對不足的地方。

2. 思考5.2

如果我們通過 kseg0 讀寫裝置,我們對於裝置的寫入會快取到 Cache 中。通過 kseg0 訪問裝置是一種錯誤的行為,在實際編寫程式碼的時候這麼做會引發不可預知的問題。請你思考:這麼做這會引起什麼問題?對於不同種類的裝置(如我們提到的串列埠裝置和 IDE 磁碟)的操作會有差異嗎?可以從快取的性質和快取重新整理的策略來考慮。

快取機制是為了提高效率而設計的,具體體現為資料發生改變時並不立即寫入記憶體,而是在cache發生替換時才寫入。而我們的控制檯需要實時的互動,因此若寫入kseg0部分,資料會經過cache,甚至可能很久都不被真正寫入,因此我們看到的輸出可能並不是該部分實際的內容,基於這種歷史資料很可能給出錯誤的互動行為。

不同種類的裝置操作有差異。串列埠相對於IDE磁碟速度更快,因此操作串列埠時可以考慮提高快取的重新整理頻率。相比之下,可能是由於涉及到多個線路的資料衝突問題,IDE的操作更加複雜。

3. 思考5.3

一個磁碟塊最多儲存 1024 個指向其他磁碟塊的指標,試計算我們的檔案系統支援的單個檔案的最大大小為多大?

因為BY2BLK = 4096,即一個磁碟塊大小為4KB,所以1024個磁碟塊共1024 * 4KB = 4MB。單個檔案最大為4MB。

4. 思考5.4

查詢程式碼中的相關定義,試回答一個磁碟塊中最多能儲存多少個檔案控制塊?一個目錄下最多能有多少個檔案?

BY2FILE = 256,即一個檔案控制塊為256B。BY2BLK = 4096,即一個磁碟塊4KB。因此一個磁碟塊中包含4KB / 256B = 16個檔案控制塊。

一個目錄包含1024個指向磁碟塊的指標,即最多有1024 * 16 = 16384個檔案。

5. 思考5.5

請思考,在滿足磁碟塊快取的設計的前提下,我們實驗使用的核心支援的最大磁碟大小是多少?

DISKMAX = 0x40000000,因此支援的最大磁碟大小為1GB。

6. 思考5.6

如果將 DISKMAX 改成 0xC0000000, 超過使用者空間,我們的檔案系統還能正常工作嗎?為什麼?

若檔案過大,將不能正常工作。地址超過使用者空間後,若進行alloc是沒有許可權的,此時程式會直接終止。

7. 思考5.7

閱讀 user/file.c,你會發現很多函式中都會將一個 struct Fd* 型的 指標轉換為 struct Filefd* 型的指標,請解釋為什麼這樣的轉換可行。

注意到,兩個結構體的定義如下:

struct Fd {
u_int fd_dev_id;
u_int fd_offset;
u_int fd_omode;
};

struct Filefd {
struct Fd f_fd;
u_int f_fileid;
struct File f_file;
};

Filefd結構體的第一個成員就是Fd,因此指向Filefd的指標同樣指向這個Fd的起始位置,故可以強制轉換。

8. 思考5.8

請解釋 Fd, Filefd, Open 結構體及其各個域的作用。比如各個結構體會在哪些過程中被使用,是否對應磁碟上的物理實體還是單純的記憶體資料等。說明形式自定,要求簡潔明瞭,可大致勾勒出檔案系統資料結構與物理實體的對應關係與設計框架。

Fd用於記錄檔案的基本資訊,需要單獨佔用一頁:

// file descriptor
struct Fd {
u_int fd_dev_id; // 外設的id
u_int fd_offset; // 讀寫的偏移量
u_int fd_omode; // 開啟方式,包括只讀、只寫、讀寫
};

Filefd用於記錄檔案的詳細資訊,Fd也儲存在其中:

// file descriptor + file
struct Filefd {
struct Fd f_fd; // file descriptor
u_int f_fileid; // 檔案的id
struct File f_file; // 真正的檔案本身
};

Open是開啟檔案行為的抽象:

struct Open {
struct File *o_file; // 指向開啟的檔案
u_int o_fileid; // 開啟檔案的id
int o_mode; // 開啟方式
struct Filefd *o_ff; // 指向讀寫的位置(偏移)
};

9. 思考5.9

閱讀serve函式的程式碼,我們注意到函式中包含了一個死迴圈for (;;) {...},為什麼這段程式碼不會導致整個核心進入panic 狀態?

因為serve呼叫ipc_recv函式後會將自身狀態變為ENV_NOT_RUNNABLE,進入等待狀態。從某種意義上它更像是一個後臺程式,在其他程序發出檔案系統請求後才被喚醒並開始服務。

二、實驗難點

本次實驗程式碼量較大,難度也體現在了閱讀課程組提供的程式碼上,我閱讀了主要程式碼部分,並對各個函式的功能做了理解與記錄:

使用者程序

fd.c

  • dev_lookup(int dev_id, struct Dev **dev):查詢id對應的dev

  • fd_alloc(struct Fd **fd):遍歷尋找一個未使用的fd

  • fd_close(struct Fd *fd):呼叫syscall_mem_unmap解除fd對應地址所在頁的對映

  • fd_lookup(int fdnum, struct Fd **fd):根據fdnum查詢對應的fd

  • fd2data(struct Fd *fd):返回fd對應的儲存資料的首地址

  • fd2num(struct Fd *fd):返回fd對應的fdnum

  • num2fd(int fd):返回fdnum對應的fd的地址(實名吐槽引數命名為fd)

  • close(int fdnum):根據fdnum查詢到相應的fd,呼叫dev對應的close函式關閉檔案描述符,之後呼叫fd_close解除對映

  • close_all(void):對所有fd呼叫close函式關閉

  • dup(int oldfdnum, int newfdnum):呼叫syscall_mem_map將舊檔案複製到新檔案

  • read(int fdnum, void *buf, u_int n):

    • 呼叫fd_lookup查詢fdnum對應的fd

    • 呼叫dev_lookup查詢dev_id對應的dev

    • 呼叫dev_read從offset處讀取n個位元組到buf,更新offset

    • buf以'\0'結尾

  • readn(int fdnum, void *buf, u_int n):反著讀n個位元組到進buf

  • write(int fdnum, const void *buf, u_int n):

    • 呼叫fd_lookup查詢fdnum對應的fd

    • 呼叫dev_lookup查詢dev_id對應的dev

    • 呼叫dev_write將buf中的n個位元組寫到offset位置,更新offset

  • seek(int fdnum, u_int offset):根據fdnum查詢到相應的fd,設定offset

file.c

  • open(const char *path, int mode):

    • 呼叫fd_alloc分配出一個空閒的fd,用於記錄即將開啟的檔案

    • 呼叫fsipc_open開啟path路徑上的檔案(由serve程序將file資訊載入到fd上)

    • 呼叫fd2data獲取fd對應的用於儲存資料的首地址

    • for迴圈遍歷,呼叫syscall_mem_alloc以va起始開闢儲存空間,呼叫fsipc_map(由serve程序將硬碟上的檔案資料讀取到va位置)

    • 返回檔案對應的fdnum

  • file_close(struct Fd *fd):

    • 呼叫fd2data獲得fd對應的儲存資料的首地址

    • 呼叫fsipc_dirty由serve程序將檔案資料對應頁標記dirty

    • 呼叫fsipc_close由serve程序關閉檔案

    • 將資料頁面解除對映

  • file_read(struct Fd *fd, void *buf, u_int n, u_int offset):從fd對應的offset位置讀取n個位元組到buf

  • read_map(int fdnum, u_int offset, void **blk):找到fdnum對應的fd中offset偏移位置的虛擬地址,並傳遞給blk

  • file_write(struct Fd *fd, const void *buf, u_int n, u_int offset):從buf中寫n個位元組到fd的offset偏移位置,如果檔案大小不夠,呼叫ftruncate函式擴容

  • ftruncate(int fdnum, u_int size):

    • 呼叫fsipc_set_size由serve程序設定新的檔案大小

    • 若增加檔案大小,呼叫fsipc_map由serve程序將新增頁面對映到va起始的相應地址

    • 若減小檔案大小,呼叫syscall_mem_unmap解除多餘頁面的對映

  • remove(const char *path):呼叫fsipc_remove由serve程序刪除檔案

fsipc.c

  • fsipc(u_int type, void *fsreq, u_int dstva, u_int *perm):程序通訊

    • 將請求傳送給serve程序(傳遞了fsreq),serve程序拿到了之後開始按類別處理

    • 設定使用者程序接收資訊的地址,允許接收,此時接收serve處理後傳回的資訊

  • fsipc_open(const char *path, u_int omode, struct Fd *fd):將開啟檔案的路徑、開啟方式通過“工具人”頁面傳遞給serve程序,請求服務

  • fsipc_map(u_int fileid, u_int offset, u_int dstva):將需要對映的檔案id、偏移offset通過“工具人”頁面傳遞給serve程序,請求服務

  • fsipc_set_size(u_int fileid, u_int size):將檔案id、需要設定的檔案大小通過“工具人”頁面傳遞給serve程序,請求服務

  • fsipc_close(u_int fileid):將需要關閉的檔案id通過“工具人”頁面傳遞給serve程序,請求服務

  • fsipc_dirty(u_int fileid, u_int offset):將檔案id、需要設定的檔案大小通過“工具人”頁面傳遞給serve程序,請求服務

  • fsipc_remove(const char *path):將需要刪除的檔案路徑通過“工具人”頁面傳遞給serve程序,請求服務

fs程序

fs.c

  • file_dirty(struct File *f, u_int offset):呼叫file_get_block獲得offset處對應的block,寫入原有內容使其標記為dirty

  • dir_lookup(struct File *dir, char *name, struct File **file):從dir目錄下找到名為name的檔案

  • dir_alloc_file(struct File *dir, struct File **file):在dir目錄下開闢一個新的file結構體

  • skip_slash(char *p):跳過'/'

  • walk_path(char *path, struct File **pdir, struct File **pfile, char *lastelem):沿路徑path查詢檔案,若存在,存到pfile中,若不存在,將不存在的部分存入lastelem中

  • file_open(char *path, struct File **file):沿路徑找到這一檔案

  • file_create(char *path, struct File **file):在path路徑下建立檔案,呼叫dir_alloc_file開闢新的檔案空間,將檔名稱賦給新開闢的檔案

  • file_truncate(struct File *f, u_int newsize):截斷檔案大小,若大小小於NDIRECT,順便將f_indirect清空。呼叫file_clear_block釋放資料塊(在位圖裡置位1)

  • file_set_size(struct File *f, u_int newsize):設定file的新大小,若大小變小,呼叫file_truncate截斷,之後將檔案寫回磁碟

  • file_flush(struct File *f):將檔案中的內容寫回到磁碟中

  • file_close(struct File *f):關閉檔案,將資料寫回(包括檔案和檔案所在目錄)

  • file_remove(char *path):walk_path找到路徑上的檔案,呼叫file_truncate將檔案大小設為0,將檔案和目錄內容寫回磁碟

serv.c

  • serve_init(void):初始化opentab

  • open_alloc(struct Open **o):找一塊新的空間儲存Open結構體(分配一個新的頁面)

  • open_lookup(u_int envid, u_int fileid, struct Open **po):查詢fileid對應的open檔案

  • serve_open(u_int envid, struct Fsreq_open *rq):

    • 從rq中回去請求資訊

    • 呼叫open_alloc分配開啟檔案

    • 呼叫file_open按路徑開啟檔案

    • 將檔案資訊傳遞給開啟檔案

    • 將開啟檔案通過程序通訊傳遞給使用者程序

  • serve_map(u_int envid, struct Fsreq_map *rq):

    • 呼叫opne_lookup查詢開啟的檔案

    • 呼叫file_get_block查詢檔案對應的資料塊

    • 通過程序通訊傳遞這一資料塊

  • serve_set_size(u_int envid, struct Fsreq_set_size *rq):

    • 呼叫opne_lookup查詢開啟的檔案

    • 呼叫file_set_size改變檔案大小

  • serve_close(u_int envid, struct Fsreq_close *rq):

    • 呼叫opne_lookup查詢開啟的檔案

    • 呼叫file_close關閉開啟的檔案

  • serve_remove(u_int envid, struct Fsreq_remove *rq):呼叫file_remove根據path刪除檔案

  • serve_dirty(u_int envid, struct Fsreq_dirty *rq):

    • 呼叫opne_lookup查詢開啟的檔案

    • 呼叫file_dirty將檔案佔用的頁面標記dirty

  • serve(void):

    • 呼叫ipc_recv從使用者程序接收請求

    • 根據req型別呼叫不同的serve函式

    • 系統呼叫接觸REQVA地址的對映

使用者程序和檔案系統的邏輯

以open函式的呼叫為例:

三、體會與感想

本次lab看上去似乎並不是很難,當然程式碼量還是有些大,需要認真閱讀,理解呼叫邏輯。總花費時間約30小時,看上去各個lab用時還是很平均的。在lab4發現GitHub上有往屆的測試程式碼後,我彷彿發現了新世界,於是每次課上測試之前都試圖通過往屆程式碼獲取一些提示,但事實上lab5的兩次上機題目都和往屆有很大的差異,不得不說課上看到題的時候還是很懵的,但萬幸過了exam?有驚無險。

lab5-2課上的時候似乎全程在模仿課程組提供的open邏輯,可能也是因為緊張,對每一步都模稜兩可的,所以似乎更不如說是靠運氣過了。現在想來雖然課下有嘗試講出使用者程序請求檔案系統服務的邏輯鏈,但具體細節卻沒有注意到(比如通過ipc通訊傳遞異常值),只有到課上測試卡bug了才暴露出來,紙上談兵啊。

OS上機的漫漫長路過去了,然而整個過程還是充滿了忐忑,4次extra,掛了兩次lab,驚險收支平衡,看來還是沒有很好地掌握很多內容的執行邏輯,需要更好地理解,做到舉一反三。

四、殘留難點

本次lab暫無殘留難點。