xv6 file system

发布时间 2023-12-04 18:50:44作者: trashwin

xv6 file system

在我看来文件系统某种程度上是最复杂的一部分(单从页数也足以说明了),而且我对文件系统的了解其实很少,因此这部分仔细看了一下。

xv6文件系统提供类似unix的文件、目录和路径名,并将其数据存储在virtio磁盘上以实现持久化。
文件系统解决了几个挑战:

  1. 文件系统需要磁盘上的数据结构来表示目录树和文件,记录保存每个文件内容的块的标识,并记录磁盘的哪些区域是空闲的。
  2. 文件系统必须支持崩溃恢复。也就是说,如果发生崩溃,文件系统必须在重新启动后仍能正常工作。
  3. 不同的进程可能同时在文件系统上操作,因此文件系统代码必须协调以维护invariants(这个词一直不知道怎么翻译,应该是修改过程需要原子操作的量,修改前后具有相同含义,但过程中可能变化)。
  4. 访问磁盘比访问内存慢几个数量级,因此文件系统必须在内存中维护缓存。

xv6文件系统一共有七层。

disk

磁盘硬件传统上将磁盘上的数据表示为512字节块,即扇区。操作系统为其文件系统使用的块大小可能与磁盘使用的扇区大小不同,但块大小通常是扇区大小的倍数。Xv6在buf中保存已读入内存的块的副本。存储在这个结构中的数据有时与磁盘不同步:它可能还没有从磁盘读入(磁盘正在处理它,但还没有返回扇区的内容),或者它可能已经被软件更新了,但还没有写入磁盘。

xv6将磁盘划分为几个部分,文件系统不使用块0(它包含引导扇区)。区块1是超级区块(由一个名为mkfs的单独程序填充,构建一个初始文件系统),它包含有关文件系统的元数据(以块为单位的文件系统大小、数据块数量、inode数量和日志中的块数量)。从2开始的块记录日志,之后是inodes,之后是位图块,记录数据块使用情况,剩下的块是数据块。

buffer

Buffer cache有两个任务

  • 同步对磁盘块的访问,以确保内存中只有一个块的副本,并且一次只有一个内核线程使用该副本;
  • 缓存常用块,提供快速读写。

buffer的主接口由bread和bwrite组成,前者获取一个包含可在内存中读取或修改的块副本,后者将修改后的缓冲区写入磁盘上相应的块,二者均调用virtio_disk_rw。bget获取LRU缓冲队列对应块号的缓存,或者未缓存时选取队尾缓存,无论哪种情况返回的缓存块都带锁(每个缓存块各自的睡眠锁,而不是缓存整体的那把大自旋锁),保证一次只有一个线程使用该缓存块。brelse释放缓存块的睡眠锁。bio.c中双链和各种操作的实现非常规整,简单易读,值得学习。

buf缓冲区有两个与之相关联的状态字段。字段valid表示缓冲区包含该块的副本。字段disk表示缓冲区内容已被交给磁盘。

xv6并未实现缓存的置换,在没有空闲块时,只是简单地panic。为什么不选择队尾的块置换呢?或者睡眠?也许是简单吧QAQ

logger

这部分描述得好抽象啊,看书完全不懂,结合代码看了好久?
xv6通过一种简单的日志记录形式解决了文件系统操作期间的崩溃问题。
系统调用不直接写入磁盘上的文件系统数据结构,它在磁盘的日志中放置了它希望进行的所有磁盘写操作的描述。一旦系统调用记录了它的所有写操作,它就会向磁盘写入一条特殊的提交记录,表明该日志包含一个完整的操作。然后系统调用将写操作复制到磁盘上的文件系统数据结构中。在这些写操作完成之后,系统调用擦除磁盘上的日志。

如果在操作提交之前发生崩溃,恢复代码将忽略它。如果在操作提交之后发生崩溃,那么恢复将重复的所有写操作,如果操作已经开始将它们写入磁盘上的数据结构,则可能会重复这些操作。在任何一种情况下,日志都使操作相对于崩溃是原子的:要么所有操作的写操作都出现在磁盘上,要么都不出现。

日志的固定位置记录在超级块中。磁盘日志由一个header块和一系列logged block记录块组成。header块包含一组扇区号(每个对应一个日志块即修改的块),以及总的日志块数。计数要么为零,表示日志中没有事务,要么非零,表示日志中包含一个完整的已提交事务(此时block中存放的就是日志块的块号)。xv6在事务提交时写入头块,并在将记录的块复制到文件系统后将计数设置为零。因此,在事务中途崩溃将导致日志头块中的计数为零,提交后的崩溃将导致非零计数。

struct logheader {
  int n;
  int block[LOGSIZE];
};

struct log {
  struct spinlock lock;
  int start;       // 磁盘中日志的开始块号,第一个为header,后续为日志块
  int size;
  int outstanding; // how many FS sys calls are executing.
  int committing;  // in commit(), please wait.
  int dev;
  struct logheader lh;
};

xv6在磁盘上指定了固定数量的空间LOGSIZE来保存日志,系统调用在事务中写入的块总数必须适合该空间。

每个系统调用的代码指示写序列的开始begin_op和结束end_op,这些写序列相对于崩溃来说是原子的。为了允许不同进程并发执行文件系统操作,日志系统可以将多个系统调用的写操作累积到一个事务中。减少了磁盘操作的数量,还同时为磁盘系统提供了更多的并发写操作,可能允许磁盘在单个磁盘旋转期间写所有这些操作。xv6的virtio驱动程序不支持这种批处理,但是xv6的文件系统允许这样做。

日志操作的通常形式:

begin_op等待,直到日志系统当前没有提交并且有足够的日志空间来容纳来自此调用的写操作。log.outstanding记录日志空间中的系统调用数(为多进程并发准备的,单处理器outstanding始终为1或0),总占用空间为log.outstanding * MAXOPBLOCKS。

void
begin_op(void)
{
  acquire(&log.lock);
  while(1){
    if(log.committing){
      sleep(&log, &log.lock);
    } else if(log.lh.n + (log.outstanding+1)*MAXOPBLOCKS > LOGSIZE){
      // this op might exhaust log space; wait for commit.
      sleep(&log, &log.lock);
    } else {
      log.outstanding += 1;
      release(&log.lock);
      break;
    }
  }
}

log_write 在内存中记录修改块的块号,在磁盘日志中为其保留一个槽位,并将缓冲区固定在缓存中(调用bpin增加引用计数,导致bget不会将其替换)。在提交之前,块必须留在缓存中:在此之前,缓存副本是修改的唯一记录;在提交之前,不能将其写入磁盘上的位置;同一事务中的其他读操作必须看到修改。一个块在单个事务中被多次写入时,在日志中为该块分配相同的槽位。

void
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");
  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;
  if (i == log.lh.n) {  // Add new block to log?
    bpin(b);
    log.lh.n++;
  }
  release(&log.lock);
}

end_op 首先减少未完成系统调用的计数。如果计数现在为零,则通过调用commit提交当前事务。这个过程有四个阶段,write_log 将事务中修改的每个块从缓存复制到磁盘上的日志块中;write_head 将头块写入磁盘,此时磁盘中已经有了事务的记录,计数不为0,之后的崩溃将导致恢复;install_trans从日志中读取每个块并将其写入文件系统中的适当位置,完成实际上修改磁盘的操作;将内存中的header缓存计数改为0,再次write_head,这必须在下一个事务开始写入记录的块之前发生,这样崩溃就不会导致使用一个事务的header和后续事务的记录块进行恢复。

void
end_op(void)
{
  int do_commit = 0;

  acquire(&log.lock);
  log.outstanding -= 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);
  }
}

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
  }
}

而recover_from_log和commit内容很像,只不过header来源不是现在内存中的header,而是磁盘日志中的header。

static void
recover_from_log(void)
{
  read_head();
  install_trans(1); // if committed, copy from log to disk
  log.lh.n = 0;
  write_head(); // clear the log
}

在写文件时,用循环讲一个大的写操作分解为多个事务,每次只写不分块,避免日志溢出。

block allocator

