1. 程式人生 > >XV6學習(14)Lab fs: File system

XV6學習(14)Lab fs: File system

程式碼在[github](https://github.com/weijunji/xv6-6.S081/tree/fs)上。 這次實驗是要對檔案系統修改,使其支援更大的檔案以及符號連結,實驗本身並不是很複雜。但檔案系統可以說是XV6中最複雜的部分,整個檔案系統包括了七層:檔案描述符,路徑名,目錄,inode,日誌,緩衝區,磁碟。 檔案描述符類似於Linux,將檔案、管道、裝置、套接字等都抽象為檔案描述符,從而可以使用`read`和`write`系統呼叫對其進行讀寫。XV6的`read`和`write`是使用if-else來對描述符型別進行判斷,選擇對應的底層函式;而在Linux中,則是使用函式指標直接指向對應的底層函式,避免進行多次判斷。 路徑名則提供了根據路徑名從目錄系統中查詢檔案的功能。在路徑查詢過程中需要避免可能會出現的死鎖,例如路徑名中包含`..`。 目錄層類似於檔案,目錄檔案的內部會儲存該目錄的目錄項`struct dirent`,其中包含了檔名和對應的inode號。在XV中目錄查詢是使用遍歷目錄項陣列來依次比較,時間複雜度為O(n);而在NTFS、ZFS等檔案系統中,會使用磁碟平衡樹來組織目錄項,使目錄查詢的複雜度降低為O(lgn)。 inode層為檔案在磁碟上的組織,在磁碟中會有一塊區域用於儲存`inode`資訊,包括檔案型別、大小、連結數以及檔案每個塊對應的磁碟塊號。通過路徑從目錄系統中查詢到對應的`inode`號,之後就可以從磁碟上讀取對應的`inode`資訊,之後就可以根據偏移量查詢對應的磁碟塊號,最後對其進行讀寫。 日誌層提供了事務以及故障恢復的功能,當有多個磁碟操作必須原子完成時就要用到事務(如刪除檔案時要從目錄中刪除檔案,刪除檔案對應的inode,對空閒塊bitmap進行修改等)。日誌先將操作寫到磁碟的日誌區上,寫入完成後再寫入`commit`,最後再將所有操作真正寫到磁碟上去。當在寫入`commit`之前發生故障,就不需要進行操作,因為事務沒有被提交;當在寫入`commit`之後發生故障,就將日誌區的日誌全部重寫一遍,保證事務被正確提交。 緩衝區則提供了磁碟塊快取,同時保證一個磁碟塊在緩衝區中只有一個,使得同一時間只能有一個執行緒對同一個塊進行操作,避免讀到的資料不一致。 ## Large files (moderate) 這一個實驗是要使XV6支援更大的檔案。原始XV6中的檔案塊號`dinode.addr`是使用一個大小為12的直接塊表以及一個大小為256的一級塊表,即檔案最大為`12+256`塊。可以通過將一個直接塊表中的項替換為一個二級塊表來使系統支援大小為`11+256+256*256`個塊的檔案。 首先修改對應的巨集以及`inode`定義。 ```c #define NDIRECT 11 #define NINDIRECT (BSIZE / sizeof(uint)) #define MAXFILE (NDIRECT + NINDIRECT + NINDIRECT * NINDIRECT) struct dinode { ... uint addrs[NDIRECT+2]; // Data block addresses }; struct inode { ... uint addrs[NDIRECT+2]; // Data block addresses }; ``` 之後修改`bmap`函式,使其支援二級塊表,其實就是重複一次塊表的查詢過程。 ```c static uint bmap(struct inode *ip, uint bn) { ... bn -= NINDIRECT; if(bn < NINDIRECT * NINDIRECT){ // double indirect int idx = bn / NINDIRECT; int off = bn % NINDIRECT; if((addr = ip->addrs[NDIRECT + 1]) == 0) ip->addrs[NDIRECT + 1] = addr = balloc(ip->dev); bp = bread(ip->dev, addr); a = (uint*)bp->data; if((addr = a[idx]) == 0){ a[idx] = addr = balloc(ip->dev); log_write(bp); } brelse(bp); bp = bread(ip->dev, addr); a = (uint*)bp->data; if((addr = a[off]) == 0){ a[off] = addr = balloc(ip->
dev); log_write(bp); } brelse(bp); return addr; } panic("bmap: out of range"); } ``` 最後修改`itrunc`函式使其能夠釋放二級塊表對應的塊,主要就是注意一下`brelse`的呼叫就行了,仿照一級塊表的處理就行了。 ```c void itrunc(struct inode *ip) { ... if(ip->addrs[NDIRECT + 1]){ bp = bread(ip->dev, ip->addrs[NDIRECT + 1]); a = (uint*)bp->
data; struct buf *bpd; uint* b; for(j = 0; j < NINDIRECT; j++){ if(a[j]){ bpd = bread(ip->dev, a[j]); b = (uint*)bpd->data; for(int k = 0; k < NINDIRECT; k++){ if(b[k]) bfree(ip->dev, b[k]); } brelse(bpd); bfree(ip->dev, a[j]); } } brelse(bp); bfree(ip->dev, ip->addrs[NDIRECT + 1]); ip->addrs[NDIRECT + 1] = 0; } ip->size = 0; iupdate(ip); } ``` ## Symbolic links (moderate) 這一個實驗是要實現符號連結,符號連結就是在檔案中儲存指向檔案的路徑名,在開啟檔案的時候根據儲存的路徑名再去查詢實際檔案。與符號連結相反的就是硬連結,硬連結是將檔案的`inode`號指向目標檔案的`inode`,並將引用計數加一。 `symlink`的系統呼叫實現起來也很簡單,就是建立一個`inode`,設定型別為`T_SYMLINK`,然後向這個`inode`中寫入目標檔案的路徑就行了。 ```c uint64 sys_symlink(void) { char target[MAXPATH]; memset(target, 0, sizeof(target)); char path[MAXPATH]; if(argstr(0, target, MAXPATH) < 0 || argstr(1, path, MAXPATH) < 0){ return -1; } struct inode *ip; begin_op(); if((ip = create(path, T_SYMLINK, 0, 0)) == 0){ end_op(); return -1; } if(writei(ip, 0, (uint64)target, 0, MAXPATH) != MAXPATH){ // panic("symlink write failed"); return -1; } iunlockput(ip); end_op(); return 0; } ``` 最後在`sys_open`中新增對符號連結的處理就行了,當模式不是`O_NOFOLLOW`的時候就對符號連結進行迴圈處理,直到找到真正的檔案,如果迴圈超過了一定的次數(10),就說明可能發生了迴圈連結,就返回-1。這裡主要就是要注意`namei`函式不會對`ip`上鎖,需要使用`ilock`來上鎖,而`create`則會上鎖。 ```c uint64 sys_open(void) { ... if(ip->
type == T_DEVICE && (ip->major < 0 || ip->major >= NDEV)){ ... } if(ip->type == T_SYMLINK){ if(!(omode & O_NOFOLLOW)){ int cycle = 0; char target[MAXPATH]; while(ip->type == T_SYMLINK){ if(cycle == 10){ iunlockput(ip); end_op(); return -1; // max cycle } cycle++; memset(target, 0, sizeof(target)); readi(ip, 0, (uint64)target, 0, MAXPATH); iunlockput(ip); if((ip = namei(target)) == 0){ end_op(); return -1; // target not exist } ilock(ip); } } } if((f = filealloc()) == 0 || (fd = fdalloc(f)) < 0){ .