MIT 6.828 操作系统课程系列9 File System#

lab0中已学习过一些文件系统内容。之前的lab中也经常会遇到文件系统相关逻辑。这次lab详细看文件系统。
看手册第8章。文件系统的作用就是存储和管理数据。通常文件可供多个用户和程序共享。
xv6支持类unix的文件、目录、路径,把数据存在虚拟磁盘上。

做文件系统需要面临一些挑战

  1. 需要一个树型结构来描述文件和目录。记录数据的归属、每个文件的内容、磁盘占用情况。

  2. 需要支持崩溃后的恢复。比如执行某些事务时突然断电,重启后必须还能正常工作。

  3. 多个进程可能会同时操作同个文件。

  4. 访问磁盘比访问内存慢了几个数量级,必须有cache机制。

概览#

xv6文件系统分为七层。

功能

file descriptor

unix的很多资源都抽象成fd(管道、设备、文件等等)

pathname

路径支持

directory

特殊的inode。内容是一批目录或文件。

inode

独立的文件,每个文件表述为一个incode,有唯一的编号,有一个或多个block保存文件内容。

logging

可让上层把几个更新block的操作打包成事务,这样这些操作就是原子的,可解决崩溃时数据不一致的问题。

buffer cache

缓存disk block。保证一个block在同一时刻最多只有一个kernel进程在对其操作。

disk

读写虚拟硬盘上的block

文件系统必须规划好inode和block怎么存在磁盘上。xv6把磁盘分成几个部分。

// xv6文件系统布局

//            | boot | superblock |  log   |    inodes    |   bitmap  |         data       |

// block编号      0        1         2 ...        ...           ...         ... FSSIZE-1

// 数量           1        1         nlog     ninodeblocks    nbitmap          nblocks

block 0(保存boot信息)是不用的。
block 1是superblock。保存文件系统的整体信息:总block数量,inode数量,log里的block数量。
block 2存log。
接着是inodes,每个block存数个inode。
bitmap保存block的使用状态。
实际的数据block。每个block要么是空的(bitmap里标注为空闲),要么存了文件或目录的内容。
superblock存文件系统总体信息。


Buffer cache层#

两个功能

  1. 对block做并发管理,保证一个block只有一个副本在内存里,而且同一时刻最多只能有一个kernel线程在使用这个副本。

  2. 缓存常用的block。这样不用每次读磁盘。

buffer cache提供breadbwrite接口。分别时读写block。
一个kernel线程使用完buffer后必须用brelse进行释放。
buffer cache用对每个buffer都做一个sleep锁来保证同一时刻只有一个线程在使用同一个buffer。
bread会返回一个已锁定的buffer。brelse释放锁。

buffer cache用固定数量的buffer来保存磁盘block。当文件系统请求一个不存在于cache的block时,可能发生回收。
会回收最久没使用的block来给新的block用(假设越久没用意味着再次使用的概率越小)。


#define BSIZE 1024  // block size

#define MAXOPBLOCKS  10  // max # of blocks any FS op writes
#define NBUF         (MAXOPBLOCKS*3)  // size of disk block cache


// 一个block的buffer
struct buf {
    int valid;   // has data been read from disk?
    int disk;    // does disk "own" buf?
    uint dev;
    uint blockno;           // block号
    struct sleeplock lock;  // 每个buf都有自己的锁
    uint refcnt;
    struct buf *prev; // LRU cache list
    struct buf *next;
    uchar data[BSIZE];      // 实际数据
};


// 全局唯一bcache。包含30个buf。
struct {
    struct spinlock lock;
    struct buf buf[NBUF]; // 30个buf

    // Linked list of all buffers, through prev/next.
    // Sorted by how recently the buffer was used.
    // head.next is most recent, head.prev is least.
    struct buf head;
} bcache;


// 初始化
binit(void)
{
    struct buf *b;

    initlock(&bcache.lock, "bcache");

    // Create linked list of buffers
    bcache.head.prev = &bcache.head;
    bcache.head.next = &bcache.head;
    for(b = bcache.buf; b < bcache.buf+NBUF; b++){
        // head永远不变
        // 每次插到head之后
        b->next = bcache.head.next;
        b->prev = &bcache.head;
        initsleeplock(&b->lock, "buffer");
        bcache.head.next->prev = b;
        bcache.head.next = b;
    }
}


// 返回一个cahce资源(buf)
static struct buf*
bget(uint dev, uint blockno)
{
    struct buf *b;

    acquire(&bcache.lock);

    // 遍历所有buf
    for(b = bcache.head.next; b != &bcache.head; b = b->next){

        // 如果dev和blockno匹配,已经在cache中,带锁返回。
        if(b->dev == dev && b->blockno == blockno){
            b->refcnt++;
            release(&bcache.lock);
            acquiresleep(&b->lock);
            return b;
        }
    }

    // Not cached.
    // Recycle the least recently used (LRU) unused buffer.

    // 没在cache。从buf末尾往前找refcnt为0的。
    for(b = bcache.head.prev; b != &bcache.head; b = b->prev){
        if(b->refcnt == 0) {
            b->dev = dev;
            b->blockno = blockno;
            b->valid = 0;
            b->refcnt = 1;
            release(&bcache.lock);
            acquiresleep(&b->lock);
            return b;
        }
    }
    panic("bget: no buffers");
}


// 读一个磁盘block。返回其buf
struct buf*
bread(uint dev, uint blockno)
{
    struct buf *b;

    // 获取一个buf
    b = bget(dev, blockno);
    if(!b->valid) {
        virtio_disk_rw(b, 0); // 进驱动层读实际磁盘数据到buf。细节暂时忽略。
        b->valid = 1;
    }
    return b;
}


// buf数据写到磁盘
bwrite(struct buf *b)
{
    // 必须为锁定状态
    if(!holdingsleep(&b->lock))
        panic("bwrite");

    virtio_disk_rw(b, 1); // 进驱动实际写磁盘
}


// 释放一个buf
brelse(struct buf *b)
{
    if(!holdingsleep(&b->lock))
        panic("brelse");

    releasesleep(&b->lock);

    acquire(&bcache.lock);

    // 减引用
    b->refcnt--;
    if (b->refcnt == 0) {
        // no one is waiting for it.

        // 引用减为0。挪到head之后。
        b->next->prev = b->prev;
        b->prev->next = b->next;
        b->next = bcache.head.next;
        b->prev = &bcache.head;
        bcache.head.next->prev = b;
        bcache.head.next = b;
    }
  
    release(&bcache.lock);
}

Inode层#

inode可以指磁盘上的文件数据(包括文件大小,数据block列表),也可以指内存中的inode,包含磁盘inode的副本和一些其他信息。
磁盘inode是磁盘上一串连续的数据(inode block)。所有inode大小一样,所以给定一个n,很容易能取得磁盘上的第n个inode。
这个n被成为inode number或i-number。
inode可以看成一个无名的文件。

磁盘inode的结构在lab0学习过,再看一遍。
mkfs的一些函数与fs重名但实现不同。mkfs是独立生成文件系统,不用考虑系统运行时的各种问题。最终的格式一样。

type分为文件、目录、特殊文件(设备)。type为0代表inode为未使用状态。
nlink为引用此inode的目录计数。
size为此文件的字节数。
addrs为文件包含的block编号

使用中状态的inode放在内存里。
处于内存中意味着有指针指向这个inode。ref就是这个计数,如果变为0,就会从内存中去除。
igetiput获取和释放inode指针,加减引用计数。


#define NDIRECT 12
#define NINDIRECT (BSIZE / sizeof(uint))
#define MAXFILE (NDIRECT + NINDIRECT)

// 内存inode
// 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];
};

// 磁盘inode
// On-disk inode structure
struct dinode {
  short type;           // File type
  short major;          // Major device number (T_DEVICE only)
  short minor;          // Minor device number (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
};

#define NINODE 50

// 全局inode表。内存中最多放50个inode。
struct {
  struct spinlock lock;
  struct inode inode[NINODE];
} itable;

// inode type
#define T_DIR     1   // Directory
#define T_FILE    2   // File
#define T_DEVICE  3   // Device