xv6的block allocator在磁盘上维护一个空闲的位图,每个块有一个位,0位表示对应的块是空闲的,1位表示正在使用。
balloc顺序遍历位图bitmap,直到找到一个空闲块,返回块号。
bfree清除块号对应的位。

inode

inode可以指包含文件大小和数据块编号列表的磁盘数据结构,或者指内存中的inode,它包含磁盘上inode的副本以及内核中所需的额外信息。
磁盘上的inode放在inode块的连续磁盘区域中。每个索引节点的大小都是相同的,所以在给定数字n的情况下,很容易找到磁盘上的第n个索引节点。数字n(称为inode号或i-number)是在实现中标识inode的方式。

磁盘上的inode是由struct dinode定义。type字段用于区分文件、目录和设备,0表示磁盘上的inode是空闲的。nlink字段计算引用该inode的目录条目的数量,以便识别何时应该释放磁盘上的inode及其数据块。size字段记录了文件中内容的字节数,addr数组记录保存文件内容的磁盘块的块号(包括可能得索引块)。

// 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+2];   // Data block addresses
};

文件的多级索引结构,bigfile主要操作的部分

内核将活动节点的集合保存在一个itable表(某种程度上算是inode缓存)中,struct inode是struct dinode在磁盘上的内存副本。内核只有在存在指向该索引节点的C指针时才会将该索引节点存储在内存中。ref字段计算引用内存中索引节点的C指针的数量,如果引用计数降至零,内核将从内存中丢弃该索引节点。iget和input函数获取和释放指向索引节点的指针,修改引用计数。指向inode的指针可以来自文件描述符、当前工作目录和临时内核代码(如exec)。

struct {
  struct spinlock lock;
  struct inode inode[NINODE];
} itable;

// 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+2];
};

在xv6的inode代码中有四种锁或类似锁的机制。

  • itable.lock锁保护索引节点在索引节点表中最多出现一次,以及保护内存中索引节点的ref字段。
  • inode.lock确保对inode的字段(如文件长度)以及inode的文件或目录内容块的独占访问。
  • 如果索引节点的ref大于零,则系统将在表中维护该索引节点,而不会为其他索引节点重用该表项。
  • nlink字段(在磁盘上,如果在内存中,则在内存中复制),该字段计算引用文件的目录条目的数量,如果nlink大于零,xv6就不会释放它。

由iget返回的结构节点指针保证在调用input之前有效:索引节点不会被删除,指针所引用的内存也不会被另一个索引节点重用。iget提供对索引节点的非排他性访问,因此可以有多个指向同一个索引节点的指针。文件系统代码的许多部分都依赖于iget的这种行为,既要保持对索引节点的长期引用(作为打开的文件和当前目录),又要防止竞争,同时避免操作多个索引节点的代码中的死锁(例如路径名查找)。

iget返回的结构索引节点可能没有任何有用的内容。为了确保它拥有磁盘上inode的副本,代码必须调用illock。这将锁定inode(因此没有其他进程可以锁定它),并从磁盘读取尚未读取的inode。iunlock释放索引节点上的锁。将索引节点指针的获取与锁定分离有助于在某些情况下避免死锁,例如在目录查找期间。多个进程可以保存指向iget返回的索引节点的C指针,但是一次只有一个进程可以锁定该索引节点。

static struct inode*
iget(uint dev, uint inum)
{
  struct inode *ip, *empty;

  acquire(&itable.lock);

  // Is the inode already in the table?
  empty = 0;
  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;
  }

  // Recycle an inode entry.
  if(empty == 0)
    panic("iget: no inodes");

  ip = empty;
  ip->dev = dev;
  ip->inum = inum;
  ip->ref = 1;
  ip->valid = 0;
  release(&itable.lock);

  return ip;
}

void
ilock(struct inode *ip)
{
  struct buf *bp;
  struct dinode *dip;

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

  acquiresleep(&ip->lock);

  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;
    if(ip->type == 0)
      panic("ilock: no type");
  }
}

ialloc分配一个新的索引节点,类似于balloc,它循环遍历磁盘上的索引节点结构,寻找一个标记为空闲的。

struct inode*
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;
    if(dip->type == 0){  // a free inode
      memset(dip, 0, sizeof(*dip));
      dip->type = type;
      log_write(bp);   // mark it allocated on the disk
      brelse(bp);
      return iget(dev, inum);
    }
    brelse(bp);
  }
  printf("ialloc: no inodes\n");
  return 0;
}

input 通过减少引用计数释放一个指向索引节点的C指针。如果这是最后一次引用,那么itable表中该inode的槽现在是空闲的。如果input看到没有指向索引节点的C指针引用,并且该索引节点没有指向它的链接(没有出现在任何目录中),那么必须释放该索引节点及其数据块。调用itrunc将文件截断为零字节,释放数据块;设置索引节点类型为0(未分配),并将inode写入磁盘。

iput可以写入磁盘。这意味着任何使用文件系统的系统调用都可以写入磁盘,因为系统调用可能是对文件有引用的最后一个系统调用。即使像read这样看似只读的调用,最终也可能调用iput,这意味着只读系统调用使用文件系统,也必须封装在事务中。

当文件的链接计数降至零时,iput不会立即截断文件,因为某些进程可能仍然在内存中保存对索引节点的引用:进程可能仍然在读写文件。但是,如果在最后一个进程关闭文件的文件描述符之前发生崩溃,那么该文件将在磁盘上标记为已分配,但没有目录条目指向它。

解决方法:

  1. 在重新启动后进行恢复时,文件系统扫描整个文件系统,查找标记为已分配的文件,但没有指向它们的目录条目,释放这些文件。
  2. 文件系统在磁盘上记录链接计数降为零但引用计数不为零的文件的inode号。

directory

目录在内部的实现很像文件,索引节点类型为T_DIR,它的数据是一个目录条目序列。每个条目都是一个结构体,它包含一个最多有DIRSIZ个字符的名称和一个inode号;如果较短,则以NULL字节结束。

struct dirent {
  ushort inum;
  char name[DIRSIZ];
};

函数dirlookup通过对比dirent的name和要查找的文件name,在目录中搜索具有给定名称的条目,找到后调用iget返回一个指向相应索引节点的inode指针。

dirlookup是iget返回未锁定索引节点的原因。调用者锁定了dp,因此如果查找的是当前目录的别名,那么在返回之前试图锁定索引节点将导致死锁。调用者可以解锁dp,然后锁定ip,确保它一次只持有一个锁。

函数dirlink将一个具有给定名称和inode号的新目录条目写入目录dp。

path name

路径名查找涉及对dirlookup的一系列调用,每深入一层调用一次。

namei返回path相应的索引节点。函数nameiparent是一个变体,它在最后一个元素之前停止,返回父目录的inode并将路径的最后一项复制到name中。
二者都调用广义函数namex来完成实际工作。

namex 首先决定路径寻找从哪里开始。如果路径以/开始,则从根开始;否则,当前目录。它使用skipelem依次考虑路径中的每一项(路径为/a/b/c/d,则a、b等就是项)。循环的每次迭代都在当前索引节点ip中查找name。最后,循环使用dirlookup查找path元素,并通过设置ip = next为下一次迭代做准备。当循环用完path元素时,它返回ip。

过程namex可能需要很长时间才能完成:它可能涉及多个磁盘操作,以读取路径名中遍历的目录的inode和目录块(如果它们不在缓冲区缓存中)。x6经过精心设计,如果一个内核调用namex时在磁盘IO上阻塞,另一个查找不同路径名的内核线程可以并发地进行。

static struct inode*
namex(char *path, int nameiparent, char *name)
{
  struct inode *ip, *next;

  if(*path == '/')
    ip = iget(ROOTDEV, ROOTINO);
  else
    ip = idup(myproc()->cwd);

  while((path = skipelem(path, name)) != 0){
    ilock(ip);
    if(ip->type != T_DIR){
      iunlockput(ip);
      return 0;
    }
    if(nameiparent && *path == '\0'){
      // Stop one level early.
      iunlock(ip);
      return ip;
    }
    if((next = dirlookup(ip, name, 0)) == 0){
      iunlockput(ip);
      return 0;
    }
    iunlockput(ip);
    ip = next;
  }
  if(nameiparent){
    iput(ip);
    return 0;
  }
  return ip;
}

这种并发性带来了一些挑战。

一个潜在的风险是,查找可能正在搜索一个已被另一个内核线程删除的目录,并且其块已被另一个目录或文件重用。xv6在namex中执行dirlookup时,查找线程持有该目录上的锁,并且dirlookup返回使用iget获得的inode节点,iget增加inode节点的引用计数,只有在从dirlookup接收到inode节点之后,namex才会释放对目录的锁。现在,另一个线程可以从目录中解除索引节点的链接,但是xv6还不会删除这个索引节点,因为索引节点的引用计数仍然大于零。

另一个风险是死锁。例如,当查找.目录时,next指向与ip相同的索引节点。在释放ip上的锁之前锁定next会导致死锁。为了避免这种死锁,namex在获取next上的锁之前解锁目录。

file descriptor

Unix中的大多数资源都表示为文件,包括控制台、管道等设备以及真正的文件。文件描述符是实现这种一致性的层。
xv6为每个进程提供了自己的打开文件表。每个打开的文件都由一个struct file表示,它是一个inode或管道的包装器,加上I/O偏移量。

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与子进程共享,就会发生这种情况。引用计数跟踪对特定打开文件的引用数。readable 和 writable 记录读写方式。

系统中所有打开的文件都保存在一个全局文件表中,即ftable。文件表具有分配文件(filealloc)、创建重复引用(filedup)、释放引用(fileclose)以及读取和写入数据(fileread和filewrite)的函数。

  • filealloc 扫描文件表中未引用的文件(f->ref == 0)并返回一个新的引用
// Allocate a file structure.
struct file*
filealloc(void)
{
  struct file *f;

  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);
  return 0;
}
  • filedup (kernel/file.c:48)增加引用计数
  • filclose (kernel/file.c:60)将其递减。当文件的引用计数达到零时,filclose根据类型释放底层管道或inode。
// Close file f.  (Decrement ref count, close when reaches 0.)
void
fileclose(struct file *f)
{
  struct file ff;

  acquire(&ftable.lock);
  if(f->ref < 1)
    panic("fileclose");
  if(--f->ref > 0){
    release(&ftable.lock);
    return;
  }
  ff = *f;
  f->ref = 0;
  f->type = FD_NONE;
  release(&ftable.lock);

  if(ff.type == FD_PIPE){
    pipeclose(ff.pipe, ff.writable);
  } else if(ff.type == FD_INODE || ff.type == FD_DEVICE){
    begin_op();
    iput(ff.ip);
    end_op();
  }
}

函数filestat、fileread和filewrite实现了对文件的统计、读取和写入操作。filestat 只允许在inode上调用stati。fileread 和 filewrite 检查打开模式是否允许该操作,然后将调用传递给管道或inode实现。
inode函数要求调用者处理锁,inode锁定有一个方便的副作用,即读取和写入偏移量会自动更新,因此同时对同一文件的多次写入不会覆盖彼此的数据,尽管它们的写入可能会交错进行。

// fileread
else if(f->type == FD_INODE){
    ilock(f->ip);
    if((r = readi(f->ip, 1, addr, f->off, n)) > 0)
      f->off += r;
    iunlock(f->ip);
}

system call

sys_link 首先获取它的参数,两个字符串old和new。假设old存在并且不是目录, sys_link将其ip->nlink计数递增。然后sys_link调用nameiparent来查找new 的父目录和最后一个路径项,并创建一个指向old的索引节点的新目录条目。新的父目录必须存在,并且与现有的inode位于同一设备上:inode编号在单个磁盘上只有唯一的含义。

在没有事务的情况下,在创建链接之前更新ip->nlink将使文件系统暂时处于不安全状态,并且两者之间的崩溃可能导致严重破坏。

// Create the path new as a link to the same inode as old.
uint64
sys_link(void)
{
  char name[DIRSIZ], new[MAXPATH], old[MAXPATH];
  struct inode *dp, *ip;

  if(argstr(0, old, MAXPATH) < 0 || argstr(1, new, MAXPATH) < 0)
    return -1;

  begin_op();
  if((ip = namei(old)) == 0){
    end_op();
    return -1;
  }

  ilock(ip);
  if(ip->type == T_DIR){
    iunlockput(ip);
    end_op();
    return -1;
  }

  ip->nlink++;
  iupdate(ip);
  iunlock(ip);

  if((dp = nameiparent(new, name)) == 0)
    goto bad;
  ilock(dp);
  if(dp->dev != ip->dev || dirlink(dp, name, ip->inum) < 0){
    iunlockput(dp);
    goto bad;
  }
  iunlockput(dp);
  iput(ip);

  end_op();

  return 0;

bad:
  ilock(ip);
  ip->nlink--;
  iupdate(ip);
  iunlockput(ip);
  end_op();
  return -1;
}

create

sys_link为现有inode创建一个新名称,而create为新的索引节点创建一个新名称。它是三种文件创建系统调用共同使用的函数:

  • 使用O_CREATE标志打开创建一个新的普通文件
  • mkdir创建一个新目录
  • mkdev创建一个新的设备文件。与sys_link一样,create首先调用nameiparent来获取父目录的inode,然后调用dirlookup来检查名称是否已经存在。如果该名称确实存在,create的行为取决于它被用于哪个系统调用:如果create被用于代表open (type == T_FILE),并且存在的名称本身是一个常规文件,那么成功,否则错误。如果名称不存在,create现在使用ialloc分配一个新的索引节点。如果新的inode是一个目录,初始化它的.和..条目,并在父目录创建条目,并增加父目录文件的nlink(子目录的..指向它)。
static struct inode*
create(char *path, short type, short major, short minor)
{
  struct inode *ip, *dp;
  char name[DIRSIZ];

  if((dp = nameiparent(path, name)) == 0)
    return 0;

  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((ip = ialloc(dp->dev, type)) == 0){
    iunlockput(dp);
    return 0;
  }

  ilock(ip);
  ip->major = major;
  ip->minor = minor;
  ip->nlink = 1;
  iupdate(ip);

  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)
    goto fail;

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

  iunlockput(dp);

  return ip;

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

以上描述完也就真正知道了文件描述符就是进程打开文件表ofile的索引,和打开文件指针本质上是等价的。
打开文件时,先在系统打开文件表找一个空位置返回,然后在进程打开文件表找一个空位置填入,并返回这个位置的索引,也就是文件描述符。

real world

实际操作系统中的缓冲区缓存要比xv6的复杂得多,但它有两个相同的目的:缓存和同步对磁盘的访问。xv6的缓冲缓存使用了LRU策略,一个更有效的LRU缓存将消除链表,而是使用哈希表进行查找,并使用堆进行LRU驱逐。现代缓冲缓存通常与虚拟内存系统集成,以支持内存映射文件。

xv6的日志系统效率很低。提交不能和文件系统系统调用并发发生。系统日志记录整个块,即使块中只有几个字节被更改。它执行同步日志写操作,每次写一个块都可能需要整个磁盘旋转时间。

日志记录并不是提供崩溃恢复的唯一方法。早期的文件系统在重新启动期间使用清除程序检查每个文件和目录以及块和索引节点空闲列表,查找和解决不一致。对于大型文件系统,清理可能需要数小时。

xv6使用与早期UNIX相同的索引节点和目录的基本磁盘布局,BSD的UFS/FFS和Linux的ext2/ext3基本上使用相同的数据结构。文件系统布局中效率最低的部分是目录,每次查找需要对所有磁盘块进行线性扫描。Microsoft Windows的NTFS、macOS的HFS和Solaris的ZFS,将目录实现为平衡树,保证了对数时间的目录查找。

现代Unix系统允许使用与磁盘存储相同的系统调用访问多种资源:命名管道、网络连接、远程访问的网络文件系统以及监视和控制接口(如/proc)。与xv6在fileread和filewrite中的if语句不同,这些系统通常为每个打开的文件提供一个函数指针表,并调用函数指针来调用该inode的调用实现。网络文件系统和用户级文件系统提供了将这些调用转换为网络rpc并在返回之前等待响应的函数(什么意思?)。

bigfile实验还算好些,直接对着格式超就行了。必须把NDIRECT改为11,然后addr数组大小改为NDIRECT+2。

// bmap
bn -= NINDIRECT;
if(bn < TWOINDIRECT){
  if((addr = ip->addrs[NDIRECT+1]) == 0){
    addr = balloc(ip->dev);
    if(addr == 0)
      return 0;
    ip->addrs[NDIRECT+1] = addr;
  }

  bp = bread(ip->dev, addr);
  a = (uint*)bp->data;
  uint bias = bn % NINDIRECT;
  uint bidx = bn / NINDIRECT;
  if((addr = a[bidx]) == 0){
    addr = balloc(ip->dev);
    if(addr == 0) 
      return 0;
    a[bidx] = addr;
    log_write(bp);
  }
  brelse(bp);

  bp = bread(ip->dev, addr);
  a = (uint*)bp->data;
  if((addr = a[bias]) == 0){
    addr = balloc(ip->dev);
    if(addr == 0) 
      return 0;
    a[bias] = addr;
    log_write(bp);
  }
  brelse(bp);
  return addr;
}

// itrunc
if(ip->addrs[NDIRECT+1]){
  bp = bread(ip->dev, ip->addrs[NDIRECT+1]);
  a = (uint*)bp->data;
  struct buf* bpb;
  uint* b;
  for(i = 0; i < NINDIRECT; i++){
    if(a[i]){
      bpb = bread(ip->dev, a[i]);
      b = (uint*)bpb->data;
      for(j = 0; j < NINDIRECT; j++){
        if(b[j])
          bfree(ip->dev, b[j]);
      }
      brelse(bpb);
      bfree(ip->dev, a[i]);
      a[i] = 0;
    }
  }
  brelse(bp);
  bfree(ip->dev, ip->addrs[NDIRECT+1]);
  ip->addrs[NDIRECT+1] = 0;
}

sys_link本质上两个目录条目指向了了同一个inode
sys_symlink本质上是一个目录条目指向了另一个目录条目,而这个目录条目指向了inode

有点折磨,思路很简单,但读文档读了半天,这么多函数调用读了半天,这部分函数来回跳跃。
需要使用create新建一个文件(其实是在磁盘上写了一个inode),把指向的路径填写到文件数据里,根据O_NOFOLLOW判断是读文件本身还是指向的文件。
namei查找路径对应的inode时,返回的是未带锁的inode指针,由于使用的dirlookup调用iget,所以需要使用iunlockput解锁。

uint64
sys_symlink(void)
{
  char path[MAXPATH];
  char target[MAXPATH];
  struct inode *ip;

  if(argstr(1, path, MAXPATH) < 0 || argstr(0, target, MAXPATH) < 0){
    return -1;
  }
  begin_op();
  if((ip = namei(target))){
    ilock(ip);
    if(ip->type == T_DIR){
      iunlockput(ip);
      end_op();
      return -1;
    }

    iupdate(ip);
    iunlockput(ip);
  }

  if((ip = create(path, T_SYMLINK, 0, 0)) == 0){
    end_op();
    return -1;
  }

  if(writei(ip, 0, (uint64)target, 0, MAXPATH) != MAXPATH){
    iunlockput(ip);
    end_op();
    return -1;
  }
  iunlockput(ip);
  end_op();
  return 0;
}



// sys_open
if((omode & O_NOFOLLOW) == 0){
  int deps = 0;
  while(ip->type == T_SYMLINK){
    readi(ip,0,(uint64)path,0,MAXPATH);
    iunlockput(ip);
    if((ip = namei(path)) == 0){
      end_op();
      return -1;
    }
    deps++;
    if(deps > 10){
      end_op();
      return -1;
    }
    ilock(ip);
  }
}

看文档写实验快吐了,用了一天多(本来文件系统其实知道的就不多,但看完感觉还是有点糊涂),还好最后一个实验了,mmap等等再看吧,要补课内实验作业了。