不落辰

知不可乎骤得,托遗响于悲风

0%

操作系统-xv6-文件系统(日志)

  • xv6文件系统解析
    • 重点在bcache和log
    • 有待复习

问题及解释

一句话描述:如何通过name找到inode?
等会画一下一个完整的文件系统调用。

一句话描述:日志系统如何防止系统崩溃使得disk处于不一致状态?

ok
日志loggedblock在buf list中这是显然
但是log header除了在struct log header中 在写入disk的过程中还会产生个在buflist中的副本
回答:都是为了写入磁盘嘛,必须途径buffer cache层的辣

目录为什么不能sys_link?
硬链接不支持directory。ubuntu下 hard link not allowed for directory。xv6下的 sys_link 也会检查要增加link的inode是否是directory;若是,则失败。
为什么 ?

ok
涉及的文件有点多啊
最底层的buf:bio.c
日志的实现:log.c
file system 对inode的维护以及日志的使用… :fs.c
上层使用file system的接口:file.c
系统调用:sys_file.c

ok
文件系统通过调用virtio_disk_rw,实现对设备的写操作
ques : virtio_disk_rw如何确定从disk的哪个位置读?通过buf.dev和buf.block no确定

ok
待解决问题:cpu到底在什么时候和磁盘进行交互?而不是和内存缓存?
所有和磁盘交互的动作都必须先进入buffer cache层,然后通过disk驱动程序virtio_disk_rw对磁盘进行读写
其余时候cpu都是在和内存交互

ok
bcache里缓存的都是inode block吗。有data block吗?。当然有。bcacehe可以缓存disk上的任意block。

ok
待解决问题:inode 什么时候应该有ondisk 什么时候应该用memory
bread找到的dinode是On-disk inode structure
iget找到的是in-memory copy of an inode
这俩之前有何区别联系?
有的属性只有mem中的inode有,而disk上的dinode没有。如inode.ref 是说当前内核在内存中有多少C指针引用该inode,仅仅是为了内核正确运行,而这个信息显然不必存在disk上,故dinode没有。

文件系统

功能概述

  • 文件系统背后的机制

    • 对硬件的抽象
    • crash safety
    • 如何在磁盘上排布文件系统
      • 重启计算机时,磁盘上的所有数据(如目录和文件)都能恢复。
      • file system的工作之一就是将所有的数据结构以一种能够在重启之后重新构建file system存放在disk上
    • 性能
      • 尽量避免写磁盘
        • 文件系统所在的硬件设备很慢。如SSD为0.1到1ms完成读写一个disk block;HDD通常是在10ms量级。
  • API角度看文件系统功能

    • open(“/x/y”,–);
      • human readable pathname
    • write(fd , “abc” , 3);
      • implicit offset : write没有传入offset,所以具体写到文件的那个位置是由fs负责维护的。
    • link(“/x/y”,”/x/z”);
      • multiple names : 为同一个文件指定多个名字,因此fs需要负责跟踪指向同一文件的多个文件名。
    • unlink(“x/y”)
    • 除此之外,我还想提一点。文件系统的目的是实现上面描述的API,也即是典型的文件系统API。但是,这并不是唯一构建一个存储系统的方式。如果只是在磁盘上存储数据,你可以想出一个完全不同的API。举个例子,数据库也能持久化的存储数据,但是数据库就提供了一个与文件系统完全不一样的API。所以记住这一点很重要:还存在其他的方式能组织存储系统。我们这节课关注在文件系统,文件系统通常由操作系统提供,而数据库如果没有直接访问磁盘的权限的话,通常是在文件系统之上实现的(注,早期数据库通常直接基于磁盘构建自己的文件系统,因为早期操作系统自带的文件系统在性能上较差,且写入不是同步的,进而导致数据库的ACID不能保证。不过现代操作系统自带的文件系统已经足够好,所以现代的数据库大部分构建在操作系统自带的文件系统之上)。

    • fs如何实现上述功能? (fs的核心数据结构?)

数据结构

  • file system的核心数据结构

  • inode

    • 一个inode代表一个文件。
    • fs通过uint inum识别、引用inode
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      //   inode中的信息完全足够用来实现read/write系统调用,至少可以找到哪个disk block需要用来执行read/write系统调用 
      // in-memory copy of an inode
      struct inode {
      uint dev; // Device number
      uint inum; // Inode number 用于识别inode
      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; // 指向inode的数量
      uint size; // file大小
      uint addrs[NDIRECT+1]; // direct and indirect block number
      };
  • 利用inode的direct number和indirect block number找到block

    • 假设需要读取file的第8000个字节,那么你该读取哪个data block呢?从inode的数据结构中该如何计算呢?
      • blockth: 8000 / block_size(1024) = 7。也即file的第8000个byte是file的第几个data block。之后便可依托类似多级页表的机制找到data block地址
      • bytesth: 8000 % block_size(1024) = 7。得知位于data block的第几个byte
  • file descriptor

    • 与user交互
    • 维护对于文件的offset

层次如下

  • 程序mkfs设置对应于引导扇区、超级块、日志块、inode块和位图块的比特位。
    • Disk layout: [ boot block | sb block | log | inode blocks | free bit map | data blocks ]

以下约定xxx的元数据为
描述xxx的数据,是描述xxx的状态如lock,也包括描述其data的位置、大小等相关信息

以下约定 不被独占 为 当前thread没有对该对象lock上锁

  • file system 的 层次结构
    • disk 磁盘
      • 实际保存数据的存储设备,正是这些设备提供了持久化存储,
    • buffer cache / block cache
      • 全是data block的缓存? 不是的。disk上的block都可以缓存到这里
      • 缓存disk的数据到内存
    • logging
      • crash safety
    • inode cache
      • 为了向单个inode提供同步synchronization。
        • 大家可以同步的、正确的、访问同一个inode,inode cache保证了对inode并发访问的正确性。
    • Directory
    • Pathname
    • Fd
    • 实际上所有的文件系统都有组件对应这里不同的分层

Disk

  • 术语
    • sector
      • 磁盘驱动可以读写的最小单元,它过去通常是512字节
    • block
      • 是操作系统或者文件系统视角的数据。它由文件系统定义,在XV6中它是1024字节。所以XV6中一个block对应两个sector。通常来说一个block对应了一个或者多个sector
    • 有的时候,人们也将磁盘上的sector称为block。所以这里的术语也不是很精确
  • fs 将磁盘看作一个巨大的block数组 : block[0,n-1]

  • 类似于ext2

    • 70 个meta block
    • 199930 个data block
  • 70个meta block

    • block 0
      • boot sector / 没用
    • block 1
      • super block : 描述fs信息,如fs由多少block组成
        • there should be one superblock per disk device, but we run with only one device
        • xv6里面只有一个device(待解决问题:感觉这个所谓的device说得就是disk.)
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          // Disk layout:
          // [ boot block | super block | log | inode blocks |
          // free bit map | data blocks]
          //
          // mkfs computes the super block and builds an initial file system. The
          // super block describes the disk layout:
          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
          };
    • [block2,block32)
      • log block
    • [block32 , block45)
      • inode block
        • 1个inode 64 bytes
        • 1个block 1024bytes
    • [block45 , block70)
      • bitmap block : data block 是否 free
  • 199930 个data block

    • [block46,block1000)
      • data blocks
      • file和目录内容

Buffer Cache

bio.c
Buffer Cache是唯一与磁盘直接交互的模块.
上层通过buffer cache 读写磁盘.

以下约定,将disk上的block 被缓存在 内存中的 buf list上的buf
称为 block cache。亦或者称为 block buf 亦或 block cache buf 亦或称为buf
以block cache和buf为主

xv6中 内存中的所有东西要写入磁盘,都必须先拷贝进buffer cache层的buf 然后再落入磁盘

作用

  • 同步保证正确性同步对磁盘块的访问,以确保磁盘块在内存中只有一个副本,并且一次只有一个内核线程使用该副本
    • Buffer cache每个buf使用一个sleeplock,以确保每个缓冲区(因此也是每个磁盘块)每次只被一个线程使用
  • 加速缓存常用块,以便不需要从慢速磁盘重新读取它们。。

结构

  • cache list
    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct {
    struct spinlock lock; // 保护buf链表
    struct buf buf[NBUF];

    // 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;
  • block cache : struct buf(.data[])
    • 其中buf.data[BSIZE] 就是 disk上 block的cache。也即,buf就是block 的副本缓冲区。
    • buf的其他字段是关于cache的元数据
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      struct buf {
      int valid; // buf是否包含block副本 has data been read from disk?
      int disk; // buf内容是否已经交给磁盘 does disk "own" buf?
      uint dev;
      uint blockno;
      struct sleeplock lock;
      uint refcnt; // 有多少proc在使用
      struct buf *prev; // LRU cache list
      struct buf *next;
      uchar data[BSIZE];
      };
  • 区分
    • kalloc.c : memory allocator 是将dram用一个个大小为4KB的page以freelist形式管理起来。
    • bio.c : 这里管理磁盘缓存是通过将一部分dram用一个个大小为1KB (1024Bytes)的buf以list形式管理起来。每一个buf都是一个block的cache(因为disk上一个block的大小就是1024bytes)
      • 这个list上面的buf是所有的buf,既有free的又有非free的。是否free通过ref_cnt判断。

接口

  • struct buf* bread(uint dev, uint blockno)
    • 上层调用bread以获取一个上锁的block的locked buffer。
    • 上层调用者在内存中独占的读写该buf。
    • 由流程可知,获取的该locked buffer,必然是一个最新的填充了目标block(dev + blockno)的内容的缓存块。
    • 如果调用者修改了buf,那么在释放缓冲区之前必须调用bwrite将更改的数据写入磁盘
    • 流程
        1. bget获取locked buffer
        1. 如果buffer不是block的副本,也即buf.valid = 0。那么通过virtio_disk_rw与磁盘交互,读取出data到buffer中
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          // Return a locked buf with the contents of the indicated block.
          struct buf*
          bread(uint dev, uint blockno)
          {
          struct buf *b;
          // get locked buffer prepared for dev blockno
          b = bget(dev, blockno);
          // 如果buffer里面本身没有dev blockno的数据
          if(!b->valid) {
          // 从disk读入到buf
          virtio_disk_rw(b, 0);
          b->valid = 1;
          }
          return b;
          }
  • void bwrite(struct buf *b)
    • 通过virtio_disk_rw与磁盘交互,将上层在mem中修改过的buf真正的写入磁盘
  • void brelse(struct buf *b)
    • 释放buf的锁,并–引用计数
    • kernel thread结束对buf的使用后,必须通过调用brelse释放buf。
    • 当ref_cnt = 0,将buf移动到list末尾。
  • static struct buf* bget(uint dev, uint blockno)
    • 扫描buf list,查找具有给定设备dev和扇区号block no的缓冲区buf。
      • 如果存在这样的缓冲区,bget增加引用计数,获取缓冲区的sleeplock。然后返回locked的缓冲区。
        1
        2
        3
        4
        5
        6
        7
        8
        for(b = bcache.head.next; b != &bcache.head; b = b->next){
        if(b->dev == dev && b->blockno == blockno){
        b->refcnt++;
        release(&bcache.lock);
        acquiresleep(&b->lock);
        return b;
        }
        }
      • 如果对于dev和blockno,不存在其buf,那么bget就要分配一个buf作为dev的block no的cache。采用lru算法,查找最近的b->ref_cnt = 0的buf。
        • Bget编辑该buf元数据以记录新设备dev和扇区号blockno
        • 并令b->valid = 0,确保了bread将从磁盘读取块数据,而不是错误地使用该buf以前的内容,增加引用计数,并获取其sleeplock。
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          // Not cached.
          // Recycle the least recently used (LRU) unused buffer.
          for(b = bcache.head.prev; b != &bcache.head; b = b->prev){
          if(b->refcnt == 0) {
          b->dev = dev;
          b->blockno = blockno;
          b->valid = 0; // 确保了bread将从磁盘读取块数据,而不是错误地使用该buf以前的内容
          b->refcnt = 1;
          release(&bcache.lock);
          acquiresleep(&b->lock);
          return b;
          }
          }

tip

  • 对于struct bcache 和 buf的修改需要遵守以下几点原则.
  • 1. bcache.spinlock :
    • bcache.lock用于保护有关缓存哪些块的信息
    • 对bcache做任何修改,都必须持有bcache.spinlock
  • 2. buf.sleeplock :
    • buf.sleeplock保护block cache(buf)的读写
    • 对disk上block的cache,即对buf.data[BSIZE]做访问,都必须持有buf.sleeplock。
    • 确保了任何时候只有一个进程可以读写block cache
    • 也就确保了读者看到写操作,写者之间的写操作也不会混乱。
  • 3. 只有在buf.ref_cnt = 0时,才驱逐block cache。
    • 也即在buf.ref_cnt > 0时,block不会被block cache修改。
  • 4. 1个block只能有1个block cache,也即1个block只有1个struct buf
    • 如果buffer cache中有两份block 33的cache将会出现问题。假设一个进程要更新inode19,另一个进程要更新inode20。如果它们都在处理block 33的cache,并且cache有两份,那么第一个进程可能持有一份cache并先将inode19写回到磁盘中,而另一个进程持有另一份cache会将inode20写回到磁盘中,并将inode19的更新覆盖掉。

  • 5.如果调用者修改了缓冲区buf,那么在释放缓冲区之前必须调用bwrite将更改的数据写入磁盘
  • 在bcache.lock临界区域之外获取buf.sleeplock是安全的,且也有其作用
    • 非零b->refcnt用于防止buf被重新用于不同的磁盘块。
    • 如bpin()
  • ps : 在bread(bget)和brelse之间,用户持有其获得的buf.lock,可以独占的对其进行读写

Log 日志

log.c

作用

  • 崩溃会造成磁盘上的文件系统处于不一致的状态。我理解的不一致:一件事情做到一半,正常来讲应该要么做要么没做
    • 例如,假设在文件截断(将文件长度设置为零并释放其内容块)期间发生崩溃。根据磁盘写入的顺序,崩溃可能会
      • 留下对标记为空闲的内容块的引用的inode,
        • 引用已释放块的inode在重新启动后可能会导致严重问题。重新启动后,内核可能会将该块分配给另一个文件,现在我们有两个不同的文件无意中指向同一块。如果xv6支持多个用户,这种情况可能是一个安全问题,因为旧文件的所有者将能够读取和写入新文件中的块,而新文件的所有者是另一个用户。
      • 也可能留下已分配但未引用的内容块。
        • 相对来说是良性的
  • Xv6通过简单的日志记录形式解决了文件系统操作期间的崩溃问题。
    • xv6系统调用不会直接写入磁盘上相应的文件系统数据结构。相反,它会在磁盘上的log(日志)中放置它希望进行的所有磁盘写入的描述
      • log_write: 通过写入logheader记录disk上的哪些block需要被修改,并通过bpin保持其cache不被覆盖。
    • 一旦logheader记录了所有事务的所有的系统调用,它就会向磁盘写入一条特殊的commit(提交)记录,表明日志包含一个完整的操作。
    • 然后,日志系统将写内容复制到磁盘上的文件系统数据结构。
    • 完成这些写入后,日志系统将擦除磁盘上的日志。

关于流程、崩溃及恢复

崩溃后重启,file system如何通过日志恢复磁盘?一言以蔽之:install_trans() : 读取disk 上的logheader,将logged block中的data拷贝到disk上的指定block。

  • 0. 事务还在内存中 并没有提交

    • 也即 所有的被修改的datablock cache 也即struct logheader中对于修改的记录都是在内存中,还没有落入磁盘
    • crashA:完整的丢失写入数据
      • 如果此时发生crash,data全部位于内存中,则重启周recover无法恢复。
  • 1. commit:提交事务(事务被提交前,全部位于内存中)。

    • 这里所谓的事务,就是一堆需要落入磁盘的操作(系统调用)
    • 事务的提交分为,两个阶段
      • 1.1 write_log. 提交了日志系统中所有已完成事务中修改了的目标block的cache,将其提交到disk的logged block中。
      • 1.2 write_head. 提交了日志系统中所有已完成事务的记录,也即本日志中事务中有哪些block需要被修改的记录
    • 完成这12阶段,即disk上已经有了完整的所有已完成事务的记录 以及 修改内容
    • crashC(已完成1.1、1.2):即便此时系统发生崩溃,重启时通过recover_from_log也可以完全恢复disk:将logged block根据logheader拷贝到相应的inode/bitmap/datablock中
    • crashB(已完成1.1):如果只完成了1,但是没有完成2,系统就崩溃了,那么无妨,这种崩溃是良性的,不会导致文件系统/disk处于不一致状态。因为这种崩溃在disk上留下的是已分配但未引用的datablock。也即 logged block被填充了,但是logheader block并没有记录他们这些loggedblock被修改了,也即并没有引用这些logged block。会造成完整的磁盘上的内容缺失。这些已分配但是未引用的logged block稍后会被覆盖
    • crashB:如果在完成1的过程中崩溃,同上
    • 如果在完成2的过程中崩溃,咋整?岂不是真的寄掉了?。
      • 胡咧咧一个解释:由于2过程要写入的header block只有一个块,假定对一个块的写入是原子的。因此,崩溃时要么对这个块没进行一点写入,要么已经写完了这个块。也即对该块的写入是原子的。
  • 2. install_trans : 处理(安装)事务

    • 完成1.1 1.2阶段之后,就是安装事务了
    • 根据logheader block中的记录,将logged block复制到其相应的inode/bitmap/datablock中
    • 处理完之后,日志对于这些事务的记录header block以及内容logged block就没有作用了。
    • 如果2崩溃了,没关系,disk区域上的log块仍然在,recover时可以正常恢复。
  • 3. 清空日志记录

    • 清空headerblock。清除日志系统中对于事务的记录。
    • crashD:此时crash,无妨。recover时重新安装一遍之前安装过的block即可。无影响。
  • 从代码中可以看到

    • xv6允许一次commit多个事务,也就是说允许多个事务并发。(都begin_op,然后都end_op)
    • 但是当在commit时,就不允许再开启新事务了,需要等待commit完成。

编程模型

  • 想要对disk进行操作时,
    • 写begin_op标志事务开始,
    • 然后通过bget拿到block对应的cache,之后对cache进行读写操作。
    • 操作完成之后,通过log_write记入日志,
    • 然后end_op标志事务结束
      1
      2
      3
      4
      5
      6
      7
      begin_op();           //  开启事务
      ...
      bp = bread(...); // 获取目标的block cache
      bp->data[...] = ...; // 对block cache进行读写修改
      log_write(bp); // 日志记录需要修改的block
      ...
      end_op(); // 结束并提交事务
  • 日志的一个示例使用发生在filewrite. 事务如下所示 .
    • 这段代码被包装在一个循环中,该循环一次将大的写操作分解为几个扇区的单个事务,以避免日志溢出。
    • 作为此事务的一部分,对writei的调用写入许多块:文件的inode、一个或多个位图块以及一些数据块。
      1
      2
      3
      4
      5
      begin_op(); 
      ilock(f->ip);
      r = writei(f->ip, ...);
      iunlock(f->ip);
      end_op();

结构

  • disk上的log区域为如下

    • header block
      • 磁盘上对所有已提交事务的记录
        • 也即指示应当将哪些 logged block 复制到哪些 block 中
        • 包括一个数量n以及需落入的块号
        • 在内存中的表现就是 struct logheader{}
    • logged block
      • 内存中的block cache 先落入disk上的logged block
      • 然后再根据header block的指示,将disk上的logged block 复制到 目标block
    • unlogged block
      • 空闲的,等待cache落入的block
  • 内存中的表现形式如下

  • header block 在内存中就是 struct logheader。也就是说headerblock就这俩数据。

    1
    2
    3
    4
    5
    6
    // Contents of the header block, used for both the on-disk header block
    // and to keep track in memory of logged block# before commit.
    struct logheader {
    int n; // 总共有多少个block被修改
    int block[LOGSIZE]; // 需要修改的block编号
    };
    • n = 0 , 表示日志中没有事务
    • n !=0 , 表示日志包含一个完整的已commit的事务 , 并具有n个logged block
  • 整个日志系统的状态以及元数据都在struct log中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    struct log {
    struct spinlock lock;
    int start;
    int size;
    int outstanding; // 预定日志空间的事务数
    int committing; // in commit(), please wait.
    int dev;
    struct logheader lh; // 记录了哪些block需要被修改
    };
    struct log log; // 日志系统
    • committing : 日志系统是否正在执行commit
    • outstanding : 预定日志空间的事务数。只有当减少至0的时候才会commit。
    • logheader : 如上
    • dev : 哪个磁盘的文件系统。xv6中只有一个磁盘,故只有一个文件系统。

接口

  • begin_op : 标志开始当前事务

    • 等待日志系统commit完成,且有足够的未被使用的log block
      1
      2
      3
      //  正在提交。等待
      if(log.committing)
      sleep(&log, &log.lock);
    • ++log.outstanding : 为本次事务预定日志空间
      1
      2
      3
      4
      acquire(&log.lock);
      // 开启日志
      log.outstanding += 1;
      release(&log.lock);
  • log_write : 通过写入logheader,并通过bpin保持其cache不被覆盖。

    • 充当bwrite的代理。
    • 在内存中记录disk上的哪些block需要被修改。
      • logheader block[] 记录
    • 在磁盘上的日志中预定一个槽位
      • logheader.n++
    • 保持将要落入磁盘的block cache,防止被覆盖。
      • bpin
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        //  1. 更新log header ,将块的扇区号blockno记录在内存中
        // 如果b对应的block no已经被记录了,那么不必再记录(重复记录也无妨)
        for (i = 0; i < log.lh.n; i++) {
        if (log.lh.block[i] == b->blockno) // log absorbtion
        break;
        }
        // 幂等操作 无论是否是新增blockno
        log.lh.block[i] = b->blockno;
        if (i == log.lh.n) { // Add new block to log?
        // 2. bpin将该block的buf固定在cache中
        bpin(b); // 防止struct buf b 被释放
        // 3. 在磁盘的日志块中预定一个槽位
        log.lh.n++;
        }
  • endop : 标志当前事务完成,告知日志;若此时日志中没有未完成的事务,则执行commit。

    • 核心代码
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      acquire(&log.lock);
      // 标志完成一个事务
      --log.outstanding;
      // 如果日志中的所有事务均已完成 则 commit 日志
      if(log.outstanding == 0){
      do_commit = 1;
      log.committing = 1;
      }
      release(&log.lock);

      // commit日志 if do_commit = 1
      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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    static void commit()
    {
    if (log.lh.n > 0) {
    write_log(); // Write modified blocks from cache to log
    write_head(); // Write header to disk -- the real commit
    install_trans(0); // Now install writes to home locations
    log.lh.n = 0;
    write_head(); // Erase the transaction from the log
    }
    }
    • 一次commit可能涉及多个事务的写入。
    • write_log : 将日志记录落入磁盘
      • 根据logheader中的记录,将被修改的 目标block cache 落入 disk上的 log block
      • 目标block cache –copy into–> log block cache –落入–> disk log block
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        // Copy modified blocks from cache to log.
        static void
        write_log(void)
        {
        int tail;

        for (tail = 0; tail < log.lh.n; tail++) {
        struct buf *to = bread(log.dev, log.start+tail+1); // 新从disk读出来的unlogged block
        struct buf *from = bread(log.dev, log.lh.block[tail]); // 之前访问修改的cache block
        memmove(to->data, from->data, BSIZE); // copy into log block cache
        bwrite(to); // write the log into disk
        brelse(from);
        brelse(to);
        }
        }
    • write_head : 将日志记录头落入磁盘
      • 将 logheader 落入 disk上的 header block
      • logheader –copy–> headerblock cache —落入—> disk上的headerblock
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        // Write in-memory log header to disk. through buffer list
        // This is the true point at which the
        // current transaction commits.
        static void
        write_head(void)
        {
        struct buf *buf = bread(log.dev, log.start);
        struct logheader *hb = (struct logheader *) (buf->data);
        int i;
        hb->n = log.lh.n;
        for (i = 0; i < log.lh.n; i++) {
        hb->block[i] = log.lh.block[i];
        }
        bwrite(buf);
        brelse(buf);
        }
    • install_trans : 处理已经提交的事务:将日志安装到磁盘上。
      • 根据logheader中的记录,将保存在log cache的内容落入其原本应当在的inode/bitmap/data/…block
      • 核心逻辑
      • log cache –> 目标block cache –> disk 目标block
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        static void
        install_trans(int recovering)
        {
        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 cache to dst cache
        bwrite(dbuf); // write dst cache to disk
        brelse(lbuf);
        brelse(dbuf);
        }
        }
    • 清空disk上的headerblock
      • log.lh.n = 0;
      • write_head();
        • log header记录的需要修改的block的数量为0,即等价于header block没有记录任何日志,即清空headerblock。那么就可以认为日志系统中没有任何被commit的事务,也即没有任何loggged block。
        • 这必须在下一个事务开始写入日志块之前发生,以便崩溃不会导致使用一个事务的头块和后续事务的日志块进行恢复
  • recover_from_log : 它读取日志头,如果有已经commited的事务,则将logged block 拷贝到相应的block中

    • fsinit -> initlog -> recover from log
      1
      2
      3
      4
      5
      6
      7
      8
      static void recover_from_log(void)
      {
      read_head(); // 从disk logheaderblock 读出 记录到 mem中的 struct logheader
      install_trans(1); // if committed, copy from log to disk
      log.lh.n = 0;
      write_head(); // clear the log
      // 清空已经处理完的事务 以便后续记录新的事务
      }

Inode Cache层

Inode结构

  • 术语inode(即索引结点)有2种含义。

      1. 指包含文件大小和数据块编号列表的磁盘上的数据结构(struct dinode)
      1. 指内存中的inode,它包含磁盘上inode(dinode)的副本以及内核中所需的额外信息。(struct inode)
  • An inode describes a single unnamed file.

  • disk上的inode:The inode disk structure holds metadata:

    • the file’s type, its size, the number of links referring to it, and the list of blocks holding the file’s content.

    • dinode : inode在磁盘上的结构

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      // On-disk inode structure
      struct dinode {
      // meta data
      short type; // File type 文件 / 目录 / 特殊文件 / 0表示inode空闲
      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)
      // the list of blocks holding the file's content
      uint addrs[NDIRECT+1]; // Data block addresses 保存inode代表的文件的内容的磁盘块号
      };
    • nlink : 引用本inode的entry目录项的数量(entry是真实存在于disk上的结构,断电之后还会保存在磁盘里面)

      • 当nlink = 0时,就在disk上真正释放该inode。
      • 这就是硬链接
      • tips:
        • 之前还没看xv6的时候,总看有的sb资料说 硬链接不占磁盘空间,当时觉得扯淡。怎么可能呢?现在解释如下
        • 硬链接hard link就是一个个directory下的一个个entry。其只是不占据inode blocks而已,但也要占磁盘空间啊。entry存储在directory的data block下
        • 至于软连接,xv6里面并没有实现。
    • addrs :

      • [0,11] direct block
      • [12] indirect block
  • inode cache 内存中的inode : The kernel keeps a cache of in-use inodes in memory to provide a place for synchronizing access to inodes used by multiple processes.
    • The cached inodes include book-keeping information that is not stored on disk: ip->ref and ip->valid.
    • 作用:1. 为多个进程访问inode提供同步(主要)
    • 作用:2. 缓存,加速
    • icache and inode : inode在内存中的结构
      • inode cache : a cache of in-use inode
        • inode cache 是 活跃的inode的集合。
          1
          2
          3
          4
          struct {
          struct spinlock lock; // 1. 保证同一个dinode在inode中最多出现一次。 2. 维护inode.ref的正确性。
          struct inode inode[NINODE];
          } icache;
      • struct inode :
        • 是disk上dinode的在内存的缓存,只有C指针引用某个inode时,icache中才会存储该inode
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          17
          //  The cached inodes include book-keeping information that is not stored on disk: ip->ref and ip->valid.
          // 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
          // 确保独占的访问 inode的size,data block等
          int valid; // inode has been read from disk?

          short type; // copy of disk inode
          short major;
          short minor;
          short nlink; // 引用本inode的目录项的数量
          uint size; // 文件大小吧,感觉应该是指向的datablock的大小之和。
          uint addrs[NDIRECT+1]; // 最终都指向 block number
          };
      • ref : 内存中引用本struct inode的C指针数量
        • 如果ref = 0,那么,icache就可以将该struct inode从缓存中踢掉。
        • 与什么软链接、硬链接无关
        • C指针是存在于内存的,不是磁盘里真实存在的东西。
      • valid : struct inode是否已经从磁盘中读取dinode部分
        • 如果valid = 0,那么,重新从disk读取。

接口

datablock分配

  • balloc : datablock分配器

    • 遍历bitmap,通过bread获取free data block的cache,并返回其block no。
      • 注意只是将该free data block加载进了cache,返回了blockno。并没有独占该block cache 也即并没有lock该block cache
      • 这个工具函数就不放代码了,见注释吧,挺简单。
      • 注意下对于meta blocks的bit : super block , logging , inodes ,bitmap。在mkdfs中,bitmap就将其全部将其置为1,这样才符合bitmap指向datablock的逻辑。
  • bfree : datablock释放器

    • static void bfree(int dev, uint b)
    • (我认为)真正的释放disk上的data block b
      • 并没有将buf清0,而是仅仅通过将bitmap的相应bit清0来代表该block里面的数据已经没用,可以被覆盖,也即该block是一个free block ,可以再被分配。
      • 当然是commit之后生效。
    • 核心逻辑
      1
      2
      3
      4
      5
      6
      7
      bi = b % BPB;
      m = 1 << (bi % 8);
      if((bp->data[bi/8] & m) == 0)
      panic("freeing free block");
      // 清空该bit
      bp->data[bi/8] &= ~m;
      log_write(bp);

inode分配

  • iget: 从inode cache中 返回 编号为inum的inode
    • iget(uint dev, uint inum)
    • Does not lock the inode and does not read it from disk
    • 返回一个不被独占的inode。
    • 如果这个inum对应的inode之前被使用过
      • 那么在icache中就可以找到该inode,
      • ++ref,返回即可
        1
        2
        3
        4
        5
        6
        7
        //  ip->ref++ 代表内存中引用struct inode结构体的人又多了一个。
        if(ip->ref > 0 && ip->dev == dev && ip->inum == inum){
        ip->ref++;
        printf("1 ip->type = %d\n",ip->type);
        release(&icache.lock);
        return ip;
        }
    • 如果这个inum对应的inode之前没使用过,
      • 那么在icache中找不到该inode,替换icache中无用的inode,
      • 对inode 进行 inode独有的metadata的初始化.(dinode的metadata之后会从disk读)
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        //  ref = 0 , 即该struct inode已经内存中无人引用。
        // 那么将其替换掉
        if(empty == 0 && ip->ref == 0) // Remember empty slot.
        empty = ip;
        // 初始化inode的metadata
        ip = empty;
        ip->dev = dev;
        ip->inum = inum; // dev and inum
        ip->ref = 1; // 引用为1
        ip->valid = 0;
  • ialloc : 从inode cache获取一个已经初始化了inode metadata的free inode
    • 返回的是个没独占的inode。
    • 核心逻辑
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      struct inode*
      ialloc(uint dev, short type)
      {
      struct buf *bp;
      struct dinode *dip;
      for(inum = 1; inum < sb.ninodes; inum++){
      // 获取disk上第inum个inode所属的block buf(位于buf list)
      bp = bread(dev, IBLOCK(inum, sb));
      // 获取该dinode
      dip = (struct dinode*)bp->data + inum%IPB;
      // free dip
      if(dip->type == 0){
      memset(dip, 0, sizeof(*dip));
      dip->type = type; // 声明该dinode被使用
      log_write(bp); // 落入磁盘
      brelse(bp);
      // 返回一个inode cache中的inode
      // 可以看到此时返回的inode 其中的type和dip->type并不一致。那么如何解决?:在调用者获取了该inode之后,会对属于dinode的metadata进行初始化
      return iget(dev, inum);
      // in-memory copy of an inode
      }
      brelse(bp);
      }
      }
  • 可以看到ialloc的调用情景,在获取inode之后,会对struct inode中dinode的metadata部分进行初始化。
    1
    2
    3
    4
    5
    6
    ip = ialloc(dp->dev, type);
    ilock(ip);
    ip->major = major;
    ip->minor = minor;
    ip->nlink = 1;
    iupdate(ip);
  • ilock

      1. acquire(sleeplock)
      1. 根据需要从磁盘中读取struct inode中尚未读取的dinode部分
    • 在读写inode的元数据或内容之前,代码必须使用ilock锁定inode
    • 将inode指针的获取与锁定分离有助于在某些情况下避免死锁,例如在目录查找期间。多个进程可以持有指向iget返回的inode的C指针,但一次只能有一个进程锁定inode。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      void ilock(struct inode *ip)
      {
      // 1. ilock(inode)
      acquiresleep(&ip->lock);
      // 2. 从磁盘中读取struct inode中尚未读取的dinode部分
      if(ip->valid == 0){
      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;
      memmove(ip->addrs, dip->addrs, sizeof(ip->addrs));
      brelse(bp);
      ip->valid = 1;
      }
      }
  • iput

    • iput(struct inode *ip)
      1. 放下本thread对于inode的引用
      • ip->ref–;
      • 如果inode的ref降至0,那么意味着inode cahce中的该inode在内存中无人引用。此后在iget中可以将其替换掉。
      1. 如果此刻已经无人再引用该inode,且其对应的dinode的nlink = 0(也即硬链接计数为0,也即没有entry指向该inode)
      • 那么该inode不只留在内存中无用,留在磁盘中也无用。那么如下
      • inode大小置0并释放其所引用的addrs + 标记为未分配 + 写入磁盘
    • iput中释放inode的锁定协议值得仔细研究
      • 先不研究了。。。要学不完了。。
  • iunlock

    • 释放inode的lock
      1
      2
      3
      4
      void iunlock(struct inode *ip)
      {
      releasesleep(&ip->lock);
      }
  • inode指针的获取和上锁进行分离

    • iget : inode cache中 inode指针的获取
    • ilock : 对 inode cache中 inode进行lock
    • 作用:有助于在某些情况下避免deadlock
      • 例如在目录查找期间 : 多个进程可以持有指向iget返回的inode的C指针,但一次只能有一个进程锁定inode。
  • iupdate

    • 作用:将inode cache中对inode的修改,落入disk上的dinode中.
    • inode cache —copy into–> dinode block cache –落入-> disk dinode
    • inode cache直写:每次修改inode之后,必须立刻执行iupdate,将其落入disk
    • 简单,不放代码了。

inode使用

  • bmap

    • bmap(struct inode *ip, uint bn)
    • 作用:z 返回inode指向的第bn个datablock的blockno,并根据需要分配datablock
      • The bn argument is a “logical block number” – a block number within the file, relative to the start of the file. The block numbers in ip->addrs[], and the argument to bread(), are disk block numbers. You can view bmap() as mapping a file’s logical block numbers into disk block numbers.
      • 如直接块插槽没有指向的block,则balloc为其分配
      • 如没有indirect block,则balloc为其分配
      • 如间接块插槽没有指向的block,则balloc为其分配。
    • 并不独占datablock cache
    • ip->addrs[]或间接块中条目为零表示未分配块。
      • 通过balloc分配datablock (bmap获取并返回free datablock的blockno)
    • 感觉很像vm.c里的一些函数
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      static uint bmap(struct inode *ip, uint bn)
      {
      // 如果inode的直接块并没有指向datablock
      if(bn < NDIRECT){
      if((addr = ip->addrs[bn]) == 0)
      ip->addrs[bn] = addr = balloc(ip->dev);
      return addr;
      }

      // 间接块
      bn -= NDIRECT;
      // Load indirect block, allocating if necessary.
      if(bn < NINDIRECT){
      // 如果indirect间接块为0 ,那么为其分配balloc一个freeblock作为indirect block
      if((addr = ip->addrs[NDIRECT]) == 0)
      ip->addrs[NDIRECT] = addr = balloc(ip->dev);
      // 得到indirect block
      bp = bread(ip->dev, addr);
      a = (uint*)bp->data;
      // indirect block的条目bn没有指向datablock
      if((addr = a[bn]) == 0){
      a[bn] = addr = balloc(ip->dev);
      log_write(bp);
      }
      brelse(bp);
      return addr;
      }
      // 如果blockno 超过NDIRECT+NINDIRECT,则bmap调用panic崩溃
      panic("bmap: out of range");
      }
  • itrunc

    • 清空inode : 释放inode所引用的块,并置inode代表的文件大小为0
        1. 释放其所指向的所有直接块(disk 上)
        1. 释放其所指向的所有间接块(disk 上)
        1. 文件大小置为0
  • readi

    • int readi(struct inode *ip, int user_dst, uint64 dst, uint off, uint n)
    • 作用:读取从inode代表的文件的第off个bytes开始(从disk / buf),读取n个bytes到dst中
    • 调用了bmap
    • disk(datablock) ——-> block cache —copyinto—> user/kernel virtual adddress
    • 和巡逻机
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      int readi(struct inode *ip, int user_dst, uint64 dst, uint off, uint n)
      {
      // 确保起始指针偏移量不超过文件的末尾。
      if(off > ip->size || off + n < off)
      return 0;
      // 确保最多只能读取到文件末尾
      if(off + n > ip->size)
      n = ip->size - off;
      // 开始读取inode指向的datablock
      // 将data从disk copy到内核内存再copy到用户内存
      // total:总共读了file的多少bytes ; off:读到file的第几个byte了 ; dst : user virtual address
      for(tot=0 ; tot<n ; tot+=m, off+=m, dst+=m){
      // bmap(ip, off/BSIZE) : 获取file的第off个bytes对应的block no
      bp = bread(ip->dev, bmap(ip, off/BSIZE));
      // 将buf拷贝到用户内存user_dst
      either_copyout(user_dst, dst, bp->data + (off % BSIZE), m);
      brelse(bp);
      }
      return tot;
      }
  • wirtei

    • writei(struct inode *ip, int user_src, uint64 src, uint off, uint n)
    • 用户从inode指向的第off个bytes开始,写n个bytes,从src address到disk
    • user/kernel addr -> block cache -> disk
    • 代码略。类似readi
  • stati
    • stati(struct inode *ip, struct stat *st)
    • Copy stat information from inode
    • Caller must hold ip->lock

Directory 目录层

结构

  • 目录也是一个文件,因此也是一个inode来代表

    • directory inode :
      • type = T_DIR
      • 其addrs所指向的datablock,其中包含的data都是一个个entry,代表directory下的子文件/子目录
  • entry的结构如下:

    • entry不是file,并不需要由inode代表,而仅仅是一个directory的inode的datablock中的数据。其结构如下
      1
      2
      3
      4
      5
      6
      //  目录项、entry。文件夹中的一系列代表其下的文件/文件夹的东西就叫entry. 
      // dirent不是文件夹,之前还误以为是文件夹。傻冒。
      struct dirent {
      ushort inum; // 用于索引inode
      char name[DIRSIZ]; // 一
      };

接口

  • dirlookup
    • struct inode* dirlookup(struct inode *dp, char *name, uint *poff)
    • 在目录directory中搜索具有给定名称的条目entry。
      • 调用了readi,iget
      • 如果找到
          1. 将*poff设置为条目在目录中的字节偏移量
          1. 通过iget获得的未锁定的inode 并 return unlocked inode*
      • 如果没找到
        • return 0
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          for(off = 0; off < dp->size; off += sizeof(de)){
          // 每轮读出一个entry : struct dirent
          readi(dp, 0, (uint64)&de, off, sizeof(de);
          // 当前的entry所能索引到的inode 和name是否匹配
          if(namecmp(name, de.name) == 0){
          // entry matches path element
          *poff = off;
          inum = de.inum;
          // 返回该entry所对应的inode
          return iget(dp->dev, inum);
          }
          }
  • dirlink
    • int dirlink(struct inode *dp, char *name=xxx, uint inum=123)
    • 作用:在directory dp下 ,新增一个entry。entry的name是xxx,指向的inode是123
    • 调用了dirlookup,readi,writei , 核心逻辑见下。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      int dirlink(struct inode *dp, char *name, uint inum)
      {
      // Check that name is not present.
      if((ip = dirlookup(dp, name, 0)) != 0){
      iput(ip);
      return -1;
      }
      // 找到空闲的entry
      for(off = 0; off < dp->size; off += sizeof(de)){
      readi(dp, 0, (uint64)&de, off, sizeof(de));
      if(de.inum == 0)
      break;
      }
      // 写入name和inum
      strncpy(de.name, name, DIRSIZ);
      de.inum = inum;
      // 写入disk (如果现有的size里面没有free entry 那么就会增添新entry , 扩展dp->size)
      writei(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de)
      }

Pathname 路径层

  • 工具函数:skipelem

    • static char* skipelem(char *path, char *name)
    • skipelem 将path中的 第一个path element(应该是个文件夹名字)取出拷贝到name中 , 返回path中除去path element之后剩余的内容
      • 对于取出的给name的path element,如果是路径末尾的最后一个path element,那么就是个file的名字;如果不是路径末尾的最后一个path element,即中间的pathelement,则是一个directory的名字。
      • 可以去除/
      • 返回null时就代表name现在保存的已经是path中最后一个element
      • 传入的*path = ‘\0’时,返回nullptr,且不改变name
    • 如下
      1
      2
      3
      4
      5
      6
      7
      8
      9
      printf("path left: %s\n",skipelem("/123///456/789/",name));
      printf("name : %s\n",name);

      printf("path left: %s\n",skipelem("aaaa",name));
      printf("name : %s\n",name);

      name[0] = 0;
      printf("path left: %s\n",skipelem("",name));
      printf("name : %s\n",name);
  • namex

    • static struct inode* namex(char *path, int nameiparent, char *name)
    • nameiparent = 0 : 返回path最后一个element(file/directory)的inode,并将最后一个element的名字拷贝到name中
    • nameiparent = 1 : 返回path最后一个element(file/directory)的的parent的inode,并将最后一个element的名字拷贝到name中
    • 从哪里查找的?:
      • 将path一级级,逐级拆分出一个个name,在当前directory inode下查找出(dirlookup)inode;
      • 然后将该inode,作为下一级name的directory node,也即在这个刚查出来的inode下,查找name对应的inode。
      • 一级级查找,直到查到path的最后一级别的inode。返回即可。
  • namex过程可能需要很长时间才能完成:它可能涉及多个磁盘操作来读取路径名中所遍历目录的索引节点和目录块(如果它们不在buffer cache中)。

  • Xv6经过精心设计,如果一个内核线程对namex的调用在磁盘I/O上阻塞,另一个查找不同路径名的内核线程可以同时进行。Namex分别锁定路径中的每个目录,以便在不同目录中进行并行查找。

  • struct inode* namei(char *path)

    • 返回path最后一个element的inode
    • return namex(path, 0, name);
  • struct inode* nameiparent(char *path, char *name)

    • 返回path最后一个element的父目录的inode
    • return namex(path, 1, name);
    • 可用于在某dir路径下创建新entry。

fd 文件描述符层

结构

  • 为什么可以多个process同时打开一个文件?答案就是如下结构体 struct file。
  • struct file{} : 文件结构体
    • inode或管道的封装,加上一个I/O偏移量
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      struct file {
      enum { FD_NONE, FD_PIPE, FD_INODE, FD_DEVICE } type;
      int ref; // reference count 当前内存中有几个人正在使用该文件
      char readable;
      char writable;
      struct pipe *pipe; // FD_PIPE
      struct inode *ip; // FD_INODE and FD_DEVICE
      uint off; // FD_INODE
      short major; // FD_DEVICE
      };
    • 每次调用open都会创建一个新的打开文件(一个新的struct file):如果多个进程独立地打开同一个文件,那么不同的实例将具有不同的I/O偏移量。
    • 另一方面,单个打开的文件(同一个struct file)可以多次出现在一个进程的文件表中,也可以出现在多个进程的文件表中
    • 如果一个进程使用open打开文件,然后使用dup创建别名,或使用fork与子进程共享,就会发生这种情况。
  • struct ftable : 文件表file table 存储所有struct file
    • file table : 每个槽位都是个struct file容器,存储所有打开的文件,即创建的struct file
      1
      2
      3
      4
      struct {
      struct spinlock lock;
      struct file file[NFILE];
      } ftable;
  • 文件描述符表 : 每个进程都有的一个fd表

    • proc结构体中有struct file *ofile[NOFILE]; // Open files
    • 这就是常说的文件描述符表/fd表
    • 是每个process 由 fd 到 struct file的索引表
  • 可以看到,struct inode和struct file本身并不具有name属性,实际上都是entry中附加包含的

接口

  • filealloc

    • struct file* filealloc(void)
    • 作用:从ftable中返回一个free的struct file容器给调用者用于承载文件。
      • 仅仅是在操作struct file
        1
        2
        3
        4
        5
        6
        7
        8
        9
        acquire(&ftable.lock);
        for(f = ftable.file; f < ftable.file + NFILE; f++){
        if(f->ref == 0){
        f->ref = 1;
        release(&ftable.lock);
        return f;
        }
        }
        release(&ftable.lock);
  • fdalloc

    • fdalloc(struct file *f)
    • 将 struct file记录在process的文件描述符表ofile中的空闲位置,为用户提供一种方式:通过free fd来索引struct file
      • 记录file: process->ofile[fd] = f
      • 返回fd
  • filedup

    • 重复引用file
    • f->ref++;
  • fileclose

    • 作用:释放引用,若对file的ref降至0,则iput其inode
      • 操作struct file,有可能操作inode
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        void fileclose(struct file *f)
        {
        acquire(&ftable.lock);
        if(--f->ref > 0){
        release(&ftable.lock);
        return;
        }
        // f.ref == 0;
        ff = *f;
        // 清空ftable记录
        f->ref = 0;
        f->type = FD_NONE;
        release(&ftable.lock);
        // 释放file的inode
        begin_op();
        iput(ff.ip);
        end_op();
        }
  • fileread

    • 通过readi,在file->inode中读取数据送入用户内存。(读取的起点是struct file.offset)
    • 核心逻辑如下
      • 检查readable/writable是否允许该操作,然后将调用传递给pipe / inode的实现
        1
        2
        3
        4
        5
        6
        7
        8
        fileread(struct file *f, uint64 addr, int n)
        {
        ilock(f->ip);
        // f->off即为操作文件时的起始偏移量
        r = readi(f->ip, 1, addr, f->off, n);
        f->off += r;
        iunlock(f->ip);
        }
  • filewrite

    • 通过writei,向file->node写数据
    • 检查打开模式是否允许该操作,然后将调用传递给管道或inode的实现
      • 核心逻辑如下
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        int i = 0;
        while(i < bytes_to_write){
        begin_op();
        ilock(f->ip);
        r = writei(f->ip, 1, addr + i, f->off, n1);
        f->off += r;
        iunlock(f->ip);
        end_op();
        i+=r;
        }
  • 通过fileread和filewrite可以看出,xv6中,文件的read和write共同维护同一个offset.

  • 且可以看出,得益于ilock(file.inode),使得各个进程可以原子的更新offset;即,使得对同一文件的同时多次写入不能覆盖彼此的数据,尽管他们的写入最终可能是交错的。

系统调用

  • sys_link
    • 作用:新建立一条硬链接 从 new entry 索引到 现有inode
    • 直观来看就是在编辑目录,为现有的inode新增一个名字。通过创建个指向现有inode的entry实现。
    • 下面约定叫老文件old对应的inode,即需要新增链接的这个node,称为目标inode
    • trapframe传入old,new
      • old : 目标inode的name
      • new : 新entry的路径
    • 逻辑
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      sys_link(void)
      {
      // old : 老文件的name
      // new : 即将新建立的entry,我们将要让该entry指向old inode。注意这个new不是file,而是一个entry
      // 目的 : 要在new所在的directory,建立一个新entry(old_inum , new_entry_name)
      char new[MAXPATH], old[MAXPATH];

      begin_op();
      // 操作目标inode : ++nlink
      {
      ip = namei(old); // 根据name old获取对应的inode
      ilock(ip);

      // check whether ip is directory ; if true , fail;
      ip->nlink++;

      iupdate(ip);
      iunlock(ip);
      }
      // 操作new的parent inode : 创建entry(old_inum,new)
      {
      dp = nameiparent(new, name);
      ilock(dp);
      dirlink(dp, name, ip->inum); // directory dp下 新增entry(inum,name)
      iunlockput(dp);
      iput(ip);
      }
      end_op();
      }
    • 硬链接不支持directory。ubuntu下 hard link not allowed for directory。xv6下的 sys_link 也会检查要增加link的inode是否是directory;若是,则失败
  • sys_unlink

    • 删除entry,–inode.nlink
      1
      2
      3
      memset(&entry, 0, sizeof(entry));
      writei(entry, 0, (uint64)&entry, off, sizeof(entry));
      ip->nlink--;
  • create

    • static struct inode* create(char *path, short type, short major, short minor)
    • 作用新创建一个inode,并在path处创建一个entry指向该inode。注意更新其parent directory。
    • 返回一个独占的inode
    • 简单来说,就是创建新node 并给其新name
    • 逻辑如下
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      //  create 为新inode创建一个新名称的新entry
      // 返回一个被锁定的inode
      static struct inode* create(char *path, short type, short major, short minor)
      {
      // 找到path 最后一个 element的 parent directory
      dp = nameiparent(path, name);

      ilock(dp);

      // 如果之前存在 视为成功
      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;
      }


      // if name对应的entry对应的inode并不存在 , 那么进行分配inode
      // get初始化了inode metadata的i弄得
      ip = ialloc(dp->dev, type);

      // inode 中dinode meta data初始化
      ilock(ip);
      ip->major = major;
      ip->minor = minor;
      ip->nlink = 1;
      iupdate(ip);

      // 如果create是要创建一个目录 , 也即 inode要作为一个目录的inode
      if(type == T_DIR){ // Create . and .. entries.
      dp->nlink++; // for ".."
      // No ip->nlink++ for ".": avoid cyclic ref count.
      iupdate(dp); // write through
      // 在inode下,新增自带的. 和 .. entry
      dirlink(ip, ".", ip->inum);
      dirlink(ip, "..", dp->inum);
      }

      // 在parent directory下 创建索引新建inode的entry
      dirlink(dp, name, ip->inum);

      iunlockput(dp);

      return ip;
      }
  • sys_open、sys_mkdir和sys_mknod都利用了create

  • sys_mkdir: 创建文件夹

    1
    2
    3
    4
    begin_op();
    ip = create(path, T_DIR, 0, 0);
    iunlockput(ip);
    end_op();
  • sys_open: 创建文件

      1. 以某种方式获得inode
      • O_CREATE标志,调用create
      • 否则,调用namei
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        //  如果open被传递了O_CREATE标志,它将调用create(kernel/sysfile.c:301)。
        // create ilock(inode)
        if(omode & O_CREATE){
        ip = create(path, T_FILE, 0, 0);
        // 否则,它将调用namei(kernel/sysfile.c:307)
        // namei不ilock(inode),因此需要sys_open自己ilock
        } else {
        ip = namei(path)
        ilock(ip);
        }
      1. 为新建的inode 分配 free struct file 和 free fd。
      • 从全局的ftable中分配free struct file 来作为文件容器,承载inode。
      • 将 struct file记录在process的文件描述符表ofile中的空闲位置,为用户提供一种方式:通过free fd来索引struct file。
        1
        2
        3
        4
        //  从ftable中分配free struct file 来承载inode
        // 从ofile中分配free fd 来索引struct file
        f = filealloc();
        fd = fdalloc(f);
      1. 填充该文件
        1
        2
        3
        4
        5
        6
        7
        8
        9
        f->type = FD_INODE;
        f->off = 0;
        f->ip = ip; // inode挂在struct file上
        f->readable = !(omode & O_WRONLY); // 根据mode指明权限
        f->writable = (omode & O_WRONLY) || (omode & O_RDWR);
        // if mode为O_TRUNC
        itrunc(ip);
        iunlock(ip);
        end_op();
      1. return fd
      • 给user返回file索引:fd。
      • 结束。文件至此创建完毕。

小结

  • 首先,文件系统是一个位于磁盘的数据结构
  • 关注block cache的实现,这对于性能来说是至关重要的,因为读写磁盘是代价较高的操作,可能要消耗数百毫秒,而block cache确保了如果我们最近从磁盘读取了一个block,那么我们将不会再从磁盘读取相同的block。