// cwd初始化
// 首先在mkfs中已经做好了根目录,并把user程序添加到了根目录。见lab0。
// 系统启动时把cwd初始化为根目录,我们命令行ls就能看到对应的文件了。
#define ROOTDEV 1
#define ROOTINO 1

userinit
    // 获取根目录的inode
    p->cwd = namei("/");
        char name[DIRSIZ];
        return namex(path, 0, name); // namex(char *path, int nameiparent, char *name)

            // iget以dev和inum获取一个内存inode资源
            // 这里是根目录
            ip = iget(ROOTDEV, ROOTINO); // iget(uint dev, uint inum)
                struct inode *ip, *empty;
                acquire(&itable.lock);

                // Is the inode already in the table?
                empty = 0;

                // 遍历inode表。如果找到dev/inum相等的,ref++,直接返回。
                for(ip = &itable.inode[0]; ip < &itable.inode[NINODE]; ip++){
                    if(ip->ref > 0 && ip->dev == dev && ip->inum == inum){
                        ip->ref++;
                        release(&itable.lock);
                        return ip;
                    }
                    if(empty == 0 && ip->ref == 0)    // Remember empty slot.
                        empty = ip;
                }

                // 如果没找到,而且没有空闲的inode资源。panic
                if(empty == 0)
                    panic("iget: no inodes");

                // 否则占据一个inode资源
                ip = empty;
                ip->dev = dev;
                ip->inum = inum;
                ip->ref = 1;
                ip->valid = 0;
                release(&itable.lock);

                return ip;

            return ip;


// 创建目录的流程
mkdir
    sys_mkdir // syscall
        char path[MAXPATH];
        struct inode *ip;

        begin_op(); // log相关。暂忽略

        argstr(0, path, MAXPATH); // 取path

        // 创建目录
        // 比如cwd为/。可创建mkdir a。然后可mkdir a/b。
        create(path, T_DIR, 0, 0) // create(char *path, short type, short major, short minor)

            struct inode *ip, *dp;
            char name[DIRSIZ];

            // 获取目标目录的inode。比如/a/b/c就会得到/a/b的inode
            // 会检查是否存在,一级一级检查,有问题会返回0。
            if((dp = nameiparent(path, name)) == 0)
                namex(path, 1, name); // namex(char *path, int nameiparent, char *name)

                    // 先获取当前目录的inode
                    if(*path == '/')
                        ip = iget(ROOTDEV, ROOTINO);
                    else
                        ip = idup(myproc()->cwd);
                            ip->ref++;

                    // 从当前目录cwd开始

                    // name是一个临时buffer
                    // 不断从path中取第一项到name。path更新为剩余的路径项。
                    // 如果返回0,就是路径已经空了,取不出了,跳出。
                    while((path = skipelem(path, name)) != 0){
                        ilock(ip);
                            // 锁定inode。如果不包含实际磁盘数据,读出数据。
                            struct buf *bp;
                            struct dinode *dip;

                            if(ip == 0 || ip->ref < 1)
                                panic("ilock");

                            acquiresleep(&ip->lock);

                            // 如果没读过数据
                            if(ip->valid == 0){
                                // 磁盘实际数据读到buf
                                bp = bread(ip->dev, IBLOCK(ip->inum, sb));
                                dip = (struct dinode*)bp->data + ip->inum%IPB;
                                ip->type = dip->type;
                                ip->major = dip->major;
                                ip->minor = dip->minor;
                                ip->nlink = dip->nlink;
                                ip->size = dip->size;

                                // 把dinode的block号复制到inode
                                memmove(ip->addrs, dip->addrs, sizeof(ip->addrs));
                                brelse(bp);
                                ip->valid = 1;
                                if(ip->type == 0)
                                    panic("ilock: no type");
                            }

                        // 每次得到的inode必须是目录,否则就是有问题。
                        if(ip->type != T_DIR){
                            iunlockput(ip);
                            return 0;
                        }

                        // 结束
                        if(nameiparent && *path == '\0'){
                            // Stop one level early.
                            iunlock(ip);
                            return ip;
                        }

                        // 在当前inode中找名为name的目录
                        // 遍历目录中的dirent
                        // 不展开了
                        // 如果找不到就是有问题
                        if((next = dirlookup(ip, name, 0)) == 0){
                            iunlockput(ip);
                            return 0;
                        }
                        iunlockput(ip);

                        // 往下一层目录走
                        ip = next;
                    }

                    if(nameiparent){
                        iput(ip);
                        return 0;
                    }

                    return ip;

                return 0;

            // 锁定。并保证读出磁盘数据
            ilock(dp);

            // 如果name已存在,返回已存在的。
            if((ip = dirlookup(dp, name, 0)) != 0){
                iunlockput(dp);
                ilock(ip);
                if(type == T_FILE && (ip->type == T_FILE || ip->type == T_DEVICE))
                    return ip;
                iunlockput(ip);
                return 0;
            }

            // 分配新的inode
            if((ip = ialloc(dp->dev, type)) == 0){
                ialloc(uint dev, short type)
                    int inum;
                    struct buf *bp;
                    struct dinode *dip;

                    for(inum = 1; inum < sb.ninodes; inum++){
                        bp = bread(dev, IBLOCK(inum, sb));
                        dip = (struct dinode*)bp->data + inum%IPB;

                        // type0为空闲状态
                        if(dip->type == 0){  // a free inode
                            memset(dip, 0, sizeof(*dip));
                            dip->type = type; // 更新了bcache
                            log_write(bp);   // log相关。暂忽略
                            brelse(bp);

                            // 成功。
                            // 再iget得到内存inode
                            return iget(dev, inum);
                        }
                        brelse(bp);
                    }

                    // 失败
                    printf("ialloc: no inodes\n");
                    return 0;

                // 失败
                iunlockput(dp);
                return 0;
            }

            // 锁定。确保读出磁盘数据
            ilock(ip);
            ip->major = major;
            ip->minor = minor;
            ip->nlink = 1;

            // ip为内存inode,包含实时内存数据。
            // 获取对应的dinode。把数据复制到dinode,即cache。
            // 写到cache是预更新。最后commit后才是真正写到磁盘。
            iupdate(ip);
                struct buf *bp;
                struct dinode *dip;

                // 磁盘数据读到cache
                bp = bread(ip->dev, IBLOCK(ip->inum, sb));

                // 更新cache
                dip = (struct dinode*)bp->data + ip->inum%IPB;
                dip->type = ip->type;
                dip->major = ip->major;
                dip->minor = ip->minor;
                dip->nlink = ip->nlink;
                dip->size = ip->size;
                memmove(dip->addrs, ip->addrs, sizeof(ip->addrs));
                log_write(bp);  // log相关。暂忽略
                brelse(bp);

            // dirlink在目录的inode中添加dirent项

            if(type == T_DIR){  // Create . and .. entries.
                // No ip->nlink++ for ".": avoid cyclic ref count.
                if(dirlink(ip, ".", ip->inum) < 0 || dirlink(ip, "..", dp->inum) < 0)
                    goto fail;
            }

            if(dirlink(dp, name, ip->inum) < 0)
                // 
                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))
                        return -1;

                    return 0;

                goto fail;

            if(type == T_DIR){
                // now that success is guaranteed:
                dp->nlink++;  // for ".."
                iupdate(dp);
            }

            iunlockput(dp);
                iunlock(ip);
                    if(ip == 0 || !holdingsleep(&ip->lock) || ip->ref < 1)
                        panic("iunlock");
                    releasesleep(&ip->lock);

                iput(ip);

            return ip;

            fail:
                // something went wrong. de-allocate ip.
                ip->nlink = 0;
                iupdate(ip);
                iunlockput(ip);
                iunlockput(dp);
                return 0;


        // create()结束
        iunlockput(ip);
        end_op();  // log相关,暂忽略。会进行commit,真正执行文件操作。
        return 0;


// 写文件流程

#define MAXOPBLOCKS  10  // max # of blocks any FS op writes

write // user
    sys_write // syscall
        filewrite(struct file *f, uint64 addr, int n)
            // 从addr读n字节写到f

            if(f->type == FD_INODE) // inode
                int max = ((MAXOPBLOCKS-1-1-2) / 2) * BSIZE;  // 一次写最大字节数=3k
                int i = 0;          // 已写字节数
                while(i < n){
                    int n1 = n - i; // 剩余字节数
                    if(n1 > max)    // 每次最多写max
                        n1 = max;

                    begin_op();
                    ilock(f->ip);

                    if ((r = writei(f->ip, 1, addr + i, f->off, n1)) > 0)

                        // 从addr的i偏移处,读n1字节写到inode的off偏移处。
                        writei(struct inode *ip, int user_src, uint64 src, uint off, uint n)
                            uint tot, m;
                            struct buf *bp;

                            if(off > ip->size || off + n < off)
                                return -1;

                            if(off + n > MAXFILE*BSIZE)
                                return -1;

                            for(tot=0; tot<n; tot+=m, off+=m, src+=m){
                                uint addr = bmap(ip, off/BSIZE); // bmap(struct inode *ip, uint bn)

                                    // bmap获取自身offset所对应的全局block号
                                    // addr是全局block号

                                    uint addr, *a;
                                    struct buf *bp;

                                    // bn=offset/BSIZE。落在addrs的哪个block

                                    if(bn < NDIRECT){
                                        // 如果落在direct里
                                        if((addr = ip->addrs[bn]) == 0){
                                            // 如果未分配block。分配新的全局block(标记bitmap)。返回block号
                                            addr = balloc(ip->dev);
                                                int b, bi, m;
                                                struct buf *bp;

                                                bp = 0;

                                                // #define BPB (BSIZE*8)
                                                // 一个block可包含8kbit
                                                // 循环。每次前进8k。
                                                // b为block号
                                                for(b = 0; b < sb.size; b += BPB){

                                                    // 得到b的bitmap所在的block buf
                                                    bp = bread(dev, BBLOCK(b, sb));

                                                    // 遍历每个bit。不能超出BPB也不能超出总block数。
                                                    for(bi = 0; bi < BPB && b + bi < sb.size; bi++){
                                                        m = 1 << (bi % 8);
                                                        if((bp->data[bi/8] & m) == 0){  // Is block free?
                                                            bp->data[bi/8] |= m;  // Mark block in use.
                                                            log_write(bp);
                                                            brelse(bp);
                                                            bzero(dev, b + bi);
                                                            return b + bi;
                                                        }
                                                    }
                                                    brelse(bp);
                                                }
                                                printf("balloc: out of blocks\n");
                                                return 0;

                                            if(addr == 0)
                                                return 0;
                                            ip->addrs[bn] = addr;
                                        }
                                        return addr;
                                    }

                                    bn -= NDIRECT;

                                    // 落在indirect里
                                    if(bn < NINDIRECT){
                                        // Load indirect block, allocating if necessary.
                                        // 分配承载indirect的block
                                        if((addr = ip->addrs[NDIRECT]) == 0){
                                            addr = balloc(ip->dev);
                                            if(addr == 0)
                                                return 0;
                                            ip->addrs[NDIRECT] = addr;
                                        }

                                        // 读indirect block本身
                                        bp = bread(ip->dev, addr);
                                        a = (uint*)bp->data;

                                        // 读目标block。如未分配,分配新block。
                                        if((addr = a[bn]) == 0){
                                            addr = balloc(ip->dev);
                                            if(addr){
                                                a[bn] = addr;
                                                log_write(bp);
                                            }
                                        }
                                        brelse(bp);
                                        return addr;
                                    }

                                    panic("bmap: out of range");

                                if(addr == 0)
                                    break;

                                // 到此得到目标block号addr

                                bp = bread(ip->dev, addr);
                                m = min(n - tot, BSIZE - off%BSIZE); // 计算边界

                                // 用户地址复制数据到kernel
                                if(either_copyin(bp->data + (off % BSIZE), user_src, src, m) == -1) {
                                    brelse(bp);
                                    break;
                                }
                                log_write(bp);
                                brelse(bp);
                            }

                            // writei写完

                            if(off > ip->size)
                                ip->size = off;

                            // write the i-node back to disk even if the size didn't change
                            // because the loop above might have called bmap() and added a new
                            // block to ip->addrs[].
                            iupdate(ip);

                            return tot;


                        f->off += r;
                    
                    iunlock(f->ip);
                    end_op();

                    if(r != n1){
                        // error from writei
                        break;
                    }
                    i += r;
                }
                ret = (i == n ? n : -1);



可以看到代码非常难看。

Logging层#

文件系统一个有趣的点是可防灾。当一个操作包含多个写时,如果一部分写之后,系统崩溃,那么此时文件系统的一致性就会出现问题。
比如宕机发生在删除文件期间。可能会造成:

  1. indode包含了一个标记为free的block。

  2. 遗留一个分配了但无主的block。

后者情况稍好,前者在机器重启后可能造成严重问题。
重启后,kernel可能把这个free的block分配给其他文件,造成两个文件包含了同一个block,可相互篡改。

xv6的syscall不会直接写磁盘。它把所有写操作做成一个log,需要执行时写一个commit命令,这时syscall才实际执行这些写操作。完成后把log删除。

如果系统崩溃、重启,在执行任何进程之前,检查log。
如果log标记了commit,执行log里的写操作。
如果没标记commit,直接忽略。
最后把log删除。

Log 设计#

log存在superblock后面的log区域。包含一个header(头block)和后续的数据block(logged blocks)。
header包含一串sector编号,即logged blocks的编号。和logged blocks的计数。
开机时从磁盘读这个计数,如果是0,说明log里没有事务(transaction)。否则说明存在事务。

在可能崩溃的环境下,一系列的写操作必须时原子的。为了让不同的进程并发执行,logging系统能把多个system call的写操作放进一个事务里。
这样一个commit可能涵盖多个system call。为了防止一个system call被分割到多个事务,logging系统只会在没有文件相关的system call处于运行状态时才会commit。

多个事务同时commit称作group commit。这样能减少磁盘操作,因为分摊了固定的commit开销。
还有可能更快速的执行写操作,磁盘只需旋转一次就可完成。
xv6的virtio驱动不支持这种批量操作,但文件系统是支持的。

xv6用磁盘上固定的空间存log。事务里的写block总数b必须在这个空间范围之内。
这样有两个后果:

  1. 任何一个system call的写操作不能超出空间范围。
    这对于多数system call没影响。除了write和unlink,会写大量block。

  2. 一个大文件的写可能会写很多block,和bitmap block,和一个inode。
    unlink一个大文件可能会写多个block和一个inode。
    xv6的system call会把一个大的写操作分成小的写操作,放进log里。unlink不会造成这个问题。

其他后果是logging系统不会允许一个system call开始运行除非能确认它的写操作能放进log的剩余空间。

#define MAXOPBLOCKS  10  // max # of blocks any FS op writes
#define LOGSIZE      (MAXOPBLOCKS*3)  // max data blocks in on-disk log

struct logheader {
    int n;              // log数量
    int block[LOGSIZE]; // 最多30个block号
};


// 全局唯一log
struct log {
    struct spinlock lock;
    int start;
    int size;
    int outstanding; // how many FS sys calls are executing.
    int committing;  // in commit(), please wait.
    int dev;
    struct logheader lh;
};
struct log log;


// fs相关的syscall之前必须调用
// 增加一个outstanding计数,占用一块log资源。
begin_op(void)
{
    acquire(&log.lock);
    while(1){
        if(log.committing){ // commit中。死等。
            sleep(&log, &log.lock);
        } else if(log.lh.n + (log.outstanding+1)*MAXOPBLOCKS > LOGSIZE){ // log空间不够。等待
            // this op might exhaust log space; wait for commit.
            sleep(&log, &log.lock);
        } else {
            log.outstanding += 1; // 正在处理计数+1
            release(&log.lock);
            break;
        }
    }
}

// 在begin_op后进行log
// 主要逻辑就是在log数据中添加buf的block号(不重复添加)。并refcnt++。
log_write(struct buf *b)
{
    int i;

    acquire(&log.lock);
    if (log.lh.n >= LOGSIZE || log.lh.n >= log.size - 1)
        panic("too big a transaction"); // log满
    if (log.outstanding < 1)
        panic("log_write outside of trans"); // 逻辑严重错误

    // 遍历。如果已存在可复用。

    for (i = 0; i < log.lh.n; i++) {
        if (log.lh.block[i] == b->blockno)   // log absorption
            break;
    }
    log.lh.block[i] = b->blockno;

    // 如果不存在。refcnt++
    if (i == log.lh.n) {  // Add new block to log?
        bpin(b);
            acquire(&bcache.lock);
            b->refcnt++;
            release(&bcache.lock);

        log.lh.n++;
    }
    release(&log.lock);
}


// fs操作完毕。进行commit。
end_op(void)
{
    int do_commit = 0;

    acquire(&log.lock);
    log.outstanding -= 1; // 正在处理计数-1

    if(log.committing)
        panic("log.committing"); // 逻辑严重错误
    
    if(log.outstanding == 0){
        do_commit = 1;
        log.committing = 1;
    } else {
        // begin_op() may be waiting for log space,
        // and decrementing log.outstanding has decreased
        // the amount of reserved space.
        wakeup(&log);
    }
    release(&log.lock);

    if(do_commit){
        // call commit w/o holding locks, since not allowed
        // to sleep with locks.
        commit();
        acquire(&log.lock);
        log.committing = 0;
        wakeup(&log);
        release(&log.lock);
    }
}


commit()
{
    if (log.lh.n > 0) {
        write_log();     // Write modified blocks from cache to log
            int tail;

            // 遍历log中的所有block。把这些block的实际数据写到磁盘(log区域)。
            // 也就是把log数据本身固化到磁盘
            // 会空出第一个位置给header
            // 期间崩溃不要紧。因为header还没写到磁盘。直接算作把这次commit丢弃。
            for (tail = 0; tail < log.lh.n; tail++) {
                struct buf *to = bread(log.dev, log.start+tail+1); // log block
                struct buf *from = bread(log.dev, log.lh.block[tail]); // cache block
                memmove(to->data, from->data, BSIZE);
                bwrite(to);  // 驱动磁盘实际写数据
                brelse(from);
                brelse(to);
            }

        // 磁盘log区域第一个block存header
        write_head();    // Write header to disk -- the real commit
            struct buf *buf = bread(log.dev, log.start);             // 拿磁盘log区域的第一个block
            struct logheader *hb = (struct logheader *) (buf->data); // 拿到header
            int i;
            hb->n = log.lh.n;

            // 内存中log的block号写入buf
            for (i = 0; i < log.lh.n; i++) {
                hb->block[i] = log.lh.block[i];
            }

            // 写磁盘
            bwrite(buf);
            brelse(buf);

        // 此时磁盘中已写入完整的log数据

        install_trans(0); // Now install writes to home locations
            int tail;

            // 把磁盘的log数据中所有block写到对应的磁盘block。
            // 期间崩溃不要紧。因为header还没写到磁盘。重启后会再次执行这个log的恢复流程。

            for (tail = 0; tail < log.lh.n; tail++) {
                struct buf *lbuf = bread(log.dev, log.start+tail+1); // read log block
                struct buf *dbuf = bread(log.dev, log.lh.block[tail]); // read dst
                memmove(dbuf->data, lbuf->data, BSIZE);  // copy block to dst
                bwrite(dbuf);  // write dst to disk
                if(recovering == 0)
                    bunpin(dbuf);
                brelse(lbuf);
                brelse(dbuf);
            }


        // 清空log并更新到磁盘。完成整个操作。
        log.lh.n = 0;
        write_head();    // Erase the transaction from the log
    }
}


// 文件系统和log的初始化

#define ROOTDEV       1

struct superblock {
  uint magic;        // Must be FSMAGIC
  uint size;         // Size of file system image (blocks)
  uint nblocks;      // Number of data blocks
  uint ninodes;      // Number of inodes.
  uint nlog;         // Number of log blocks
  uint logstart;     // Block number of first log block
  uint inodestart;   // Block number of first inode block
  uint bmapstart;    // Block number of first free map block
};

struct superblock sb; 

