所有的實驗報告將會在 Github 同步更新,更多內容請移步至Github:https://github.com/AngelKitty/review_the_national_post-graduate_entrance_examination/blob/master/books_and_notes/professional_courses/operating_system/sources/ucore_os_lab/docs/lab_report/
練習0:填寫已有實驗
lab8
會依賴 lab1~lab7
,我們需要把做的 lab1~lab7
的代碼填到 lab8
中缺失的位置上面。練習 0 就是一個工具的利用。這裏我使用的是 Linux
下的系統已預裝好的 Meld Diff Viewer
工具。和 lab6
操作流程一樣,我們只需要將已經完成的 lab1~lab7
與待完成的 lab7
(由於 lab8
是基於 lab1~lab7
基礎上完成的,所以這裏只需要導入 lab7
)分別導入進來,然後點擊 compare
就行了。
然後軟件就會自動分析兩份代碼的不同,然後就一個個比較比較複製過去就行了,在軟件裏面是可以支持打開對比複製了,點擊 Copy Right
即可。當然 bin
目錄和 obj
目錄下都是 make
生成的,就不用複製了,其他需要修改的地方主要有以下七個文件,通過對比複製完成即可:
proc.c
default_pmm.c
pmm.c
swap_fifo.c
vmm.c
trap.c
sche.c
根據試驗要求,我們需要對部分代碼進行改進,進一步比對發現,無需改進代碼實現,直接使用即可。
練習1: 完成讀文件操作的實現(需要編碼)
要求是首先了解打開文件的處理流程,然後參考本實驗後續的文件讀寫操作的過程分析,編寫在 sfs_inode.c 中 sfs_io_nolock 讀文件中數據的實現代碼。
ucore 的文件系統模型源於 Havard 的 OS161 的文件系統和 Linux 文件系統。但其實這二者都是源於傳統的 UNIX 文件系統設計。UNIX 提出了四個文件系統抽象概念:文件(file)、目錄項(dentry)、索引節點(inode)和安裝點(mount point)。
- 文件:UNIX 文件中的內容可理解爲是一有序字節 buffer,文件都有一個方便應用程序識別的文件名稱(也稱文件路徑名)。典型的文件操作有讀、寫、創建和刪除等。
- 目錄項:目錄項不是目錄,而是目錄的組成部分。在 UNIX 中目錄被看作一種特定的文件,而目錄項是文件路徑中的一部分。如一個文件路徑名是 “/test/testfile”,則包含的目錄項爲:根目錄 “/”,目錄 “test” 和文件 “testfile”,這三個都是目錄項。一般而言,目錄項包含目錄項的名字(文件名或目錄名)和目錄項的索引節點(見下面的描述)位置。
- 索引節點:UNIX 將文件的相關元數據信息(如訪問控制權限、大小、擁有者、創建時間、數據內容等等信息)存儲在一個單獨的數據結構中,該結構被稱爲索引節點。
- 安裝點:在 UNIX 中,文件系統被安裝在一個特定的文件路徑位置,這個位置就是安裝點。所有的已安裝文件系統都作爲根文件系統樹中的葉子出現在系統中。安裝點是一個起點,從安裝點開始可以訪問文件系統中的所有文件。
其中,文件和目錄是給應用程序看到的一個抽象。
從 ucore 操作系統不同的角度來看,ucore 中的文件系統架構包含四類主要的數據結構, 它們分別是:
- 1、超級塊(SuperBlock),它主要從文件系統的全局角度描述特定文件系統的全局信息。它的作用範圍是整個OS空間。
- 2、索引節點(inode):它主要從文件系統的單個文件的角度它描述了文件的各種屬性和數據所在位置。它的作用範圍是整個OS空間。
- 3、目錄項(dentry):它主要從文件系統的文件路徑的角度描述了文件路徑中的特定目錄。它的作用範圍是整個 OS 空間。
- 4、文件(file),它主要從進程的角度描述了一個進程在訪問文件時需要了解的文件標識,文件讀寫的位置,文件引用情況等信息。它的作用範圍是某一具體進程。
文件系統,會將磁盤上的文件(程序)讀取到內存裏面來,在用戶空間裏面變成進程去進一步執行或其他操作。通過一系列系統調用完成這個過程。
根據實驗指導書,我們可以瞭解到,ucore 的文件系統架構主要由四部分組成:
- 通用文件系統訪問接口層:該層提供了一個從用戶空間到文件系統的標準訪問接口。這一層訪問接口讓應用程序能夠通過一個簡單的接口獲得 ucore 內核的文件系統服務。
- 文件系統抽象層:向上提供一個一致的接口給內核其他部分(文件系統相關的系統調用實現模塊和其他內核功能模塊)訪問。向下提供一個抽象函數指針列表和數據結構來屏蔽不同文件系統的實現細節。
- Simple FS 文件系統層:一個基於索引方式的簡單文件系統實例。向上通過各種具體函數實現以對應文件系統抽象層提出的抽象函數。向下訪問外設接口
- 外設接口層:向上提供 device 訪問接口屏蔽不同硬件細節。向下實現訪問各種具體設備驅動的接口,比如 disk 設備接口/串口設備接口/鍵盤設備接口等。
這裏我們可以通過下圖可以比較好的理解這四個部分的關係:
接下來分析下打開一個文件的詳細處理的流程。
例如某一個應用程序需要操作文件(增刪讀寫等),首先需要通過文件系統的通用文件系統訪問接口層給用戶空間提供的訪問接口進入文件系統內部,接着由文件系統抽象層把訪問請求轉發給某一具體文件系統(比如 Simple FS 文件系統),然後再由具體文件系統把應用程序的訪問請求轉化爲對磁盤上的 block 的處理請求,並通過外設接口層交給磁盤驅動例程來完成具體的磁盤操作。
對應到我們的ucore上,具體的過程如下:
- 1、 以打開文件爲例,首先用戶會在進程中調用 safe_open() 函數,然後依次調用如下函數 open->sys_open->syscall,從而引發系統調用然後進入內核態,然後會由 sys_open 內核函數處理系統調用,進一步調用到內核函數 sysfile_open,然後將字符串 "/test/testfile" 拷貝到內核空間中的字符串 path 中,並進入到文件系統抽象層的處理流程完成進一步的打開文件操作中。
- 2、 在文件系統抽象層,系統會分配一個 file 數據結構的變量,這個變量其實是 current->fs_struct->filemap[] 中的一個空元素,即還沒有被用來打開過文件,但是分配完了之後還不能找到對應對應的文件結點。所以系統在該層調用了 vfs_open 函數通過調用 vfs_lookup 找到 path 對應文件的 inode,然後調用vop_open函數打開文件。然後層層返回,通過執行語句 file->node=node;,就把當前進程的 current->fs_struct->filemap[fd](即 file 所指變量)的成員變量 node 指針指向了代表文件的索引節點 node。這時返回 fd。最後完成打開文件的操作。
- 3、 在第2步中,調用了 SFS 文件系統層的 vfs_lookup 函數去尋找 node,這裏在 sfs_inode.c 中我們能夠知道 .vop_lookup = sfs_lookup。
- 4、看到 sfs_lookup 函數傳入的三個參數,其中 node 是根目錄“/”所對應的 inode 節點;path 是文件的絕對路徑(例如“/test/file”),而 node_store 是經過查找獲得的 file 所對應的 inode 節點。 函數以“/”爲分割符,從左至右逐一分解path獲得各個子目錄和最終文件對應的 inode 節點。在本例中是分解出 “test” 子目錄,並調用 sfs_lookup_once 函數獲得 “test” 子目錄對應的 inode 節點 subnode,然後循環進一步調用 sfs_lookup_once 查找以 “test” 子目錄下的文件 “testfile1” 所對應的 inode 節點。當無法分解 path 後,就意味着找到了testfile1對應的 inode 節點,就可順利返回了。
- 5、而我們再進一步觀察 sfs_lookup_once 函數,它調用 sfs_dirent_search_nolock 函數來查找與路徑名匹配的目錄項,如果找到目錄項,則根據目錄項中記錄的 inode 所處的數據塊索引值找到路徑名對應的 SFS 磁盤 inode,並讀入 SFS 磁盤 inode 對的內容,創建 SFS 內存 inode。
如下圖所示,ucore 文件系統中,是這樣處理讀寫硬盤操作的:
- (1)首先是應用程序發出請求,請求硬盤中寫數據或讀數據,應用程序通過 FS syscall 接口執行系統調用,獲得 ucore 操作系統關於文件的一些服務;
- (2)之後,一旦操作系統內系統調用得到了請求,就會到達 VFS 層面(虛擬文件系統),包含很多部分比如文件接口、目錄接口等,是一個抽象層面,它屏蔽底層具體的文件系統;
- (3)VFS 如果得到了處理,那麼 VFS 會將這個 iNode 傳遞給 SimpleFS,注意,此時,VFS 中的 iNode 還是一個抽象的結構,在 SimpleFS 中會轉化爲一個具體的 iNode;
- (4)通過該 iNode 經過 IO 接口對於磁盤進行讀寫。
那麼,硬盤中的文件佈局又是怎樣的呢?硬盤中的佈局信息存在SFS中,如下圖所示:
上圖所示的是一個 SFS 的文件系統,其定義在(kern/fs/sfs/sfs.h,83——94行):
struct sfs_fs {
struct sfs_super super; /* on-disk superblock */
struct device *dev; /* device mounted on */
struct bitmap *freemap; /* blocks in use are mared 0 */
bool super_dirty; /* true if super/freemap modified */
void *sfs_buffer; /* buffer for non-block aligned io */
semaphore_t fs_sem; /* semaphore for fs */
semaphore_t io_sem; /* semaphore for io */
semaphore_t mutex_sem; /* semaphore for link/unlink and rename */
list_entry_t inode_list; /* inode linked-list */
list_entry_t *hash_list; /* inode hash linked-list */
};
其中,SFS 的前 3 項對應的就是硬盤文件佈局的全局信息。
那麼,接下來分析這些文件佈局的數據結構:
(1)超級塊 super_block(kern/fs/sfs/sfs.h,40——45行)
struct sfs_super {
uint32_t magic; /* magic number, should be SFS_MAGIC */
uint32_t blocks; /* # of blocks in fs */
uint32_t unused_blocks; /* # of unused blocks in fs */
char info[SFS_MAX_INFO_LEN + 1]; /* infomation for sfs */
};
超級塊,剛剛說過是一個文件系統的全局角度描述特定文件系統的全局信息。這裏面定義了標識符 magic、總塊數 blocks、空閒塊數 unused_blocks 和一些關於 SFS 的信息,通常是字符串。
(2)根目錄結構 root_dir(kern/fs/sfs/sfs.h,48——57行)
struct sfs_disk_inode {
uint32_t size; /* size of the file (in bytes) */
uint16_t type; /* one of SYS_TYPE_* above */
uint16_t nlinks; /* # of hard links to this file */
uint32_t blocks; /* # of blocks */
uint32_t direct[SFS_NDIRECT]; /* direct blocks */
uint32_t indirect; /* indirect blocks */
};
我們剛剛講過,iNode 是從文件系統的單個文件的角度它描述了文件的各種屬性和數據所在位置,相當於一個索引,而 root_dir 是一個根目錄索引,根目錄表示,我們一開始訪問這個文件系統可以看到的目錄信息。主要關注 direct 和 indirect,代表根目錄下的直接索引和間接索引。
(3)目錄項 entry(kern/fs/sfs/sfs.h,60——63行)
struct sfs_disk_entry {
uint32_t ino; /* inode number */
char name[SFS_MAX_FNAME_LEN + 1]; /* file name */
};
數組中存放的是文件的名字,ino 是該文件的 iNode 值。
僅有硬盤文件佈局還不夠,SFS 畢竟是一個在硬盤之上的抽象,它還需要傳遞上一層過來的索引值 INODE。這個 INODE 是 SFS 層面的,我們剛剛討論的 iNode 是硬盤上實際的索引。
sfs_inode(kern/fs/sfs/sfs.h,69——77行)
struct sfs_inode {
struct sfs_disk_inode *din; /* on-disk inode */
uint32_t ino; /* inode number */
bool dirty; /* true if inode modified */
int reclaim_count; /* kill inode if it hits zero */
semaphore_t sem; /* semaphore for din */
list_entry_t inode_link; /* entry for linked-list in sfs_fs */
list_entry_t hash_link; /* entry for hash linked-list in sfs_fs */
};
我們看到,sfs_disk_inode 是 SFS 層面上的 iNode 的一個成員,代表了這兩個結構之間的上下級關係。
接下來,我們來分析更高層的數據結構 VFS(虛擬文件系統)。
在 VFS 層中,我們需要對於虛擬的 iNode,和下一層的 SFS 的 iNode 進行對接。
文件系統抽象層是把不同文件系統的對外共性接口提取出來,形成一個函數指針數組,這樣,通用文件系統訪問接口層只需訪問文件系統抽象層,而不需關心具體文件系統的實現細節和接口。
(1)VFS的抽象定義(kern/fs/vfs/vfs.h,35——46行)
struct fs {
union {
struct sfs_fs __sfs_info;
} fs_info; // filesystem-specific data
enum {
fs_type_sfs_info,
} fs_type; // filesystem type
int (*fs_sync)(struct fs *fs); // Flush all dirty buffers to disk
struct inode *(*fs_get_root)(struct fs *fs); // Return root inode of filesystem.
int (*fs_unmount)(struct fs *fs); // Attempt unmount of filesystem.
void (*fs_cleanup)(struct fs *fs); // Cleanup of filesystem.???
};
主要是一些函數指針用於處理 VFS 的操作。
(2)文件結構(kern/fs/file.c,14——24行)
struct file {
enum {
FD_NONE, FD_INIT, FD_OPENED, FD_CLOSED,
} status; //訪問文件的執行狀態
bool readable; //文件是否可讀
bool writable; //文件是否可寫
int fd; //文件在 filemap 中的索引值
off_t pos; //訪問文件的當前位置
struct inode *node; //該文件對應的內存 inode 指針
atomic_t open_count; //打開此文件的次數
};
在 file 基礎之上還有一個管理所有 file 的數據結構 file_struct(kern/fs/fs.h,25——30行)
struct files_struct {
struct inode *pwd; //當前工作目錄
struct file *fd_array; //已經打開的文件對應的數組
int files_count; //打開的文件個數
};
(3)VFS 的索引 iNode(kern/fs/vfs/inode.h,29——42行)
/*
inode 數據結構是位於內存的索引節點,把不同文件系統的特定索引節點信息(甚至不能算是一個索引節點)統一封裝起來,避免了進程直接訪問具體文件系統
*/
struct inode {
union { //包含不同文件系統特定 inode 信息的 union 域
struct device __device_info; //設備文件系統內存 inode 信息
struct sfs_inode __sfs_inode_info; //SFS 文件系統內存 inode 信息
} in_info;
enum {
inode_type_device_info = 0x1234,
inode_type_sfs_inode_info,
} in_type; //此 inode 所屬文件系統類型
atomic_t ref_count; //此 inode 的引用計數
atomic_t open_count; //打開此 inode 對應文件的個數
struct fs *in_fs; //抽象的文件系統,包含訪問文件系統的函數指針
const struct inode_ops *in_ops; //抽象的 inode 操作,包含訪問 inode 的函數指針
};
我們看到在 VFS 層面的 iNode 值,包含了 SFS 和硬件設備 device 的情況。
(4)inode 的操作函數指針列表(kern/fs/vfs/inode.h,169——186行)
struct inode_ops {
unsigned long vop_magic;
int (*vop_open)(struct inode *node, uint32_t open_flags);
int (*vop_close)(struct inode *node);
int (*vop_read)(struct inode *node, struct iobuf *iob);
int (*vop_write)(struct inode *node, struct iobuf *iob);
int (*vop_fstat)(struct inode *node, struct stat *stat);
int (*vop_fsync)(struct inode *node);
int (*vop_namefile)(struct inode *node, struct iobuf *iob);
int (*vop_getdirentry)(struct inode *node, struct iobuf *iob);
int (*vop_reclaim)(struct inode *node);
int (*vop_gettype)(struct inode *node, uint32_t *type_store);
int (*vop_tryseek)(struct inode *node, off_t pos);
int (*vop_truncate)(struct inode *node, off_t len);
int (*vop_create)(struct inode *node, const char *name, bool excl, struct inode **node_store);
int (*vop_lookup)(struct inode *node, char *path, struct inode **node_store);
int (*vop_ioctl)(struct inode *node, int op, void *data);
};
inode_ops 是對常規文件、目錄、設備文件所有操作的一個抽象函數表示。對於某一具體的文件系統中的文件或目錄,只需實現相關的函數,就可以被用戶進程訪問具體的文件了,且用戶進程無需瞭解具體文件系統的實現細節。
有了上述分析後,我們可以看看如果一個用戶進程打開文件會做哪些事情?
首先假定用戶進程需要打開的文件已經存在在硬盤上。以 user/sfs_filetest1.c 爲例,首先用戶進程會調用在 main 函數中的如下語句:
int fd1 = safe_open("/test/testfile", O_RDWR | O_TRUNC);
如果 ucore 能夠正常查找到這個文件,就會返回一個代表文件的文件描述符 fd1,這樣在接下來的讀寫文件過程中,就直接用這樣 fd1 來代表就可以了。
接下來實現需要編碼的函數:
通用文件訪問接口層的處理流程:
首先進入通用文件訪問接口層的處理流程,即進一步調用如下用戶態函數:open->sys_open->syscall,從而引起系統調用進入到內核態。到了內核態後,通過中斷處理例程,會調用到 sys_open 內核函數,並進一步調用 sysfile_open 內核函數。到了這裏,需要把位於用戶空間的字符串 ”/test/testfile” 拷貝到內核空間中的字符串 path 中,並進入到文件系統抽象層的處理流程完成進一步的打開文件操作中。
文件系統抽象層(VFS)的處理流程:
1、分配一個空閒的 file 數據結構變量 file 在文件系統抽象層的處理中,首先調用的是 file_open 函數,它要給這個即將打開的文件分配一個 file 數據結構的變量,這個變量其實是當前進程的打開文件數組 current->fs_struct->filemap[] 中的一個空閒元素(即還沒用於一個打開的文件),而這個元素的索引值就是最終要返回到用戶進程並賦值給變量 fd1。到了這一步還僅僅是給當前用戶進程分配了一個 file 數據結構的變量,還沒有找到對應的文件索引節點。
爲此需要進一步調用 vfs_open 函數來找到 path 指出的文件所對應的基於 inode 數據結構的 VFS 索引節點 node。 vfs_open 函數需要完成兩件事情:通過 vfs_lookup 找到 path 對應文件的 inode;調用 vop_open 函數打開文件。
2、找到文件設備的根目錄/的索引節點需要注意,這裏的 vfs_lookup 函數是一個針對目錄的操作函數,它會調用 vop_lookup 函數來找到 SFS 文件系統中的 /test 目錄下的 testfile 文件。爲此,vfs_lookup 函數首先調用 get_device 函數,並進一步調用 vfs_get_bootfs 函數(其實調用了)來找到根目錄/對應的 inode。這個 inode 就是位於 vfs.c 中的 inode 變量 bootfs_node。這個變量在 init_main 函數(位於kern/process/proc.c)執行時獲得了賦值。
找到根目錄/下的test子目錄對應的索引節點,在找到根目錄對應的inode後,通過調用vop_lookup函數來查找/和test這兩層目錄下的文件testfile所對應的索引節點,如果找到就返回此索引節點。
3、把 file 和 node 建立聯繫。完成第3步後,將返回到 file_open 函數中,通過執行語句 file->node=node,就把當前進程的current->fs_struct->filemap[fd](即file所指變量)的成員變量 node 指針指向了代表 /test/testfile 文件的索引節點 node。這時返回 fd。經過重重回退,通過系統調用返回,用戶態的 syscall->sys_open->open->safe_open 等用戶函數的層層函數返回,最終把把fd賦值給fd1。自此完成了打開文件操作。但這裏我們還沒有分析第2和第3步是如何進一步調用 SFS 文件系統提供的函數找位於 SFS 文件系統上的 /test/testfile 所對應的 sfs 磁盤 inode 的過程。下面需要進一步對此進行分析。
sfs_lookup(kern/fs/sfs/sfs_inode.c,975——993行)
static int sfs_lookup(struct inode *node, char *path, struct inode **node_store) {
struct sfs_fs *sfs = fsop_info(vop_fs(node), sfs);
assert(*path != '\0' && *path != '/');
//以“/”爲分割符,從左至右分解path獲得各子目錄和最終文件對應的inode節點。
vop_ref_inc(node);
struct sfs_inode *sin = vop_info(node, sfs_inode);
if (sin->din->type != SFS_TYPE_DIR) {
vop_ref_dec(node);
return -E_NOTDIR;
}
struct inode *subnode;
int ret = sfs_lookup_once(sfs, sin, path, &subnode, NULL); //循環進一步調用 sfs_lookup_once查找以“test”子目錄下的文件“testfile1”所對應的inode節點。
vop_ref_dec(node);
if (ret != 0) {
return ret;
}
*node_store = subnode;
//當無法分解path後,就意味着找到了需要對應的inode節點,就可順利返回了。
return 0;
}
看到函數傳入的三個參數,其中 node 是根目錄 “/” 所對應的 inode 節點;path 是文件的絕對路徑(例如 “/test/file”),而 node_store 是經過查找獲得的file所對應的inode節點。
函數以 “/” 爲分割符,從左至右逐一分解path獲得各個子目錄和最終文件對應的 inode 節點。在本例中是分解出 “test” 子目錄,並調用sfs_lookup_once函數獲得“test”子目錄對應的 inode 節點 subnode,然後循環進一步調用 sfs_lookup_once 查找以 “test” 子目錄下的文件 “testfile1” 所對應的 inode 節點。當無法分解 path 後,就意味着找到了 testfile1 對應的 inode 節點,就可順利返回了。
而我們再進一步觀察 sfs_lookup_once 函數,它調用 sfs_dirent_search_nolock 函數來查找與路徑名匹配的目錄項,如果找到目錄項,則根據目錄項中記錄的 inode 所處的數據塊索引值找到路徑名對應的 SFS 磁盤 inode,並讀入 SFS 磁盤 inode 對的內容,創建 SFS 內存 inode。
sfs_lookup_once(kern/fs/sfs/sfs_inode.c,498——512行)
static int sfs_lookup_once(struct sfs_fs *sfs, struct sfs_inode *sin, const char *name, struct inode **node_store, int *slot) {
int ret;
uint32_t ino;
lock_sin(sin);
{ // find the NO. of disk block and logical index of file entry
ret = sfs_dirent_search_nolock(sfs, sin, name, &ino, slot, NULL);
}
unlock_sin(sin);
if (ret == 0) {
// load the content of inode with the the NO. of disk block
ret = sfs_load_inode(sfs, node_store, ino);
}
return ret;
}
最後是需要實現的函數,這裏只註釋了讀文件的部分:
static int sfs_io_nolock(struct sfs_fs *sfs, struct sfs_inode *sin, void *buf, off_t offset, size_t *alenp, bool write) {
......
......
if ((blkoff = offset % SFS_BLKSIZE) != 0) { //讀取第一部分的數據
size = (nblks != 0) ? (SFS_BLKSIZE - blkoff) : (endpos - offset);//計算第一個數據塊的大小
if ((ret = sfs_bmap_load_nolock(sfs, sin, blkno, &ino)) != 0) {
//找到內存文件索引對應的 block 的編號 ino
goto out;
}
if ((ret = sfs_buf_op(sfs, buf, size, ino, blkoff)) != 0) {
goto out;
}
//完成實際的讀寫操作
alen += size;
if (nblks == 0) {
goto out;
}
buf += size, blkno ++, nblks --;
}
//讀取中間部分的數據,將其分爲 size 大小的塊,然後一次讀一塊直至讀完
size = SFS_BLKSIZE;
while (nblks != 0) {
if ((ret = sfs_bmap_load_nolock(sfs, sin, blkno, &ino)) != 0) {
goto out;
}
if ((ret = sfs_block_op(sfs, buf, ino, 1)) != 0) {
goto out;
}
alen += size, buf += size, blkno ++, nblks --;
}
//讀取第三部分的數據
if ((size = endpos % SFS_BLKSIZE) != 0) {
if ((ret = sfs_bmap_load_nolock(sfs, sin, blkno, &ino)) != 0) {
goto out;
}
if ((ret = sfs_buf_op(sfs, buf, size, ino, 0)) != 0) {
goto out;
}
alen += size;
}
......
每次通過 sfs_bmap_load_nolock 函數獲取文件索引編號,然後調用 sfs_buf_op 完成實際的文件讀寫操作。
uint32_t blkno = offset / SFS_BLKSIZE; // The NO. of Rd/Wr begin block
uint32_t nblks = endpos / SFS_BLKSIZE - blkno; // The size of Rd/Wr blocks
blkno 就是文件開始塊的位置,nblks 是文件的大小。
請在實驗報告中給出設計實現”UNIX的PIPE機制“的概要設方案,鼓勵給出詳細設計方案。
爲了實現 UNIX 的 PIPE 機制,可以考慮在磁盤上保留一部分空間或者是一個特定的文件來作爲 pipe 機制的緩衝區,接下來將說明如何完成對 pipe 機制的支持:
- 當某兩個進程之間要求建立管道,假定將進程 A 的標準輸出作爲進程B的標準輸入,那麼可以在這兩個進程的進程控制塊上新增變量來記錄進程的這種屬性;並且同時生成一個臨時的文件,並將其在進程A, B中打開;
- 當進程 A 使用標準輸出進行 write 系統調用的時候,通過PCB中的變量可以知道,需要將這些標準輸出的數據輸出到先前提高的臨時文件中去;
- 當進程 B 使用標準輸入的時候進行 read 系統調用的時候,根據其PCB中的信息可以知道,需要從上述的臨時文件中讀取數據;
- 至此完成了對 pipe 機制的設計;
事實上,由於在真實的文件系統和用戶之間還由一層虛擬文件系統,因此我們也可以不把數據緩衝在磁盤上,而是直接保存在內存中,然後完成一個根據虛擬文件系統的規範完成一個虛擬的 pipe 文件,然後進行輸入輸出的時候只要對這個文件進行操作即可;
練習2: 完成基於文件系統的執行程序機制的實現(需要編碼)
改寫proc.c中的load_icode函數和其他相關函數,實現基於文件系統的執行程序機制。執行:make qemu。如果能看看到sh用戶程序的執行界面,則基本成功了。如果在sh用戶界面上可以執行”ls”,”hello”等其他放置在sfs文件系統中的其他執行程序,則可以認爲本實驗基本成功。
可以在 Lab 7 的基礎上進行修改,讀 elf 文件變成從磁盤上讀,而不是直接在內存中讀。
在 proc.c 中,根據註釋我們需要先初始化 fs 中的進程控制結構,即在 alloc_proc 函數中我們需要做一下修改,加上一句 proc->filesp = NULL; 從而完成初始化。
爲什麼要這樣做的呢,因爲我們之前講過,一個文件需要在 VFS 中變爲一個進程才能被執行。
修改之後 alloc_proc 函數如下:(增加一行,kern/process/proc.c,136行)
proc->filesp = NULL; //初始化fs中的進程控制結構
所以完整的 alloc_proc 函數的實現如下:
//LAB8:EXERCISE2 YOUR CODE HINT:need add some code to init fs in proc_struct, ...
static struct proc_struct *alloc_proc(void) {
struct proc_struct *proc = kmalloc(sizeof(struct proc_struct));
if (proc != NULL) {
proc->state = PROC_UNINIT; //進程狀態爲爲初始化
proc->pid = -1; //進程ID爲-1
proc->runs = 0; //進程運行時間爲0
proc->kstack = 0; //內核棧爲0
proc->need_resched = 0; //進程不需要調度
proc->parent = NULL; //父進程爲空
proc->mm = NULL; //內存管理爲空
memset(&(proc->context), 0, sizeof(struct context));
proc->tf = NULL; //中斷幀爲空
proc->cr3 = boot_cr3; //cr3寄存器
proc->flags = 0; //標記
memset(proc->name, 0, PROC_NAME_LEN);
proc->wait_state = 0; //等待狀態
proc->cptr = proc->optr = proc->yptr = NULL; //相關指針初始化
proc->rq = NULL; //運行隊列
list_init(&(proc->run_link)); //運行隊列鏈表
proc->time_slice = 0; //進程運行的時間片
proc->lab6_run_pool.left = proc->lab6_run_pool.right = proc->lab6_run_pool.parent = NULL; //進程池
proc->lab6_stride = 0;
proc->lab6_priority = 0; //優先級
proc->filesp = NULL; //初始化fs中的進程控制結構
}
return proc;
}
此外 參數在棧中的佈局如下所示:
| High Address |
----------------
| Argument |
| n |
----------------
| ... |
----------------
| Argument |
| 1 |
----------------
| padding |
----------------
| null ptr |
----------------
| Ptr Arg n |
----------------
| ... |
----------------
| Ptr Arg 1 |
----------------
| Arg Count | <-- user esp
----------------
| Low Address |
然後就是要實現 load_icode
函數,具體的實現及註釋如下所示:
static int load_icode(int fd, int argc, char **kargv) {
/* (1) create a new mm for current process
* (2) create a new PDT, and mm->pgdir= kernel virtual addr of PDT
* (3) copy TEXT/DATA/BSS parts in binary to memory space of process
* (3.1) read raw data content in file and resolve elfhdr
* (3.2) read raw data content in file and resolve proghdr based on info in elfhdr
* (3.3) call mm_map to build vma related to TEXT/DATA
* (3.4) callpgdir_alloc_page to allocate page for TEXT/DATA, read contents in file
* and copy them into the new allocated pages
* (3.5) callpgdir_alloc_page to allocate pages for BSS, memset zero in these pages
* (4) call mm_map to setup user stack, and put parameters into user stack
* (5) setup current process's mm, cr3, reset pgidr (using lcr3 MARCO)
* (6) setup uargc and uargv in user stacks
* (7) setup trapframe for user environment
* (8) if up steps failed, you should cleanup the env.
*/
assert(argc >= 0 && argc <= EXEC_MAX_ARG_NUM);
//(1)建立內存管理器
// 判斷當前進程的 mm 是否已經被釋放掉了
if (current->mm != NULL) { //要求當前內存管理器爲空
panic("load_icode: current->mm must be empty.\n");
}
int ret = -E_NO_MEM; // E_NO_MEM 代表因爲存儲設備產生的請求錯誤
struct mm_struct *mm; //建立內存管理器
if ((mm = mm_create()) == NULL) {// 爲進程創建一個新的 mm
goto bad_mm;
}
//(2)建立頁目錄
if (setup_pgdir(mm) != 0) {// 進行頁表項的設置
goto bad_pgdir_cleanup_mm;
}
struct Page *page;//建立頁表
//(3)從文件加載程序到內存
struct elfhdr __elf, *elf = &__elf;
// 從磁盤上讀取出 ELF 可執行文件的 elf-header
if ((ret = load_icode_read(fd, elf, sizeof(struct elfhdr), 0)) != 0) {//讀取 elf 文件頭
goto bad_elf_cleanup_pgdir;
}
if (elf->e_magic != ELF_MAGIC) {// 判斷該 ELF 文件是否合法
ret = -E_INVAL_ELF;
goto bad_elf_cleanup_pgdir;
}
struct proghdr __ph, *ph = &__ph;
uint32_t vm_flags, perm, phnum;
// 根據 elf-header 中的信息,找到每一個 program header
for (phnum = 0; phnum < elf->e_phnum; phnum ++) { //e_phnum 代表程序段入口地址數目,即多少各段
off_t phoff = elf->e_phoff + sizeof(struct proghdr) * phnum; //循環讀取程序的每個段的頭部
if ((ret = load_icode_read(fd, ph, sizeof(struct proghdr), phoff)) != 0) {// 讀取program header
goto bad_cleanup_mmap;
}
if (ph->p_type != ELF_PT_LOAD) {
continue ;
}
if (ph->p_filesz > ph->p_memsz) {
ret = -E_INVAL_ELF;
goto bad_cleanup_mmap;
}
if (ph->p_filesz == 0) {
continue ;
}
vm_flags = 0, perm = PTE_U;//建立虛擬地址與物理地址之間的映射
if (ph->p_flags & ELF_PF_X) vm_flags |= VM_EXEC;// 根據 ELF 文件中的信息,對各個段的權限進行設置
if (ph->p_flags & ELF_PF_W) vm_flags |= VM_WRITE;
if (ph->p_flags & ELF_PF_R) vm_flags |= VM_READ;
if (vm_flags & VM_WRITE) perm |= PTE_W;
if ((ret = mm_map(mm, ph->p_va, ph->p_memsz, vm_flags, NULL)) != 0) {// 將這些段的虛擬內存地址設置爲合法的
goto bad_cleanup_mmap;
}
off_t offset = ph->p_offset;
size_t off, size;
uintptr_t start = ph->p_va, end, la = ROUNDDOWN(start, PGSIZE);
ret = -E_NO_MEM;
//複製數據段和代碼段
end = ph->p_va + ph->p_filesz; //計算數據段和代碼段終止地址
while (start < end) {
if ((page = pgdir_alloc_page(mm->pgdir, la, perm)) == NULL) {// 爲 TEXT/DATA 段逐頁分配物理內存空間
ret = -E_NO_MEM;
goto bad_cleanup_mmap;
}
off = start - la, size = PGSIZE - off, la += PGSIZE;
if (end < la) {
size -= la - end;
}
//每次讀取size大小的塊,直至全部讀完
if ((ret = load_icode_read(fd, page2kva(page) + off, size, offset)) != 0) { //load_icode_read 通過 sysfile_read 函數實現文件讀取,將磁盤上的 TEXT/DATA 段讀入到分配好的內存空間中去
goto bad_cleanup_mmap;
}
start += size, offset += size;
}
//建立BSS段
end = ph->p_va + ph->p_memsz; //同樣計算終止地址
if (start < la) {// 如果存在 BSS 段,並且先前的 TEXT/DATA 段分配的最後一頁沒有被完全佔用,則剩餘的部分被BSS段佔用,因此進行清零初始化
if (start == end) {
continue ;
}
off = start + PGSIZE - la, size = PGSIZE - off;
if (end < la) {
size -= la - end;
}
memset(page2kva(page) + off, 0, size);
start += size;
assert((end < la && start == end) || (end >= la && start == la));
}
while (start < end) {// 如果 BSS 段還需要更多的內存空間的話,進一步進行分配
if ((page = pgdir_alloc_page(mm->pgdir, la, perm)) == NULL) {// 爲 BSS 段分配新的物理內存頁
ret = -E_NO_MEM;
goto bad_cleanup_mmap;
}
off = start - la, size = PGSIZE - off, la += PGSIZE;
if (end < la) {
size -= la - end;
}
//每次操作 size 大小的塊
memset(page2kva(page) + off, 0, size);// 將分配到的空間清零初始化
start += size;
}
}
// 關閉傳入的文件,因爲在之後的操作中已經不需要讀文件了
sysfile_close(fd);//關閉文件,加載程序結束
//(4)建立相應的虛擬內存映射表
vm_flags = VM_READ | VM_WRITE | VM_STACK;// 設置用戶棧的權限
if ((ret = mm_map(mm, USTACKTOP - USTACKSIZE, USTACKSIZE, vm_flags, NULL)) != 0) {// 將用戶棧所在的虛擬內存區域設置爲合法的
goto bad_cleanup_mmap;
}
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-PGSIZE , PTE_USER) != NULL);
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-2*PGSIZE , PTE_USER) != NULL);
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-3*PGSIZE , PTE_USER) != NULL);
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-4*PGSIZE , PTE_USER) != NULL);
//(5)設置用戶棧
mm_count_inc(mm);// 切換到用戶的內存空間,這樣的話後文中在棧上設置參數部分的操作將大大簡化,因爲具體因爲空間不足而導致的分配物理頁的操作已經交由page fault處理了,是完全透明的
current->mm = mm;
current->cr3 = PADDR(mm->pgdir);
lcr3(PADDR(mm->pgdir));
//(6)處理用戶棧中傳入的參數,其中 argc 對應參數個數,uargv[] 對應參數的具體內容的地址
uint32_t argv_size=0, i;
for (i = 0; i < argc; i ++) {// 先算出所有參數加起來的長度
argv_size += strnlen(kargv[i],EXEC_MAX_ARG_LEN + 1)+1;
}
uintptr_t stacktop = USTACKTOP - (argv_size/sizeof(long)+1)*sizeof(long);
char** uargv=(char **)(stacktop - argc * sizeof(char *));
argv_size = 0;
for (i = 0; i < argc; i ++) { //將所有參數取出來放置 uargv
uargv[i] = strcpy((char *)(stacktop + argv_size ), kargv[i]);
argv_size += strnlen(kargv[i],EXEC_MAX_ARG_LEN + 1)+1;
}
stacktop = (uintptr_t)uargv - sizeof(int); //計算當前用戶棧頂
*(int *)stacktop = argc;
//(7)設置進程的中斷幀
struct trapframe *tf = current->tf;// 設置中斷幀
memset(tf, 0, sizeof(struct trapframe));//初始化 tf,設置中斷幀
tf->tf_cs = USER_CS;// 需要返回到用戶態,因此使用用戶態的數據段和代碼段的選擇子
tf->tf_ds = tf->tf_es = tf->tf_ss = USER_DS;
tf->tf_esp = stacktop;// 棧頂位置爲先前計算過的棧頂位置,注意在C語言的函數調用規範中,棧頂指針指向的位置應該是返回地址而不是第一個參數,這裏讓棧頂指針指向了第一個參數的原因在於,在中斷返回之後,會跳轉到ELF可執行程序的入口處,在該入口處會進一步使用call命令調用主函數,這時候也就完成了將 Return address 入棧的功能,因此這裏無需畫蛇添足壓入返回地址
tf->tf_eip = elf->e_entry;// 將返回地址設置爲用戶程序的入口
tf->tf_eflags = FL_IF;// 允許中斷,根據 IA32 的規範,eflags 的第 1 位需要恆爲 1
ret = 0;
//(8)錯誤處理部分
out:
return ret; //返回
bad_cleanup_mmap:
exit_mmap(mm);
bad_elf_cleanup_pgdir:
put_pgdir(mm);
bad_pgdir_cleanup_mm:
mm_destroy(mm);
bad_mm:
goto out;
}
load_icode 主要是將文件加載到內存中執行,從上面的註釋可知分爲了一共七個步驟:
- 1、建立內存管理器
- 2、建立頁目錄
- 3、將文件逐個段加載到內存中,這裏要注意設置虛擬地址與物理地址之間的映射
- 4、建立相應的虛擬內存映射表
- 5、建立並初始化用戶堆棧
- 6、處理用戶棧中傳入的參數
- 7、最後很關鍵的一步是設置用戶進程的中斷幀
- 8、發生錯誤還需要進行錯誤處理。
當然一旦發生錯誤還需要進行錯誤處理。
請在實驗報告中給出設計實現基於”UNIX的硬鏈接和軟鏈接機制“的概要設方案,鼓勵給出詳細設計方案;
觀察到保存在磁盤上的 inode 信息均存在一個 nlinks 變量用於表示當前文件的被鏈接的計數,因而支持實現硬鏈接和軟鏈接機制;
- 如果在磁盤上創建一個文件 A 的軟鏈接 B,那麼將 B 當成正常的文件創建 inode,然後將 TYPE 域設置爲鏈接,然後使用剩餘的域中的一個,指向 A 的 inode 位置,然後再額外使用一個位來標記當前的鏈接是軟鏈接還是硬鏈接;
- 當訪問到文件 B(read,write 等系統調用),判斷如果 B 是一個鏈接,則實際是將對B指向的文件A(已經知道了 A 的 inode 位置)進行操作;
- 當刪除一個軟鏈接 B 的時候,直接將其在磁盤上的 inode 刪掉即可;
- 如果在磁盤上的文件 A 創建一個硬鏈接 B,那麼在按照軟鏈接的方法創建完 B 之後,還需要將 A 中的被鏈接的計數加 1;
- 訪問硬鏈接的方式與訪問軟鏈接是一致的;
- 當刪除一個硬鏈接B的時候,除了需要刪除掉 B 的 inode 之外,還需要將 B 指向的文件 A 的被鏈接計數減 1,如果減到了 0,則需要將 A 刪除掉;
最終的實驗結果如下圖所示:
直接運行答案文件夾可能會出錯,需要在 Makefile 最後加上:
tags:
@echo TAGS ALL
$(V)rm -f cscope.files cscope.in.out cscope.out cscope.po.out tags
$(V)find . -type f -name "*.[chS]" >cscope.files
$(V)cscope -bq
$(V)ctags -L cscope.files
或者將 lab8 中的 Makefile,複製到 lab8_result 中(本項目已做過更正),覆蓋掉原有的 Makefile,待 make qemu 信息輸出完畢後,點進 qemu 界面,輸入 ls,回車,可以看到文件信息: