MIT 6.S081 Lab File System

KatyuMarisa發表於2021-02-02

前言

開啟自己的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];
};

我們只關注其中的addrsaddrs記錄著檔案所在磁碟的盤塊號,這個陣列的容量為13,前12個地址是直接地址,即該檔案的第 0 ~ 11 號盤塊,第13個地址是一個一級索引,用於索引檔案的第 12 ~ 12 + 256 號盤塊,如下圖所示:

這樣,一個檔案最多可以佔用 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個盤塊,由此實現了最大檔案大小的擴充套件。一個二級索引塊的例子如下:

我們首先要修改一下巨集 NINDIRECT,將其值改為11,並修改struct dinodestruct inode中addr的定義部分,以及有關檔案大小的巨集:

// kernel/fs.h
#define NDIRECT 11
#define NINDIRECT (BSIZE / sizeof(uint))
#define NDINDIRECT NINDIRECT * NINDIRECT
#define MAXFILE (NDIRECT + NINDIRECT + NDINDIRECT)

還有一些巨集(NBLOCKS等)需要修改,在相應的實驗手冊中已經指出,因此不再贅述了。

隨後需要修改兩個api:bmapitrunc。在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 dinodestruct inode中也包含了typemajornlinksize等成員,但讀過相關的程式碼後你會發現,這些成員都無需修改,尤其是size這個成員很具有迷惑性,我們分配了盤塊後這個成員看起來應該要增加的,但實際上這是一個沒被用到的成員。畢竟xv6只是一個教學系統,不應對其苛求太多。這部分的分析都在我的關於file system的blog中有詳細的說明。

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 dinodestruct inode,更好的一種理解方法是,struct inode是一個檔案控制塊,儲存著這個檔案相關的後設資料,一切對檔案的讀、寫、開啟、關閉、刪除等操作都需要通過inode來完成。而很多時候,我們需要一個檔案可以出現在多個目錄下的支援(例如說多使用者作業系統,每個使用者都擁有一個自己的目錄,它們希望共享某一檔案)。如下圖所示,這些檔案的路徑(連結)都導向了同一個inode

這種可以直接索引到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)將軟連結本身也看做是一個檔案,即軟連結本身也有自己的dinodeinode,其檔案內容也只是一行字串,表徵著軟連結指向的檔案的路徑。建立軟連結時,要為其分配一個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的鎖獲取和鎖釋放,以及iputiunlockputiunlock這三個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: (在碼了在碼了,但願今天能碼完):

相關文章