forkret(void)
    static int first = 1;

    if (first) {
        // File system initialization must be run in the context of a
        // regular process (e.g., because it calls sleep), and thus cannot
        // be run from main().
        first = 0;

        // 第一次走到user态时。文件系统初始化。
        fsinit(ROOTDEV);
            // 读superblock
            readsb(dev, &sb);
                struct buf *bp;

                // 1号为superblock
                // 拿到buf,读取数据到sb后释放。
                bp = bread(dev, 1);
                memmove(sb, bp->data, sizeof(*sb));
                brelse(bp);

            if(sb.magic != FSMAGIC)
                panic("invalid file system");
            
            initlog(dev, &sb);
                if (sizeof(struct logheader) >= BSIZE)
                    panic("initlog: too big logheader");

                initlock(&log.lock, "log");

                // 获取log的信息
                log.start = sb->logstart;
                log.size = sb->nlog;
                log.dev = dev;

                // 尝试从log恢复
                recover_from_log();
                    // 读header
                    read_head();
                        struct buf *buf = bread(log.dev, log.start);
                        struct logheader *lh = (struct logheader *) (buf->data);
                        int i;
                        log.lh.n = lh->n;
                        for (i = 0; i < log.lh.n; i++) {
                            log.lh.block[i] = lh->block[i];
                        }
                        brelse(buf);

                    // 把磁盘的log数据中所有block写到对应的磁盘block。
                    install_trans(1); // if committed, copy from log to disk

                    // 清空log并更新到磁盘。
                    log.lh.n = 0;
                    write_head();
    }





看懂代码就更清楚了

  • 核心思想就是把一系列文件操作做成一个原子操作。要么全部完成,要么什么都没发生。不存在中间状态,就达到了目的。
    比如用户编辑一个新文件并保存,保存时断电。要么就成功保存完整的内容。要么就文件根本别存在,啥也没发生。
    千万别出现保存了一部分内容这种中间状态。

  • begin_op,需要争log锁,开启一个transaction。

  • 更改某个buf后用log_write把该buf插入到内存log。
    期间如果系统崩溃,不要紧,此时并没有写磁盘,把整个transaction视为作废即可。

  • 操作完后end_op,commit时把整个内存log存磁盘。
    其中先write_log写具体block数据,可包含多个bwrite。期间如果系统崩溃也不要紧,只是写了磁盘log,还没有真正更新实际的block。把transaction视为作废即可。

  • write_head把log的header写到磁盘。
    这个起到重要的开关作用,它只包含一个bwrite,是原子性的,如果失败,即transaction作废。因为下次从log恢复时会依赖header的值,如果这次写失败,下次再读就会发现header里的log数为0,即逻辑上不存在log。
    如果成功,磁盘里就包含了完整有效的log信息,磁盘中log.lh.n变为buf数,而不是0。重启后就要根据log数据来恢复。

  • 最后把数据实际写到磁盘(从log恢复的流程跟此步骤基本一样,非常巧妙)
    之前已经把本次transaction存到磁盘了,包括block数据和header。这时把磁盘log数据全读出来,写到对应的实际磁盘block。
    再把磁盘log清空。完成整个transaction。
    期间复制block数据可包含多个bwrite,即使写到一半崩溃也是安全的。因为重启后还是能看到完整的磁盘log,可以重新执行这次transaction。
    写完block数据后,最后同样是一个开关(清空磁盘log),原子操作,标志整个transaction是否成功。

先写实际的大量数据,最后原子操作写一个开关,可保证数据的完整性。不会出现开关打开了但实际数据不对。

File descriptor层#

fd层按”一切皆是文件”的思想实现了把多数资源都表示成一个文件,用一个fd表示。
前面的lab已经看过相关代码,比如lab5的创建pipe流程。

做题#

1. Large files#

默认inode的addrs有12个direct block,另有1个indirect block,里面包含256个block号。
所以默认代码一共12+256=268个block。
需要把一个direct block变成两层indirect block,即一共11+256+256*256=65803个block。

看bigfile.c,它每次循环写1k。默认最多只能写268,再写就报错。

  • 修改NDIRECT

  • 修改MAXFILE

  • 修改dinode和inode的addrs长度

  • 修改bmap。实现两层indirect block。

  • 修改itrunc。free两层indirect block。