1. 程式人生 > >MIT 6.S081 Lab File System

MIT 6.S081 Lab File System

前言 ======= 開啟自己的blog一看,居然三個月沒更新了...回想一下前幾個月,開題 + 實驗室雜活貌似也沒佔非常多的時間,還是自己太懈怠了吧,掉線城和文明6真的是時間剎手( 不過好訊息是把15445的所有lab都錘完了,最近一個月應該沒啥活幹。立個flag,這個月更它個10篇blog,把15445的知識點、lab,以及6.S081想寫的東西都寫完。今天先做個復健,碼一下剛做完的lab8,以及xv6的file system的學習筆記。 壞訊息是leetcode一道沒動,甚至主力啥語言啥框架還沒定下來,開學找實習可能要炸了orz... 這個lab不算難,總程式碼量也就幾十行,絕大多數時間拿來讀檔案系統相關的程式碼了(兩三天吧),不過收穫也挺大,理清楚xv6檔案系統的邏輯後,lab一個晚上就搞定了。因此這篇blog我會寫的簡單一些,大頭部分放在file system分析的blog中。 強烈推薦在做這個lab前把《xv6 book》中關於file system的章節全部看完,並詳細分析相關的程式碼。我的這篇關於xv6的file system的學習筆記可能對你有幫助: TODO:(在碼了在碼了,但願今天能碼完) Lab連結:https://pdos.csail.mit.edu/6.828/2019/labs/fs.html 都2021年了還在做2019的lab,這真的好嘛( Part1 Large Files ========= xv6選擇的檔案儲存介質是磁碟,通過 `struct dinode` 來描述一個`檔案`在磁碟上儲存,並用 `struct inode`作為其對一個的`struct dinode`的拷貝,儲存在記憶體中: // kernel/fs.h struct dinode { short type; // File type short major; // T_DEVICE only short minor; // T_DEVICE only short nlink; // Number of links to inode in file system uint size; // Size of file (bytes) uint addrs[NDIRECT+1]; // Data block addresses }; // kernel/file.h // in-memory copy of an inode struct inode { uint dev; // Device number uint inum; // Inode number int ref; // Reference count struct sleeplock lock; // protects everything below here int valid; // inode has been read from disk? short type; // copy of disk inode short major; short minor; short nlink; uint size; uint addrs[NDIRECT + 1]; }; 我們只關注其中的`addrs`。`addrs`記錄著檔案所在磁碟的盤塊號,這個陣列的容量為13,前12個地址是直接地址,即該檔案的第 0 ~ 11 號盤塊,第13個地址是一個`一級索引`,用於索引檔案的第 12 ~ 12 + 256 號盤塊,如下圖所示: ![](https://img2020.cnblogs.com/blog/2028256/202102/2028256-20210202134325731-1859337887.png) 這樣,一個檔案最多可以佔用 268 個盤塊。 `bmap`是xv6中非常重要的api,其返回檔案偏移量(bn * BSIZE)所對應的的磁碟盤塊號: // kernel/fs.c static uint bmap(struct inode *ip, uint bn) { uint addr, *a; struct buf *bp; if(bn < NDIRECT){ // offset in in NDIRECT range if((addr = ip->addrs[bn]) == 0) ip->addrs[bn] = addr = balloc(ip->dev); return addr; } bn -= NDIRECT; if(bn < NINDIRECT){ // Load indirect block. If indirect block is not exist, allocate it. if((addr = ip->addrs[NDIRECT]) == 0) ip->addrs[NDIRECT] = addr = balloc(ip->dev); bp = bread(ip->dev, addr); a = (uint*)bp->data; if((addr = a[bn]) == 0){ a[bn] = addr = balloc(ip->dev); log_write(bp); } brelse(bp); return addr; } 當xv6刪除檔案時,要將檔案所對應的盤塊一一釋放。如果這個檔案存在`一級索引塊`,那麼除了釋放間接索引塊的表項所對應的盤塊之外,還要將這塊一級索引塊一併釋放: static void itrunc(struct inode *ip) { int i, j; struct buf *bp; uint *a; for(i = 0; i < NDIRECT; i++){ // free direct block if(ip->addrs[i]){ bfree(ip->dev, ip->addrs[i]); ip->addrs[i] = 0; } } if(ip->addrs[NDIRECT]){ bp = bread(ip->dev, ip->addrs[NDIRECT]); a = (uint*)bp->data; for(j = 0; j < NINDIRECT; j++){ // free block indexed by indirect-block if(a[j]) bfree(ip->dev, a[j]); } brelse(bp); bfree(ip->dev, ip->addrs[NDIRECT]); // free indirect-block ip->addrs[NDIRECT] = 0; } } Part1要求我們為xv6的檔案系統增添一個`二級索引`。一個`二級索引`佔據一個盤塊,共計有256個表項,每個表項都指向一個`一級索引塊`。由於addr的容量仍然是13,因此需要犧牲一個一級索引項,一個檔案的盤塊索引項也變成了 `11個直接索引` + `1個一級索引` + `1個二級索引`,共計可以索引 11 + 256 + 256 *256 = 65803個盤塊,由此實現了`最大檔案大小`的擴充套件。一個`二級索引塊`的例子如下: ![](https://img2020.cnblogs.com/blog/2028256/202102/2028256-20210202134610431-476805216.png) 我們首先要修改一下巨集 NINDIRECT,將其值改為11,並修改`struct dinode`和`struct inode`中addr的定義部分,以及有關檔案大小的巨集: // kernel/fs.h #define NDIRECT 11 #define NINDIRECT (BSIZE / sizeof(uint)) #define NDINDIRECT NINDIRECT * NINDIRECT #define MAXFILE (NDIRECT + NINDIRECT + NDINDIRECT) 還有一些巨集(NBLOCKS等)需要修改,在相應的實驗手冊中已經指出,因此不再贅述了。 隨後需要修改兩個api:`bmap`和`itrunc`。在`bmap`中新增計算二級索引的相關程式碼,以及對應的盤塊的分配程式碼: static uint bmap(struct inode *ip, uint bn) { uint addr, *a; struct buf *bp; // offset in direct-block range if(bn < NDIRECT){ if((addr = ip->addrs[bn]) == 0) ip->addrs[bn] = addr = balloc(ip->dev); return addr; } bn -= NDIRECT; // offset in primary-index range if(bn < NINDIRECT){ // Load indirect block, allocating if necessary. if((addr = ip->addrs[NDIRECT]) == 0) ip->addrs[NDIRECT] = addr = balloc(ip->dev); // allocate a block on disk bp = bread(ip->dev, addr); a = (uint*)bp->data; if((addr = a[bn]) == 0){ a[bn] = addr = balloc(ip->dev); log_write(bp); } brelse(bp); return addr; } bn -= NINDIRECT; struct buf *dindbuf, *indbuf; // double-indirect block buffer, indirect block buffer // offset in secondary-index range if (bn < NDINDIRECT) { // get the DIRECT index block. if it's not exist, allocate it. if ((addr = ip->addrs[NDIRECT + 1]) == 0) ip->addrs[NDIRECT + 1] = addr = balloc(ip->dev); dindbuf = bread(ip->dev, addr); // map it into buffer uint *dind = (uint *)dindbuf->data; if(dind[bn / NINDIRECT] == 0) { // allocate a indirect block for double-indirect index and log it. dind[bn / NINDIRECT] = balloc(ip->dev); log_write(dindbuf); } indbuf = bread(ip->dev, dind[bn / NINDIRECT]); uint *ind = (uint *)indbuf->data; if (ind[bn % NINDIRECT] == 0) { // allocate file block if it's not exist. ind[bn % NINDIRECT] = balloc(ip->dev); log_write(indbuf); } brelse(dindbuf); brelse(indbuf); return ind[bn % NINDIRECT]; } // out of range panic("bmap: out of range"); } 此外,還要修改`itrunc`這個方法,新增從二級索引中釋放盤塊的支援程式碼: static void itrunc(struct inode *ip) { int i, j; struct buf *bp; uint *a; // release DIRECT block for(i = 0; i < NDIRECT; i++){ if(ip->addrs[i]){ bfree(ip->dev, ip->addrs[i]); ip->addrs[i] = 0; } } // release primary-index block if(ip->addrs[NDIRECT]){ bp = bread(ip->dev, ip->addrs[NDIRECT]); a = (uint*)bp->data; for(j = 0; j < NINDIRECT; j++){ if(a[j]) bfree(ip->dev, a[j]); } brelse(bp); bfree(ip->dev, ip->addrs[NDIRECT]); ip->addrs[NDIRECT] = 0; } // release secondary-index block if (ip->addrs[NDIRECT + 1]) { struct buf *dinddbuf, *indbuf; uint *dind, *ind; dinddbuf = bread(ip->dev, ip->addrs[NDIRECT + 1]); dind = (uint *)dinddbuf->data; for (int k = 0; k < NINDIRECT; k++) { if (dind[k]) { indbuf = bread(ip->dev, dind[k]); ind = (uint *)indbuf->data; for (int l = 0; l < NINDIRECT; l++) { bfree(ip->dev, ind[l]); ind[l] = 0; } brelse(indbuf); bfree(ip->dev, dind[k]); } } brelse(dinddbuf); bfree(ip->dev, ip->addrs[NINDIRECT + 1]); } ip->size = 0; iupdate(ip); } 這段程式碼裡有兩個值得注意的地方: 1)一級索引塊、二級索引塊剛開始並沒有被分配,只有在需要使用的時候才會被分配 2)在xv6中使用了`緩衝區`來減少I/O次數,一切對盤塊的讀寫操作都首先在`緩衝塊`上進行,並且為了維持磁碟中元資料(super block)的一致性,一切寫盤塊的操作都需要呼叫`log_write`將寫操作新增到**日誌**中。在`bmap`中涉及到了兩種對盤塊的寫操作:對二級索引塊的寫操作(向其中新增表項,一個表項對應一個一級索引塊)和對一級索引塊的寫操作(向其中新增表項,一個表項對應一個檔案內容盤塊)。雖然bmap也會分配指向檔案內容的盤塊,但對這個盤塊的寫操作並不是在`bmap`中進行的,而是在其他api中進行的,因此無需將這個塊錄入到日誌中。 雖然`struct dinode`和`struct inode`中也包含了`type`、`major`、`nlink`、`size`等成員,但讀過相關的程式碼後你會發現,這些成員都無需修改。這也減小了我們不少的工作量。 Part1的相關程式碼也就這麼多了,只要認真讀下來file system相關的程式碼,並認真按照Hint來做,這就算是個白送的實驗。不過比較要命的是usertests中的`writebig`測試,它會寫一個MAXFILE大小的檔案(新增大檔案支援後,這個值從原來的2K變成了20W),會花費更長的測試時間。 xv6 kernel is booting virtio disk init 0 init: starting sh $ bigfile .................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................. wrote 65803 blocks bigfile done; ok Part2 symlink ========= 這部分實驗要求我們為檔案系統增添對`符號連結(symbolic link)`(也被稱為`軟連結`)的支援。為此我們首先簡要了解一下xv6中的硬連結和軟連結的含義。 在xv6中,每個檔案擁有唯一的`struct dinode`和`struct inode`,更好的一種理解方法是,`struct inode`是一個`檔案控制塊`,儲存著這個檔案相關的`元資料`,一切對檔案的讀、寫、開啟、關閉、刪除等操作都需要通過`inode`來完成。而很多時候,我們需要**一個檔案可以出現在多個目錄下**的支援(例如說多使用者作業系統,每個使用者都擁有一個自己的目錄,它們希望共享某一檔案)。如下圖所示,這些檔案的路徑(連結)都導向了同一個`inode`: ![](https://img2020.cnblogs.com/blog/2028256/202102/2028256-20210202135226388-163291168.png) 這種可以**直接索引到inode**的連結即是**硬連結**,本質上它是目錄檔案中的一個條目,根據這個條目,可以(準確的說是必定)獲得該檔案的inode。在xv6的api中,通過`sys_link`建立一個檔案的硬連結,其中的核心api是 `dirlink`: // Write a new directory entry (name, inum) into the directory dp. int dirlink(struct inode *dp, char *name, uint inum) { int off; struct dirent de; struct inode *ip; // Check that name is not present. if((ip = dirlookup(dp, name, 0)) != 0){ iput(ip); return -1; } // Look for an empty dirent. for(off = 0; off < dp->size; off += sizeof(de)){ if(readi(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de)) panic("dirlink read"); if(de.inum == 0) break; } strncpy(de.name, name, DIRSIZ); de.inum = inum; if(writei(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de)) panic("dirlink"); return 0; } 在`sys_link`中呼叫`dirlink`時,傳入的引數`inum`為目標檔案的`inode`編號,該編號被新增到目錄表項中;這樣我們也可以理解為什麼呼叫`ll`時,會發現同一個檔案的硬連結對應的inode號是相同的了;**硬連結如果存在,那麼其對應的`inode`必須存在**。`inode`通過維護一個`nlink`記錄著硬連結的數量。當該`inode`新增一個硬連結時,其值便增1;當某個路徑下的硬連結被刪除時,`nlink`要減1,當值減至0時,說明該檔案已經無法通過路徑訪問到,這時才能釋放相應的檔案。 **軟連結**的定義則不同,我們可以看一下`symlink`的man page中的部分內容: > symlink() creates a symbolic link named linkpath which **contains the string target**. > Symbolic links are **interpreted at run time** as if the contents of the link had been substituted into the path being followed to find a file or directory. > A symbolic link (also known as a soft link) may point to an existing file **or to a nonexistent one**; the latter case is known as a dangling link. 軟連結並不會增加`nlink`的數量,且可以指向一個不存在的路徑。軟連結也可以指向一個軟連結,遇到這種情況時將會遞迴,直至找到相應的硬連結(或者達到最大遞迴深度後返回錯誤)。 在本Part中,要求我們實現以下內容: 1)實現一個新的系統呼叫 sys_symlink,通過該系統呼叫可以建立一個軟連結; 2)為sys_open提供軟連結的開啟支援(通過軟連結開啟檔案、O_NOFOLLOW); 只要理解了軟連結的作用後,實現這個lab的思路也比較容易了,這個Part實現的思路也很多,我使用的是最簡單最容易想到的: 1)將軟連結本身也看做是一個**檔案**,即軟連結本身也有自己的`dinode`和`inode`,其檔案內容也只是一行字串,表徵著軟連結指向的檔案的路徑。建立軟連結時,要為其分配一個`inode` 2)將軟連結的`inode`新增到目錄條目中,注意要避免命名衝突 uint64 sys_symlink(void) { char target[MAXPATH + 1], path[MAXPATH + 1], name[MAXPATH + 1]; memset((void *)target, 0, MAXPATH + 1); memset((void *)path, 0, MAXPATH + 1); if (argstr(0, target, MAXPATH) < 0 || argstr(1, path, MAXPATH) < 0) { return -1; } begin_op(ROOTDEV); // get parent inode struct inode *iparent, *isym; if ((iparent = nameiparent((char *)path, (char *)name)) == 0) { end_op(ROOTDEV); return -1; } // avoid name conflict // do not hold ilock for iparent uint off; if ((isym = dirlookup(iparent, name, &off)) != 0) { // printf("symlink name conflict with an existing file\n"); // iunlockput(iparent); iput(iparent); end_op(ROOTDEV); return -1; } // allocate a dinode for symlink. isym is locked when it func create return // after this operation, symlink entry is added under corresponding path if ((isym = create(path, T_SYMLINK, 0, 0)) == 0) { panic("create inode for symlink error"); // panic is not suitable, but simplify our situations. } // fill symlink file content with targetpath int retval = 0; uint pathlen = strlen((char *)target); uint r, total; r = total = 0; while (total != pathlen) { if ((r = writei(isym, 0, (uint64)(target + total), total, pathlen - total)) > 0) { total += r; } else { retval = -1; break; } } // release iunlockput(isym); iput(iparent); end_op(ROOTDEV); return retval; } 這樣我們也可以理解為什麼在一些檔案系統上,同一個檔案的軟連結和硬連結的`inode`可能不同,因為**軟連結本身也是一個檔案,有自己的inode**,而**硬連結只是一個目錄中的條目,該條目索引到了對應的inode**。 然後修改`sys_open`,當發現開啟的path對應的是一個軟連結時,呼叫`divesymlink`,獲得該軟連結指向的檔案的`inode`。如果使用了O_NOFOLLOW,說明我們希望開啟的是**軟連結這個檔案本身**,一般fstate會使用這個flag: uint64 sys_open(void) { char path[MAXPATH]; int fd, omode; struct file *f; struct inode *ip; int n; if((n = argstr(0, path, MAXPATH)) < 0 || argint(1, &omode) < 0) return -1; begin_op(ROOTDEV); if(omode & O_CREATE){ ip = create(path, T_FILE, 0, 0); if(ip == 0){ end_op(ROOTDEV); return -1; } } else { if((ip = namei(path)) == 0){ end_op(ROOTDEV); return -1; } ilock(ip); if(ip->type == T_DIR && omode != O_RDONLY){ iunlockput(ip); end_op(ROOTDEV); return -1; } } /************************** * SYMLINK * *************************/ if (ip->type == T_SYMLINK && (omode & O_NOFOLLOW) == 0) { ip = divesymlink(ip); if (ip == 0) { // link target not exist end_op(ROOTDEV); return -1; } } if(ip->type == T_DEVICE && (ip->major < 0 || ip->major >= NDEV)){ iunlockput(ip); end_op(ROOTDEV); return -1; } if((f = filealloc()) == 0 || (fd = fdalloc(f)) < 0){ if(f) fileclose(f); iunlockput(ip); end_op(ROOTDEV); return -1; } if(ip->type == T_DEVICE){ f->type = FD_DEVICE; f->major = ip->major; f->minor = ip->minor; } else { f->type = FD_INODE; } f->ip = ip; // ref is here, so this function doesn't call iput f->off = 0; f->readable = !(omode & O_WRONLY); f->writable = (omode & O_WRONLY) || (omode & O_RDWR); iunlock(ip); end_op(ROOTDEV); return fd; } `divesymlink`的實現如下,其的引數為軟連結檔案的`inode`,返回軟連結最終指向的檔案的`inode`,如果這個檔案不存在則返回0。根據Hint,`divesymlink`還要解決軟連結之間的**環路引用問題**,這裡我們簡單的通過遞迴深度來判斷,當深度大於10時,認為軟連結最終指向的檔案不存在: static struct inode * divesymlink(struct inode *isym) { struct inode *ip = isym; char path[MAXPATH + 1]; uint len; int deep = 0; do { // get linked target // we don't know how long the file str is, so we expect once read could get fullpath. len = readi(ip, 0, (uint64)path, 0, MAXPATH); if (readi(ip, 0, (uint64)(path + len), len, MAXPATH + 1) != 0) panic("divesymlink : short read"); iunlockput(ip); if (++deep > 10) { // may cycle link return 0; } if ((ip = namei((char *)path)) == 0) { // link target not exist return 0; } ilock(ip); } while (ip->type == T_SYMLINK); return ip; } 這樣Part2也解決了。比較棘手的是api的鎖獲取和鎖釋放,以及`iput`、`iunlockput`、`iunlock`這三個api的選擇。我在這裡簡單介紹一下: 1)`iput`會使`ip->ref`的數量減1(注意與`ip->nlink`區分開)。這個值記錄著外部所持有`struct inode`指標的總數量。當這個值減小到0時,說明**沒有程序在訪問這個檔案**,此時要將`icache`中對應的`struct inode`釋放掉,但不會釋放對應的檔案; 2)`iunlock`會釋放掉`struct inode`的鎖,這把鎖用來實現併發環境下的`struct inode`的原子訪問,即我們在檢視`struct inode`中的成員(type、data等)時,必須持有這把鎖; 3)`iunlockput`同時呼叫上述兩個函式; 即如果我們不需要訪問`struct inode`的成員時,應呼叫`iunlock`釋放掉`inode`的鎖,等需要訪問時再拿起這把鎖;當我們不再需要使用`struct inode`的指標時,要呼叫`iput`,棄用掉這個指標; 這樣symlinktest也可以Pass了,對應的usertests也可以全pass: xv6 kernel is booting virtio disk init 0 init: starting sh $ symlinktest Start: test symlinks test symlinks: ok Start: test concurrent symlinks test concurrent symlinks: ok $ usertests usertests starting test reparent2: OK test pgbug: OK test sbrkbugs: usertrap(): unexpected scause 0x000000000000000c pid=3210 sepc=0x00000000000044b2 stval=0x00000000000044b2 usertrap(): unexpected scause 0x000000000000000c pid=3211 sepc=0x00000000000044b2 stval=0x00000000000044b2 OK test badarg: OK test reparent: OK test twochildren: OK test forkfork: OK test forkforkfork: OK test argptest: OK ...... ...... ...... test opentest: OK test writetest: OK test writebig: OK test createtest: OK test openiput: OK test exitiput: OK test iput: OK test mem: OK test pipe1: OK test preempt: kill... wait... OK test exitwait: OK test rmdot: OK test fourteen: OK test bigfile: OK test dirfile: OK test iref: OK test forktest: OK test bigdir: OK ALL TESTS PASSED 後記 ========== 這個Lab不算難,絕大多數時間要花費在讀程式碼上。我這篇xv6 file system的學習筆記可能會比較幫助: TODO: (在碼了在碼了,但願今天能