MIT 6.S081 教材第八章内容 -- 文件系统 -- 02

这篇具有很好参考价值的文章主要介绍了MIT 6.S081 教材第八章内容 -- 文件系统 -- 02。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。


引言

MIT 6.S081 2020 操作系统

本文为MIT 6.S081课程第八章教材内容翻译加整理。

本课程前置知识主要涉及:

  • C语言(建议阅读C程序语言设计—第二版)
  • RISC-V汇编
  • 推荐阅读: 程序员的自我修养-装载,链接与库

索引结点层

术语inode(即索引结点)可以具有两种相关含义之一。它可能是指包含文件大小和数据块编号列表的磁盘上的数据结构。或者“inode”可能指内存中的inode,它包含磁盘上inode的副本以及内核中所需的额外信息。

磁盘上的inode都被打包到一个称为inode块的连续磁盘区域中。每个inode的大小都相同,因此在给定数字n的情况下,很容易在磁盘上找到第n个inode。事实上,这个编号n,称为inode number或i-number,是在具体实现中标识inode的方式。

磁盘上的inode由struct dinodekernel/fs.h:32)定义:

  • 字段type区分文件、目录和特殊文件(设备)。type为零表示磁盘inode是空闲的。
  • 字段nlink统计引用此inode的目录条目数,以便识别何时应释放磁盘上的inode及其数据块。
  • 字段size记录文件中内容的字节数。
  • addrs数组记录保存文件内容的磁盘块的块号。
// 直接块数量
#define NDIRECT 12
// 间接块数量
#define NINDIRECT (BSIZE / sizeof(uint))
// 最大的文件大小
#define MAXFILE (NDIRECT + NINDIRECT)

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

内核将活动的inode集合保存在内存中;struct inodekernel/file.h:17)是磁盘上struct dinode的内存副本。只有当有C指针引用某个inode时,内核才会在内存中存储该inode。ref字段统计引用内存中inode的C指针的数量,如果引用计数降至零,内核将从内存中丢弃该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];
};

MIT 6.S081 教材第八章内容 -- 文件系统 -- 02,# MIT 6.S081,linux

igetiput函数分别获取和释放指向inode的指针,修改引用计数。指向inode的指针可以来自文件描述符、当前工作目录和如exec的瞬态内核代码。

xv6的inode代码中有四种锁或类似锁的机制。icache.lock保护以下两个不变量:

  • inode最多在缓存中出现一次;
  • 缓存inode的ref字段记录指向缓存inode的内存指针数量。
struct {
  struct spinlock lock;
  struct inode inode[NINODE];
} icache;

每个内存中的inode都有一个包含睡眠锁的lock字段,它确保以独占方式访问inode的字段(如文件长度)以及inode的文件或目录内容块。如果inode的ref大于零,则会导致系统在cache中维护inode,而不会对其他inode重用此缓存项。

最后,每个inode都包含一个nlink字段(在磁盘上,如果已缓存则复制到内存中),该字段统计引用文件的目录项的数量;如果inode的链接计数大于零,xv6将不会释放inode。

iget()返回的struct inode指针在相应的iput()调用之前保证有效:

  • inode不会被删除,指针引用的内存也不会被其他inode重用。

iget()提供对inode的非独占访问,因此可以有许多指向同一inode的指针。文件系统代码的许多部分都依赖于iget()的这种行为,既可以保存对inode的长期引用(如打开的文件和当前目录),也可以防止争用,同时避免操纵多个inode(如路径名查找)的代码产生死锁。

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

inode缓存只缓存内核代码或数据结构持有C指针的inode。它的主要工作实际上是同步多个进程的访问;缓存是次要的。如果经常使用inode,在inode缓存不保留它的情况下buffer cache可能会将其保留在内存中。inode缓存是直写的,这意味着修改已缓存inode的代码必须立即使用iupdate将其写入磁盘。

一旦内存中的inode被修改,必须立即将修改同步到磁盘上的dinode中


代码:Inodes

为了分配新的inode(例如,在创建文件时),xv6调用iallockernel/fs.c:196)。Ialloc类似于balloc:它一次一个块地遍历磁盘上的索引节点结构体,查找标记为空闲的一个。当它找到一个时,它通过将新type写入磁盘来声明它,然后末尾通过调用igetkernel/fs.c:210)从inode缓存返回一个条目。ialloc的正确操作取决于这样一个事实:一次只有一个进程可以保存对bp的引用:ialloc可以确保其他进程不会同时看到inode可用并尝试声明它。

// Inodes per block.  --> 一个block可以塞下多少个inode信息
#define IPB           (BSIZE / sizeof(struct dinode))
// Block containing inode i  --> 计算当前inode所在的block编号
#define IBLOCK(i, sb)     ((i) / IPB + sb.inodestart)

// Allocate an inode on device dev.
// Mark it as allocated by  giving it type type.
// Returns an unlocked but allocated and referenced inode.
struct inode*
ialloc(uint dev, short type)
{
  int inum;
  struct buf *bp;
  struct dinode *dip;
  // 遍历所有dinodes
  for(inum = 1; inum < sb.ninodes; inum++){
    // 获取当前dinode所在的block 
    bp = bread(dev, IBLOCK(inum, sb));
    // 获取block上存储的dinode结构体信息
    dip = (struct dinode*)bp->data + inum%IPB;
    // 空闲inode
    if(dip->type == 0){  // a free inode
      // 清空当前dip结构体数据
      memset(dip, 0, sizeof(*dip));
      // 设置当前inode的type
      dip->type = type;
      // 当前block块标记为脏块加入log block日志记录数组中
      log_write(bp);   // mark it allocated on the disk
      brelse(bp);
      // 返回当前dinode在inode cache的映射
      return iget(dev, inum);
    }
    brelse(bp);
  }
  panic("ialloc: no inodes");
}

Igetkernel/fs.c:243)在inode缓存中查找具有所需设备和inode编号的活动条目(ip->ref > 0)。如果找到一个,它将返回对该incode的新引用(kernel/fs.c:252-256)。在iget扫描时,它会记录第一个空槽(kernel/fs.c:257-258)的位置,如果需要分配缓存项,它会使用这个槽。

// Find the inode with number inum on device dev
// and return the in-memory copy. Does not lock
// the inode and does not read it from disk.
static struct inode*
iget(uint dev, uint inum)
{
  struct inode *ip, *empty;

  acquire(&icache.lock);

  // Is the inode already cached?
  empty = 0;
  // 检查当前inode cache中是否已经缓存当前inode,如果遍历完发现没有缓存
  // 那么empty会拿到一个空的entry用于存放当前我们要查找的inode
  for(ip = &icache.inode[0]; ip < &icache.inode[NINODE]; ip++){
    if(ip->ref > 0 && ip->dev == dev && ip->inum == inum){
      ip->ref++;
      release(&icache.lock);
      return ip;
    }
    // 记录第一个出现的空槽
    if(empty == 0 && ip->ref == 0)    // Remember empty slot.
      empty = ip;
  }

  // Recycle an inode cache entry.
  // 说明inode cache爆满--抛出异常
  if(empty == 0)
    panic("iget: no inodes");
  // 设置valid字段为0,表示用到的时候需要从磁盘加载inode的具体数据
  ip = empty;
  ip->dev = dev;
  ip->inum = inum;
  ip->ref = 1;
  ip->valid = 0;
  release(&icache.lock);

  return ip;
}

在读取或写入inode的元数据或内容之前,代码必须使用ilock锁定inode。Ilock(kernel/fs.c:289)为此使用睡眠锁。一旦ilock以独占方式访问inode,它将根据需要从磁盘(更可能是buffer cache)读取inode。函数iunlockkernel/fs.c:317)释放睡眠锁,这可能会导致任何睡眠进程被唤醒。

// Lock the given inode.
// Reads the inode from disk if necessary.
void
ilock(struct inode *ip)
{
  struct buf *bp;
  struct dinode *dip;
  // inode状态异常
  if(ip == 0 || ip->ref < 1)
    panic("ilock");

  acquiresleep(&ip->lock);
  // 如果当前inode状态为无效,那么先从磁盘加载该inode信息
  if(ip->valid == 0){
    // 从磁盘读取该inode所在的block
    bp = bread(ip->dev, IBLOCK(ip->inum, sb));
    // 获取磁盘上的dinode信息,赋值给inode
    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);
    // 设置当前inode为有效状态
    ip->valid = 1;
    if(ip->type == 0)
      panic("ilock: no type");
  }
}

Iputkernel/fs.c:333)通过减少引用计数(kernel/fs.c:356)释放指向inode的C指针。如果这是最后一次引用,inode缓存中该inode的槽现在将是空闲的,可以重用于其他inode。

如果iput发现没有指向inode的C指针引用,并且inode没有指向它的链接(发生于无目录),则必须释放inode及其数据块。Iput调用itrunc将文件截断为零字节,释放数据块;将索引节点类型设置为0(未分配);并将inode写入磁盘(kernel/fs.c:338)。

// Drop a reference to an in-memory inode.
// If that was the last reference, the inode cache entry can
// be recycled.
// If that was the last reference and the inode has no links
// to it, free the inode (and its content) on disk.
// All calls to iput() must be inside a transaction in
// case it has to free the inode.
//.
void
iput(struct inode *ip)
{
  acquire(&icache.lock);
  // 如果当前Inode的引用计数为1,并且inode有效,且nlink数为0,则释放当前inode,及其绑定的data block
  if(ip->ref == 1 && ip->valid && ip->nlink == 0){
    // inode has no links and no other references: truncate and free.

    // ip->ref == 1 means no other process can have ip locked,
    // so this acquiresleep() won't block (or deadlock).
    acquiresleep(&ip->lock);

    release(&icache.lock);
    // 释放数据块
    itrunc(ip);
    ip->type = 0;
    iupdate(ip);
    ip->valid = 0;

    releasesleep(&ip->lock);

    acquire(&icache.lock);
  }

  ip->ref--;
  release(&icache.lock);
}

// Truncate inode (discard contents).
// Caller must hold ip->lock.
// 释放当前Inode绑定的所有data block
void
itrunc(struct inode *ip)
{
  int i, j;
  struct buf *bp;
  uint *a;
  // 遍历所有直接块
  for(i = 0; i < NDIRECT; i++){
    // 将已经分配的直接块全部释放
    if(ip->addrs[i]){
      bfree(ip->dev, ip->addrs[i]);
      ip->addrs[i] = 0;
    }
  }
  // 如果间接块也分配了,那么释放间接块使用的所有磁盘空间
  if(ip->addrs[NDIRECT]){
    bp = bread(ip->dev, ip->addrs[NDIRECT]);
    a = (uint*)bp->data;
    // 释放所有已经分配的间接块
    for(j = 0; j < NINDIRECT; j++){
      if(a[j])
        // 修改位图block信息即可
        bfree(ip->dev, a[j]);
    }
    brelse(bp);
    // 释放完所有间接块,再释放存放间接块指针的当前block
    bfree(ip->dev, ip->addrs[NDIRECT]);
    ip->addrs[NDIRECT] = 0;
  }

  ip->size = 0;
  iupdate(ip);
}

// Copy a modified in-memory inode to disk.
// Must be called after every change to an ip->xxx field
// that lives on disk, since i-node cache is write-through.
// Caller must hold ip->lock.
// 将内存中修改后的inode信息持久化到磁盘中存储
void
iupdate(struct inode *ip)
{
  struct buf *bp;
  struct dinode *dip;
  // 读取当前inode对应的block,将内存中inode信息赋值给磁盘上dinode结构体
  bp = bread(ip->dev, IBLOCK(ip->inum, sb));
  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));
  // 记录当前修改后的block到日志中去
  log_write(bp);
  brelse(bp);
}

iput中释放inode的锁定协议值得仔细研究:

  • 一个危险是并发线程可能正在ilock中等待使用该inode(例如,读取文件或列出目录),并且不会做好该inode已不再被分配的准备。这不可能发生,因为如果缓存的inode没有链接,并且ip->ref为1,那么系统调用就无法获取指向该inode的指针。那一个引用是调用iput的线程所拥有的引用。的确,iputicache.lock的临界区域之外检查引用计数是否为1,但此时已知链接计数为零,因此没有线程会尝试获取新引用。

iput用于释放当前inode,释放条件是引用计数为1,并且link链接数为0,因为链接数为0,所以此时不可能存在其他线程等待使用该inode.

  • 另一个主要危险是,对ialloc的并发调用可能会选择iput正在释放的同一个inode。这只能在iupdate写入磁盘以使inode的type为零后发生。这个争用是良性的:分配线程将客气地等待获取inode的睡眠锁,然后再读取或写入inode,此时iput已完成。

iput()可以写入磁盘。这意味着任何使用文件系统的系统调用都可能写入磁盘,因为系统调用可能是最后一个引用该文件的系统调用。即使像read()这样看起来是只读的调用,也可能最终调用iput()。这反过来意味着,即使是只读系统调用,如果它们使用文件系统,也必须在事务中进行包装。

iput()和崩溃之间存在一种具有挑战性的交互。iput()不会在文件的链接计数降至零时立即截断文件,因为某些进程可能仍在内存中保留对inode的引用:

  • 进程可能仍在读取和写入该文件,因为它已成功打开该文件。

虽然某个inode的link数为0了,但是考虑到ref数大于1,可能会存在其他进程通过文件描述符进行数据读取

但是,如果在最后一个进程关闭该文件的文件描述符之前发生崩溃,则该文件将被标记为已在磁盘上分配,但没有目录项指向它。

当我们进行link操作时,会在当前文件的父目录下创建一个新的目录项<新文件名,inode>,因此如果最后一个进程unlink这个文件后,但是在iput该inode时奔溃,会导致文件已分配(bitmap还未修改),但是没有目录项指向他。

文件系统以两种方式之一处理这种情况。

  • 简单的解决方案用于恢复时:重新启动后,文件系统会扫描整个文件系统,以查找标记为已分配但没有指向它们的目录项的文件。如果存在任何此类文件,接下来可以将其释放。
  • 第二种解决方案不需要扫描文件系统。在此解决方案中,文件系统在磁盘(例如在超级块中)上记录链接计数降至零但引用计数不为零的文件的i-number。如果文件系统在其引用计数达到0时删除该文件,则会通过从列表中删除该inode来更新磁盘列表。恢复时,文件系统将释放列表中的任何文件。

Xv6没有实现这两种解决方案,这意味着inode可能被标记为已在磁盘上分配,即使它们不再使用。这意味着随着时间的推移,xv6可能会面临磁盘空间不足的风险。


代码: Inode包含内容

磁盘上的inode结构体struct dinode包含一个size和一个块号数组(见图8.3)。inode数据可以在dinodeaddrs数组列出的块中找到。前面的NDIRECT个数据块被列在数组中的前NDIRECT个元素中;这些块称为直接块(direct blocks)。接下来的NINDIRECT个数据块不在inode中列出,而是在称为间接块(indirect block)的数据块中列出。addrs数组中的最后一个元素给出了间接块的地址。因此,可以从inode中列出的块加载文件的前12 kB(NDIRECT x BSIZE)字节,而只有在查阅间接块后才能加载下一个256 kB(NINDIRECT x BSIZE)字节。这是一个很好的磁盘表示,但对于客户端来说较复杂。函数bmap管理这种表示,以便实现我们将很快看到的如readiwritei这样的更高级例程。bmap(struct inode *ip, uint bn)返回索引结点ip的第bn个数据块的磁盘块号。如果ip还没有这样的块,bmap会分配一个。

MIT 6.S081 教材第八章内容 -- 文件系统 -- 02,# MIT 6.S081,linux
函数bmapkernel/fs.c:378)从简单的情况开始:前面的NDIRECT个块在inode本身中列出(kernel/fs.c:383-387)中。下面NINDIRECT个块在ip->addrs[NDIRECT]的间接块中列出。Bmap读取间接块(kernel/fs.c:394),然后从块内的正确位置(kernel/fs.c:395)读取块号。如果块号超过NDIRECT+NINDIRECT,则bmap调用panic崩溃;writei包含防止这种情况发生的检查(kernel/fs.c:490)。

Bmap根据需要分配块。ip->addrs[]或间接块中条目为零表示未分配块。当bmap遇到零时,它会用按需分配的新块(kernel/fs.c:384-385)(kernel/fs.c:392-393)替换它们。

// Return the disk block address of the nth block in inode ip.
// If there is no such block, bmap allocates one.
// 返回inode中第n个block在磁盘中的块号,如果还没分配就进行分配
static uint
bmap(struct inode *ip, uint bn)
{
  uint addr, *a;
  struct buf *bp;
  // 如果第n个block属于直接块范畴
  if(bn < NDIRECT){
    // 该block还未分配空间
    if((addr = ip->addrs[bn]) == 0)
      // 分配一个新的磁盘块,并返回block number
      ip->addrs[bn] = addr = balloc(ip->dev);
    return addr;
  } 
  bn -= NDIRECT;
  // 如果是间接块范围
  if(bn < NINDIRECT){
    // Load indirect block, allocating if necessary.
    // 间接块空间还没有分配
    if((addr = ip->addrs[NDIRECT]) == 0)
      // 分配新的磁盘块,并返回对应的block number 
      ip->addrs[NDIRECT] = addr = balloc(ip->dev);
    // 从磁盘读取该block
    bp = bread(ip->dev, addr);
    a = (uint*)bp->data;
    // 查看间接块第n个条目对应的block是否已经分配了
    // 如果分配了,那么就进行分配
    if((addr = a[bn]) == 0){
      a[bn] = addr = balloc(ip->dev);
      // 因为修改当前间接块的内容,所以需要将当前block记录到日志中去
      log_write(bp);
    }
    brelse(bp);
    return addr;
  }

  panic("bmap: out of range");
}

itrunc释放文件的块,将inode的size重置为零。Itrunckernel/fs.c:410)首先释放直接块(kernel/fs.c:416-421),然后释放间接块中列出的块(kernel/fs.c:426-429),最后释放间接块本身(kernel/fs.c:431-432)。

// Truncate inode (discard contents).
// Caller must hold ip->lock.
void
itrunc(struct inode *ip)
{
  int i, j;
  struct buf *bp;
  uint *a;
  // 释放所有直接块
  for(i = 0; i < NDIRECT; i++){
    if(ip->addrs[i]){
      bfree(ip->dev, ip->addrs[i]);
      ip->addrs[i] = 0;
    }
  }
  // 间接块已经分配了
  if(ip->addrs[NDIRECT]){
    bp = bread(ip->dev, ip->addrs[NDIRECT]);
    a = (uint*)bp->data;
    // 先释放间接块指向的所有块
    for(j = 0; j < NINDIRECT; j++){
      if(a[j])
        bfree(ip->dev, a[j]);
    }
    brelse(bp);
    // 再释放间接块本身
    bfree(ip->dev, ip->addrs[NDIRECT]);
    ip->addrs[NDIRECT] = 0;
  }

  ip->size = 0;
  // 更新inode块到磁盘
  iupdate(ip);
}

Bmap使readiwritei很容易获取inode的数据。Readikernel/fs.c:456)首先确保偏移量和计数不超过文件的末尾。开始于超过文件末尾的地方读取将返回错误(kernel/fs.c:461-462),而从文件末尾开始或穿过文件末尾的读取返回的字节数少于请求的字节数(kernel/fs.c:463-464)。主循环处理文件的每个块,将数据从缓冲区复制到dstkernel/fs.c:466-474)。

// Read data from inode.
// Caller must hold ip->lock.
// If user_dst==1, then dst is a user virtual address;
// otherwise, dst is a kernel address.
// 从block中读取数据到用户缓冲区,参数二表示是否为用户态虚拟地址
int
readi(struct inode *ip, int user_dst, uint64 dst, uint off, uint n)
{
  uint tot, m;
  struct buf *bp;
  // 偏移量是否超过文件大小
  if(off > ip->size || off + n < off)
    return 0;
  // 如果期望读取的数据量比文件off偏移量起始的剩余字节数还多,那么缩减期望读取的数据量大小  
  if(off + n > ip->size)
    n = ip->size - off;
  // tot表示当前已经读取的字节数,off是读取偏移指针,dst是用户态缓存区偏移指针
  for(tot=0; tot<n; tot+=m, off+=m, dst+=m){
    // 从磁盘读取当前偏移量所在的block块
    bp = bread(ip->dev, bmap(ip, off/BSIZE));
    // min(剩余未读取字节数,当前block剩余空间)
    m = min(n - tot, BSIZE - off%BSIZE);
    //  从block拷贝数据到用户态缓冲区地址,数据长度为n
    if(either_copyout(user_dst, dst, bp->data + (off % BSIZE), m) == -1) {
      brelse(bp);
      tot = -1;
      break;
    }
    brelse(bp);
  }
  return tot;
}

writeikernel/fs.c:483)与readi相同,但有三个例外:从文件末尾开始或穿过文件末尾的写操作会使文件增长到最大文件大小(kernel/fs.c:490-491);循环将数据复制到缓冲区而不是输出(kernel/fs.c:36);如果写入扩展了文件,writei必须更新其大小(kernel/fs.c:504-511)。

// Write data to inode.
// Caller must hold ip->lock.
// If user_src==1, then src is a user virtual address;
// otherwise, src is a kernel address.
int
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;
  // tot代表写入数据量,off是文件写入偏移指针,src代表用户态缓冲区写入偏移指针
  for(tot=0; tot<n; tot+=m, off+=m, src+=m){
    // 从磁盘读取待写入的block到buf cache
    bp = bread(ip->dev, bmap(ip, off/BSIZE));
    // min(剩余待写入数据量,当前block剩余空闲空间)
    m = min(n - tot, BSIZE - off%BSIZE);
    // 从用户缓冲区向block写入数据
    if(either_copyin(bp->data + (off % BSIZE), user_src, src, m) == -1) {
      brelse(bp);
      n = -1;
      break;
    }
    // 当前block被修改了,需要记录日志
    log_write(bp);
    brelse(bp);
  }

  if(n > 0){
    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[].
    // 更新inode所在block 
    iupdate(ip);
  }

  return n;
}

readiwritei都是从检查ip->type == T_DEV开始的。这种情况处理的是数据不在文件系统中的特殊设备;我们将在文件描述符层返回到这种情况。

函数statikernel/fs.c:442)将inode元数据复制到stat结构体中,该结构通过stat系统调用向用户程序公开。

// Copy stat information from inode.
// Caller must hold ip->lock.
void
stati(struct inode *ip, struct stat *st)
{
  st->dev = ip->dev;
  st->ino = ip->inum;
  st->type = ip->type;
  st->nlink = ip->nlink;
  st->size = ip->size;
}

代码:目录层

目录的内部实现很像文件。其inode的typeT_DIR,其数据是一系列目录条目(directory entries)。每个条目(entry)都是一个struct direntkernel/fs.h:56),其中包含一个名称name和一个inode编号inum。名称最多为DIRSIZ(14)个字符;如果较短,则以NUL(0)字节终止。inode编号为零的条目是空的。

// Directory is a file containing a sequence of dirent structures.
#define DIRSIZ 14

//目录条目
struct dirent {
  ushort inum;
  char name[DIRSIZ];
};

函数dirlookupkernel/fs.c:527)在目录中搜索具有给定名称的条目。如果找到一个,它将返回一个指向相应inode的指针,解开锁定,并将*poff设置为目录中条目的字节偏移量,以满足调用方希望对其进行编辑的情形。如果dirlookup找到具有正确名称的条目,它将更新*poff并返回通过iget获得的未锁定的inode。Dirlookupiget返回未锁定indoe的原因。调用者已锁定dp,因此,如果对.,当前目录的别名,进行查找,则在返回之前尝试锁定indoe将导致重新锁定dp并产生死锁(还有更复杂的死锁场景,涉及多个进程和..,父目录的别名。.不是唯一的问题。)调用者可以解锁dp,然后锁定ip,确保它一次只持有一个锁 :

// Look for a directory entry in a directory.
// If found, set *poff to byte offset of entry.
struct inode*
dirlookup(struct inode *dp, char *name, uint *poff)
{
  uint off, inum;
  struct dirent de;

  if(dp->type != T_DIR)
    panic("dirlookup not DIR");
  // 遍历当前目录文件下每个目录项
  for(off = 0; off < dp->size; off += sizeof(de)){
    // 从当前目录下读取出目录项信息到de中
    if(readi(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
      panic("dirlookup read");
    // 当前目录项还没有分配,直接返回  
    if(de.inum == 0)
      continue;
    // 比较和目标目录项是否一致  
    if(namecmp(name, de.name) == 0){
      // entry matches path element
      // 是需要寻找的目标目录项
      if(poff)
        // 设置poff的值为off 
        *poff = off;
      // 记录目录项指向的inode numbder  
      inum = de.inum;
      // 从磁盘读取中对应的inode
      return iget(dp->dev, inum);
    }
  }
  // 没有对应的目录项
  return 0;
}

函数dirlinkkernel/fs.c:554)将给定名称和inode编号的新目录条目写入目录dp。如果名称已经存在,dirlink将返回一个错误(kernel/fs.c:560-564)。主循环读取目录条目,查找未分配的条目。当找到一个时,它会提前停止循环(kernel/fs.c:538-539),并将off设置为可用条目的偏移量。否则,循环结束时会将off设置为dp->size。无论哪种方式,dirlink都会通过在偏移off处写入(kernel/fs.c:574-577)来向目录添加一个新条目。

// Write a new directory entry (name, inum) into the directory dp.
int
dirlink(struct inode *dp, char *name, uint inum)
{
  int off;
  struct dirent de;
  struct inode *ip;

  // Check that name is not present.
  // 如果目录项存在,返回-1
  if((ip = dirlookup(dp, name, 0)) != 0){
    // iget增加了引用计数,这里iput将引用计数进行递减
    iput(ip);
    return -1;
  }

  // Look for an empty dirent.
  // 遍历每个目录项,寻找一个空的
  for(off = 0; off < dp->size; off += sizeof(de)){
    if(readi(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
      panic("dirlink read");
    if(de.inum == 0)
      break;
  }
  // 找到一个空的目录项
  strncpy(de.name, name, DIRSIZ);
  de.inum = inum;
  // 将新的目录项写入到当前目录下
  if(writei(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
    panic("dirlink");

  return 0;
}

代码:路径名

路径名查找涉及一系列对dirlookup的调用,每个路径组件调用一个。Nameikernel/fs.c:661)计算path并返回相应的inode。函数nameiparent是一个变体:它在最后一个元素之前停止,返回父目录的inode并将最后一个元素复制到name中。两者都调用通用函数namex来完成实际工作。

struct inode*
namei(char *path)
{
  char name[DIRSIZ];
  return namex(path, 0, name);
}

struct inode*
nameiparent(char *path, char *name)
{
  return namex(path, 1, name);
}

Namexkernel/fs.c:626)首先决定路径计算的开始位置:

  • 如果路径以斜线开始,则计算从根目录开始;
  • 否则,从当前目录开始(kernel/fs.c:630-633)。
  • 然后,它使用skipelem依次考察路径的每个元素(kernel/fs.c:635)。
  • 循环的每次迭代都必须在当前索引结点ip中查找name
  • 迭代首先给ip上锁并检查它是否是一个目录。
  • 如果不是,则查找失败(kernel/fs.c:636-640)(锁定ip是必要的,不是因为ip->type可以被更改,而是因为在ilock运行之前,ip->type不能保证已从磁盘加载。)
  • 如果调用是nameiparent,并且这是最后一个路径元素,则根据nameiparent的定义,循环会提前停止;
  • 最后一个路径元素已经复制到name中,因此namex只需返回解锁的ipkernel/fs.c:641-645)。
  • 最后,循环将使用dirlookup查找路径元素,并通过设置ip = nextkernel/fs.c:646-651)为下一次迭代做准备。
  • 当循环用完路径元素时,它返回ip

namex过程可能需要很长时间才能完成:它可能涉及多个磁盘操作来读取路径名中所遍历目录的索引节点和目录块(如果它们不在buffer cache中)。Xv6经过精心设计,如果一个内核线程对namex的调用在磁盘I/O上阻塞,另一个查找不同路径名的内核线程可以同时进行。Namex分别锁定路径中的每个目录,以便在不同目录中进行并行查找。

// Look up and return the inode for a path name.
// If parent != 0, return the inode for the parent and copy the final
// path element into name, which must have room for DIRSIZ bytes.
// Must be called inside a transaction since it calls iput().
static struct inode*
namex(char *path, int nameiparent, char *name)
{ 
  struct inode *ip, *next;
  // 如果查询以'/'开头,则加载根目录的inode
  if(*path == '/')
    ip = iget(ROOTDEV, ROOTINO);
  else
  // 加载当前工作目录的inode  
    ip = idup(myproc()->cwd);
  
  // 每次循环处理路径的一个元素 --> /a/b/c -> a,b,c
  while((path = skipelem(path, name)) != 0){
    // 锁住当前inode
    ilock(ip);
    // 检查当前inode的类型是否为目录,如果不是目录,则解锁并释放当前inode,并返回0。
    if(ip->type != T_DIR){
      iunlockput(ip);
      return 0;
    }
    // 如果nameiparent为真且path已经到达末尾(即最后一个路径元素),则提前结束循环,解锁当前inode,并返回当前inode
    if(nameiparent && *path == '0'){
      // Stop one level early.
      iunlock(ip);
      return ip;
    }
    // 通过调用dirlookup函数,在当前目录中查找给定名称的目录项,如果找不到,则解锁并释放当前inode,并返回0
    if((next = dirlookup(ip, name, 0)) == 0){
      iunlockput(ip);
      return 0;
    }
    iunlockput(ip);
    ip = next;
  }
  // 如果nameiparent为真,则说明要返回父目录的inode,此时释放当前inode,并返回0
  if(nameiparent){
    iput(ip);
    return 0;
  }
  //否则,返回最终的inode
  return ip;
}

// Copy the next path element from path into name.
// Return a pointer to the element following the copied one.
// The returned path has no leading slashes,
// so the caller can check *path=='\0' to see if the name is the last one.
// If no name to remove, return 0.
//
// Examples:
//   skipelem("a/bb/c", name) = "bb/c", setting name = "a"
//   skipelem("///a//bb", name) = "bb", setting name = "a"
//   skipelem("a", name) = "", setting name = "a"
//   skipelem("", name) = skipelem("", name) = 0
//
static char*
skipelem(char *path, char *name)
{
  char *s;
  int len;

  while(*path == '/')
    path++;
  if(*path == 0)
    return 0;
  s = path;
  while(*path != '/' && *path != 0)
    path++;
  len = path - s;
  if(len >= DIRSIZ)
    memmove(name, s, DIRSIZ);
  else {
    memmove(name, s, len);
    name[len] = 0;
  }
  while(*path == '/')
    path++;
  return path;
}

namx函数使用举例如下:

// 函数将返回 "file1.txt" 的inode,并将名称 "file1.txt" 存储在 'name' 缓冲区中。
char* path = "/dir1/file1.txt";
char name[DIRSIZ];
struct inode* file_inode = namex(path, 0, name);

// 函数将返回 "dir1" 的inode,并将名称 "dir1" 存储在 'name' 缓冲区中。
char* path = "/dir1";
char name[DIRSIZ];
struct inode* dir_inode = namex(path, 0, name);

// 函数将返回父目录 "dir1" 的inode,并将名称 "file1.txt" 存储在 'name' 缓冲区中。
char* path = "/dir1/file1.txt";
char name[DIRSIZ];
struct inode* parent_inode = namex(path, 1, name);

这种并发性带来了一些挑战。例如,当一个内核线程正在查找路径名时,另一个内核线程可能正在通过取消目录链接来更改目录树。一个潜在的风险是,查找可能正在搜索已被另一个内核线程删除且其块已被重新用于另一个目录或文件的目录。

Xv6避免了这种竞争。例如,在namex中执行dirlookup时,lookup线程持有目录上的锁,dirlookup返回使用iget获得的inode。Iget增加索引节点的引用计数。只有在从dirlookup接收inode之后,namex才会释放目录上的锁。现在,另一个线程可以从目录中取消inode的链接,但是xv6还不会删除inode,因为inode的引用计数仍然大于零。

另一个风险是死锁。例如,查找“.”时,next指向与ip相同的inode。在释放ip上的锁之前锁定next将导致死锁。为了避免这种死锁,namex在获得下一个目录的锁之前解锁该目录。这里我们再次看到为什么igetilock之间的分离很重要。


文件描述符层

Unix界面的一个很酷的方面是,Unix中的大多数资源都表示为文件,包括控制台、管道等设备,当然还有真实文件。文件描述符层是实现这种一致性的层。

正如我们在第1章中看到的,Xv6为每个进程提供了自己的打开文件表或文件描述符。每个打开的文件都由一个struct filekernel/file.h:1)表示,它是inode或管道的封装,加上一个I/O偏移量。每次调用open都会创建一个新的打开文件(一个新的struct file):

  • 如果多个进程独立地打开同一个文件,那么不同的实例将具有不同的I/O偏移量。
  • 另一方面,单个打开的文件(同一个struct file)可以多次出现在一个进程的文件表中,也可以出现在多个进程的文件表中。
  • 如果一个进程使用open打开文件,然后使用dup创建别名,或使用fork与子进程共享,就会发生这种情况。
  • 引用计数跟踪对特定打开文件的引用数。可以打开文件进行读取或写入,也可以同时进行读取和写入。readablewritable字段可跟踪此操作。
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
};

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

struct {
  struct spinlock lock;
  struct file file[NFILE];
} ftable;

前三个函数遵循现在熟悉的形式。

  • Fileallockernel/file.c:30)扫描文件表以查找未引用的文件(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;
}
  • filedupkernel/file.c:48)增加引用计数;
// Increment ref count for file f.
struct file*
filedup(struct file *f)
{
  acquire(&ftable.lock);
  if(f->ref < 1)
    panic("filedup");
  f->ref++;
  release(&ftable.lock);
  return f;
}
  • fileclosekernel/file.c:60)将其递减。当文件的引用计数达到零时,fileclose会根据type释放底层管道或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();
  }
}

函数filestatfilereadfilewrite实现对文件的statreadwrite操作:

  • Filestatkernel/file.c:88)只允许在inode上操作并且调用了stati
// Get metadata about file f.
// addr is a user virtual address, pointing to a struct stat.
int
filestat(struct file *f, uint64 addr)
{
  struct proc *p = myproc();
  struct stat st;
  
  if(f->type == FD_INODE || f->type == FD_DEVICE){
    ilock(f->ip);
    stati(f->ip, &st);
    iunlock(f->ip);
    if(copyout(p->pagetable, addr, (char *)&st, sizeof(st)) < 0)
      return -1;
    return 0;
  }
  return -1;
}
  • Filereadfilewrite检查打开模式是否允许该操作,然后将调用传递给管道或inode的实现。
// Read from file f.
// addr is a user virtual address.
int
fileread(struct file *f, uint64 addr, int n)
{
  int r = 0;
  // 检查文件是否可读
  if(f->readable == 0)
    return -1;

  if(f->type == FD_PIPE){
    r = piperead(f->pipe, addr, n);
  } else if(f->type == FD_DEVICE){
    if(f->major < 0 || f->major >= NDEV || !devsw[f->major].read)
      return -1;
    r = devsw[f->major].read(1, addr, n);
  } else if(f->type == FD_INODE){
    ilock(f->ip);
    // 借助inode文件元数据信息读取数据,从file->off偏移量开始读取
    if((r = readi(f->ip, 1, addr, f->off, n)) > 0)
      // 文件读取偏移量+r
      f->off += r;
    iunlock(f->ip);
  } else {
    panic("fileread");
  }

  return r;
}

// Write to file f.
// addr is a user virtual address.
// 向文件中写入数据,数据来源于用户空间某个虚拟地址,数据长度为n
int
filewrite(struct file *f, uint64 addr, int n)
{
  int r, ret = 0;
  // 文件是否可写判断
  if(f->writable == 0)
    return -1;
  // 文件类型是管道
  if(f->type == FD_PIPE){
    ret = pipewrite(f->pipe, addr, n);
  } 
  // 文件类型是设备类型
  else if(f->type == FD_DEVICE){
    if(f->major < 0 || f->major >= NDEV || !devsw[f->major].write)
      return -1;
    ret = devsw[f->major].write(1, addr, n);
  } 
  // 要写入的文件是INODE类型
  else if(f->type == FD_INODE){
    // write a few blocks at a time to avoid exceeding
    // the maximum log transaction size, including
    // i-node, indirect block, allocation blocks,
    // and 2 blocks of slop for non-aligned writes.
    // this really belongs lower down, since writei()
    // might be writing a device like the console.
    // 每一次写入的最大数据量限制
    int max = ((MAXOPBLOCKS-1-1-2) / 2) * BSIZE;
    int i = 0;
    // 由于单次写入数据量可能会超过单批次最大写入限制,因此会被拆分为多批次写入
    while(i < n){
      // 计算要写入的数据长度
      int n1 = n - i;
      // 判断是否超过单批次数据写入限制,如果超过,那么本次写入数据量变更为max
      if(n1 > max)
        n1 = max;
      // 开启日志事务操作记录
      begin_op();
      // 获取当前文件inode的lock
      ilock(f->ip);
      // 将数据写入inode,各个参数含义: 当前文件对应的inode,数据源地址是否为用户空间虚拟地址,源数据地址,写入文件的偏移量,写入数据长度
      // 返回值是实际写入的数据量
      if ((r = writei(f->ip, 1, addr + i, f->off, n1)) > 0)
        // 文件偏移量增加r
        f->off += r;  
      // 释放文件inode的lock  
      iunlock(f->ip);
      // 提交日志事务操作记录
      end_op();
      // 出现错误
      if(r < 0)
        break;
      // 实际写入的数据量与预期不符,则抛出异常  
      if(r != n1)
        panic("short filewrite");
      // 写入数据总量+r  
      i += r;
    }
    // 如果最终写入的数据量与预期不符,返回-1
    ret = (i == n ? n : -1);
  } else {
    panic("filewrite");
  }

  return ret;
}

如果文件表示inode,filereadfilewrite使用I/O偏移量作为操作的偏移量,然后将文件指针前进该偏移量(kernel/file.c:122-123)(kernel/file.c:153-154)。

管道没有偏移的概念。回想一下,inode的函数要求调用方处理锁(kernel/file.c:94-96)(kernel/file.c:121-124)(kernel/file.c:163-166)。inode锁定有一个方便的副作用,即读取和写入偏移量以原子方式更新,因此,对同一文件的同时多次写入不能覆盖彼此的数据,尽管他们的写入最终可能是交错的。


代码:系统调用

通过使用底层提供的函数,大多数系统调用的实现都很简单(请参阅kernel/sysfile.c)。有几个调用值得仔细看看。

函数sys_linksys_unlink编辑目录,创建或删除索引节点的引用。它们是使用事务能力的另一个很好的例子。sys_linkkernel/sysfile.c:120)从获取其参数开始,两个字符串分别是oldnewkernel/sysfile.c:125)。假设old存在并且不是一个目录(kernel/sysfile.c:129-132),sys_link会增加其ip->nlink计数。然后sys_link调用nameiparent来查找newkernel/sysfile.c:145)的父目录和最终路径元素,并创建一个指向old的inode(kernel/sysfile.c:148)的新目录条目。new的父目录必须存在并且与现有inode位于同一设备上:inode编号在一个磁盘上只有唯一的含义。如果出现这样的错误,sys_link必须返回并减少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;
  // 获取要link的已经存在的路径名,和link产生的新路径名
  if(argstr(0, old, MAXPATH) < 0 || argstr(1, new, MAXPATH) < 0)
    return -1;
  // 开启日志事务
  begin_op();
  // namei返回已经存在的文件的inode
  if((ip = namei(old)) == 0){
    end_op();
    return -1;
  }
  // 锁住当前old inode
  ilock(ip);
  // 如果当前要link的是目录,则直接返回,不进行操作
  if(ip->type == T_DIR){
    iunlockput(ip);
    end_op();
    return -1;
  }
  // 增加当前inode的link数量
  ip->nlink++;
  // 更新磁盘上当前Inode所在Block信息
  iupdate(ip);
  iunlock(ip);
  // 获取当前new文件的父目录,如果不存在则有问题
  if((dp = nameiparent(new, name)) == 0)
    goto bad;
  ilock(dp);
  // 父目录设备号必须和子文件一样,在当前目录下新增一个目录项,名字为新的文件名,inode编号和old文件一样
  if(dp->dev != ip->dev || dirlink(dp, name, ip->inum) < 0){
    iunlockput(dp);
    goto bad;
  }
  // 递减dp和ip两个inode的引用计数
  iunlockput(dp);
  iput(ip);
  // 日志事务修改落盘
  end_op();

  return 0;

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

事务简化了实现,因为它需要更新多个磁盘块,但我们不必担心更新的顺序。他们要么全部成功,要么什么都不做。例如在没有事务的情况下,在创建一个链接之前更新ip->nlink会使文件系统暂时处于不安全状态,而在这两者之间发生的崩溃可能会造成严重破坏。对于事务,我们不必担心这一点

Sys_link为现有inode创建一个新名称。

link增加了对应inode的link数,同时在对应父目录下新增一条目录项

函数createkernel/sysfile.c:242)为新inode创建一个新名称。它是三个文件创建系统调用的泛化:带有O_CREATE标志的open生成一个新的普通文件,mkdir生成一个新目录,mkdev生成一个新的设备文件。

sys_link一样,create从调用nameiparent开始,以获取父目录的inode。然后调用dirlookup检查名称是否已经存在(kernel/sysfile.c:252)。如果名称确实存在,create的行为取决于它用于哪个系统调用:

  • open的语义与mkdirmkdev不同。
  • 如果create是代表opentype == T_FILE)使用的,并且存在的名称本身是一个常规文件,那么open会将其视为成功,create也会这样做(kernel/sysfile.c:256)。否则,这是一个错误(kernel/sysfile.c:257-258)。
  • 如果名称不存在,create现在将使用iallockernel/sysfile.c:261)分配一个新的inode。如果新inode是目录,create将使用...条目对它进行初始化。
  • 最后,既然数据已正确初始化,create可以将其链接到父目录(kernel/sysfile.c:274)。
  • Createsys_link一样,同时持有两个inode锁:ipdp。不存在死锁的可能性,因为索引结点ip是新分配的:系统中没有其他进程会持有ip的锁,然后尝试锁定dp
static struct inode*
create(char *path, short type, short major, short minor)
{
  struct inode *ip, *dp;
  char name[DIRSIZ];
  // path=/home/dhy.txt --> name被赋值为dhy.txt,同时dp= home inode
  if((dp = nameiparent(path, name)) == 0)
    return 0;

  ilock(dp);
  // 在当前目录下寻找名为dhy.txt的目录项
  if((ip = dirlookup(dp, name, 0)) != 0){
    iunlockput(dp);
    ilock(ip);
    // 如果存在,并且type类型为文件,同时inode的类型为文件或者设备,直接返回当前inode
    if(type == T_FILE && (ip->type == T_FILE || ip->type == T_DEVICE))
      return ip;
    iunlockput(ip);
    // 否则返回0
    return 0;
  }
  // 如果当前目录下不存在名为dhy.txt的目录项,则分配一个新的inode
  if((ip = ialloc(dp->dev, type)) == 0)
    panic("create: ialloc");

  ilock(ip);
  ip->major = major;
  ip->minor = minor;
  ip->nlink = 1;
  iupdate(ip);
  // 如果要创建的是目录
  if(type == T_DIR){  // Create . and .. entries.
    dp->nlink++;  // for ".."
    iupdate(dp);
    // No ip->nlink++ for ".": avoid cyclic ref count.
    // 目录下永远存在两个目录项: .和..
    if(dirlink(ip, ".", ip->inum) < 0 || dirlink(ip, "..", dp->inum) < 0)
      panic("create dots");
  }
  // 添加一个目标目录项<name,inum>
  if(dirlink(dp, name, ip->inum) < 0)
    panic("create: dirlink");

  iunlockput(dp);

  return ip;
}

使用create,很容易实现sys_opensys_mkdirsys_mknodSys_openkernel/sysfile.c:287)是最复杂的,因为创建一个新文件只是它能做的一小部分。如果open被传递了O_CREATE标志,它将调用createkernel/sysfile.c:301)。否则,它将调用nameikernel/sysfile.c:307)。Create返回一个锁定的inode,但namei不锁定,因此sys_open必须锁定inode本身。这提供了一个方便的地方来检查目录是否仅为读取打开,而不是写入。假设inode是以某种方式获得的,sys_open分配一个文件和一个文件描述符(kernel/sysfile.c:325),然后填充该文件(kernel/sysfile.c:337-342)。请注意,没有其他进程可以访问部分初始化的文件,因为它仅位于当前进程的表中。

uint64
sys_open(void)
{
  char path[MAXPATH];
  int fd, omode;
  struct file *f;
  struct inode *ip;
  int n;
  // 获取要打开的文件的路径和打开的权限模式
  if((n = argstr(0, path, MAXPATH)) < 0 || argint(1, &omode) < 0)
    return -1;

  begin_op();
  // 文件不存在则进行创建
  if(omode & O_CREATE){
    // 调用create在文件存在时直接返回,不存在时创建文件
    ip = create(path, T_FILE, 0, 0);
    if(ip == 0){
      end_op();
      return -1;
    }
  } else {
    // 如果打开模式不包含O_CREATE标志,表示要打开已存在的文件
    // 调用namei函数根据路径找到文件的inode(索引节点)
    if((ip = namei(path)) == 0){
      end_op();
      return -1;
    }
    ilock(ip);
    // 如果文件类型是目录并且打开模式不是只读的,直接返回
    // 在文件系统中,目录是一种特殊类型的文件,它包含其他文件和子目录的条目。
    // 通常情况下,只有以只读模式打开目录文件才是合理的,因为目录的结构和内容需要保持一致性。
    if(ip->type == T_DIR && omode != O_RDONLY){
      iunlockput(ip);
      end_op();
      return -1;
    }
  }
  // 如果文件类型是设备且设备的主设备号无效(小于0或大于等于NDEV)
  if(ip->type == T_DEVICE && (ip->major < 0 || ip->major >= NDEV)){
    iunlockput(ip);
    end_op();
    return -1;
  }
  // 从文件列表中分配一个新的file槽位,fd同理
  if((f = filealloc()) == 0 || (fd = fdalloc(f)) < 0){
    if(f)
      fileclose(f);
    iunlockput(ip);
    end_op();
    return -1;
  }
  // 文件类型是设备
  if(ip->type == T_DEVICE){
    f->type = FD_DEVICE;
    f->major = ip->major;
  } else {
    // 文件类型是INODE
    f->type = FD_INODE;
    f->off = 0;
  }
  f->ip = ip;
  f->readable = !(omode & O_WRONLY);
  f->writable = (omode & O_WRONLY) || (omode & O_RDWR);
  // 是否需要将当前文件数据进行截断
  if((omode & O_TRUNC) && ip->type == T_FILE){
    itrunc(ip);
  }
  
  iunlock(ip);
  end_op();

  return fd;
}

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

// Allocate a file descriptor for the given file.
// Takes over file reference from caller on success.
static int
fdalloc(struct file *f)
{
  int fd;
  struct proc *p = myproc();

  for(fd = 0; fd < NOFILE; fd++){
    if(p->ofile[fd] == 0){
      p->ofile[fd] = f;
      return fd;
    }
  }
  return -1;
}
// 大家可以改造让mkdir系统调用支持-p参数
uint64
sys_mkdir(void)
{
  char path[MAXPATH];
  struct inode *ip;

  begin_op();
  // 获取路径参数并尝试去创建目录
  if(argstr(0, path, MAXPATH) < 0 || (ip = create(path, T_DIR, 0, 0)) == 0){
    end_op();
    return -1;
  }
  iunlockput(ip);
  end_op();
  return 0;
}
// 创建设备文件
uint64
sys_mknod(void)
{
  struct inode *ip;
  char path[MAXPATH];
  int major, minor;

  begin_op();
  // 路径,主设备号和次设备号
  if((argstr(0, path, MAXPATH)) < 0 ||
     argint(1, &major) < 0 ||
     argint(2, &minor) < 0 ||
     // 创建设备文件
     (ip = create(path, T_DEVICE, major, minor)) == 0){
    end_op();
    return -1;
  }
  iunlockput(ip);
  end_op();
  return 0;
}

在我们还没有文件系统之前,第7章就研究了管道的实现。函数sys_pipe通过提供创建管道对的方法将该实现连接到文件系统。它的参数是一个指向两个整数的指针,它将在其中记录两个新的文件描述符。然后分配管道并安装文件描述符。

uint64
sys_pipe(void)
{
  uint64 fdarray; // user pointer to array of two integers
  struct file *rf, *wf;
  int fd0, fd1;
  struct proc *p = myproc();
  // 获取存放管道文件描述符的数组 
  if(argaddr(0, &fdarray) < 0)
    return -1;
  // 分配管道读文件和写文件  
  if(pipealloc(&rf, &wf) < 0)
    return -1;
  fd0 = -1;
  // 分配读文件描述符和写文件描述符号
  if((fd0 = fdalloc(rf)) < 0 || (fd1 = fdalloc(wf)) < 0){
    if(fd0 >= 0)
      p->ofile[fd0] = 0;
    fileclose(rf);
    fileclose(wf);
    return -1;
  }
  // 将两个管道文件描述符copy到用户指定的fdarray中去
  if(copyout(p->pagetable, fdarray, (char*)&fd0, sizeof(fd0)) < 0 ||
     copyout(p->pagetable, fdarray+sizeof(fd0), (char *)&fd1, sizeof(fd1)) < 0){
    p->ofile[fd0] = 0;
    p->ofile[fd1] = 0;
    fileclose(rf);
    fileclose(wf);
    return -1;
  }
  return 0;
}

int
pipealloc(struct file **f0, struct file **f1)
{
  struct pipe *pi;

  pi = 0;
  *f0 = *f1 = 0;
  if((*f0 = filealloc()) == 0 || (*f1 = filealloc()) == 0)
    goto bad;
  if((pi = (struct pipe*)kalloc()) == 0)
    goto bad;
  pi->readopen = 1;
  pi->writeopen = 1;
  pi->nwrite = 0;
  pi->nread = 0;
  initlock(&pi->lock, "pipe");
  (*f0)->type = FD_PIPE;
  (*f0)->readable = 1;
  (*f0)->writable = 0;
  (*f0)->pipe = pi;
  (*f1)->type = FD_PIPE;
  (*f1)->readable = 0;
  (*f1)->writable = 1;
  (*f1)->pipe = pi;
  return 0;

 bad:
  if(pi)
    kfree((char*)pi);
  if(*f0)
    fileclose(*f0);
  if(*f1)
    fileclose(*f1);
  return -1;
}

真实世界

实际操作系统中的buffer cache比xv6复杂得多,但它有两个相同的用途:

  • 缓存和同步对磁盘的访问。

与UNIX V6一样,Xv6的buffer cache使用简单的最近最少使用(LRU)替换策略;有许多更复杂的策略可以实现,每种策略都适用于某些工作场景,而不适用于其他工作场景。更高效的LRU缓存将消除链表,而改为使用哈希表进行查找,并使用堆进行LRU替换。现代buffer cache通常与虚拟内存系统集成,以支持内存映射文件。

MIT 6.S081 教材第八章内容 -- 文件系统 -- 02,# MIT 6.S081,linux

Xv6的日志系统效率低下。提交不能与文件系统调用同时发生。系统记录整个块,即使一个块中只有几个字节被更改。它执行同步日志写入,每次写入一个块,每个块可能需要整个磁盘旋转时间。真正的日志系统解决了所有这些问题。

日志记录不是提供崩溃恢复的唯一方法。早期的文件系统在重新启动期间使用了一个清道夫程序(例如,UNIX的fsck程序)来检查每个文件和目录以及块和索引节点空闲列表,查找并解决不一致的问题。清理大型文件系统可能需要数小时的时间,而且在某些情况下,无法以导致原始系统调用原子化的方式解决不一致问题。从日志中恢复要快得多,并且在崩溃时会导致系统调用原子化。

Xv6使用的索引节点和目录的基础磁盘布局与早期UNIX相同;这一方案多年来经久不衰。BSD的UFS/FFS和Linux的ext2/ext3使用基本相同的数据结构。文件系统布局中最低效的部分是目录,它要求在每次查找期间对所有磁盘块进行线性扫描。当目录只有几个磁盘块时,这是合理的,但对于包含许多文件的目录来说,开销巨大。Microsoft Windows的NTFS、Mac OS X的HFS和Solaris的ZFS(仅举几例)将目录实现为磁盘上块的平衡树。这很复杂,但可以保证目录查找在对数时间内完成(即时间复杂度为O(logn))。

Xv6对于磁盘故障的解决很初级:如果磁盘操作失败,Xv6就会调用panic。这是否合理取决于硬件:如果操作系统位于使用冗余屏蔽磁盘故障的特殊硬件之上,那么操作系统可能很少看到故障,因此panic是可以的。另一方面,使用普通磁盘的操作系统应该预料到会出现故障,并能更优雅地处理它们,这样一个文件中的块丢失不会影响文件系统其余部分的使用。

Xv6要求文件系统安装在单个磁盘设备上,且大小不变。随着大型数据库和多媒体文件对存储的要求越来越高,操作系统正在开发各种方法来消除“每个文件系统一个磁盘”的瓶颈。基本方法是将多个物理磁盘组合成一个逻辑磁盘。RAID等硬件解决方案仍然是最流行的,但当前的趋势是在软件中尽可能多地实现这种逻辑。这些软件实现通常允许通过动态添加或删除磁盘来扩展或缩小逻辑设备等丰富功能。当然,一个能够动态增长或收缩的存储层需要一个能够做到这一点的文件系统:xv6使用的固定大小的inode块阵列在这样的环境中无法正常工作。将磁盘管理与文件系统分离可能是最干净的设计,但两者之间复杂的接口导致了一些系统(如Sun的ZFS)将它们结合起来。

Xv6的文件系统缺少现代文件系统的许多其他功能;例如,它缺乏对快照和增量备份的支持。

现代Unix系统允许使用与磁盘存储相同的系统调用访问多种资源:命名管道、网络连接、远程访问的网络文件系统以及监视和控制接口,如/proc(注:Linux 内核提供了一种通过/proc文件系统,在运行时访问内核内部数据结构、改变内核设置的机制。proc文件系统是一个伪文件系统,它只存在内存当中,而不占用外存空间。它以文件系统的方式为访问系统内核数据的操作提供接口。)。不同于xv6中filereadfilewriteif语句,这些系统通常为每个打开的文件提供一个函数指针表,每个操作一个,并通过函数指针来援引inode的调用实现。网络文件系统和用户级文件系统提供了将这些调用转换为网络RPC并在返回之前等待响应的函数。文章来源地址https://www.toymoban.com/news/detail-544945.html


练习

  1. 为什么要在ballocpanic?xv6可以恢复吗?
  2. 为什么要在iallocpanic?xv6可以恢复吗?
  3. 当文件用完时,filealloc为什么不panic?为什么这更常见,因此值得处理?
  4. 假设在sys_link调用iunlock(ip)dirlink之间,与ip对应的文件被另一个进程解除链接。链接是否正确创建?为什么?
  5. create需要四个函数调用都成功(一次调用ialloc,三次调用dirlink)。如果未成功,create调用panic。为什么这是可以接受的?为什么这四个调用都不能失败?
  6. sys_chdiriput(cp->cwd)之前调用iunlock(ip),这可能会尝试锁定cp->cwd,但将iunlock(ip)延迟到iput之后不会导致死锁。为什么不这样做?
  7. 实现lseek系统调用。支持lseek还需要修改filewrite,以便在lseek设置off超过f->ip->size时,用零填充文件中的空缺。
  8. O_TRUNCO_APPEND添加到open,以便>>>操作符在shell中工作。
  9. 修改文件系统以支持符号链接。
  10. 修改文件系统以支持命名管道。
  11. 修改文件和VM系统以支持内存映射文件。

到了这里,关于MIT 6.S081 教材第八章内容 -- 文件系统 -- 02的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 流式系统:第五章到第八章

    原文:Streaming Systems 译者:飞龙 协议:CC BY-NC-SA 4.0 我们现在从讨论编程模型和 API 转向实现它们的系统。模型和 API 允许用户描述他们想要计算的内容。在规模上准确地运行计算需要一个系统——通常是一个分布式系统。 在本章中,我们将重点介绍一个实现系统如何正确实现

    2024年01月21日
    浏览(76)
  • 【新版系统架构】第八章-系统质量属性与架构评估

    软考-系统架构设计师知识点提炼-系统架构设计师教程(第2版) 第一章-绪论 第二章-计算机系统基础知识(一) 第二章-计算机系统基础知识(二) 第三章-信息系统基础知识 第四章-信息安全技术基础知识 第五章-软件工程基础知识(一) 第五章-软件工程基础知识(需求工

    2024年02月12日
    浏览(23)
  • MIT 6.S081 Lab Three

    本文为 MIT 6.S081 2020 操作系统 实验三解析。 MIT 6.S081课程前置基础参考: 基于RISC-V搭建操作系统系列 在本实验中,您将探索页表并对其进行修改,以简化将数据从用户空间复制到内核空间的函数。 开始编码之前,请阅读xv6手册的第3章和相关文件: * kernel/memlayout.h* ,它捕获了

    2024年02月09日
    浏览(33)
  • MIT 6.S081学习笔记(第〇章)

    本文涉及 xv6 《第零章 操作系统接口》相关,主要对涉及的进程、I/O、文件描述符、管道、文件等内容产生个人理解,不具有官方权威解释; 文章的目录与书中的目录没有严格的相关性; 文中会有问题 (Question) 字段,这来源于对 xv6 book 的扩展; 文中涉及的代码均能在macOS

    2024年02月09日
    浏览(32)
  • 《计算机系统与网络安全》 第八章 操作系统安全基础

    🌷🍁 博主 libin9iOak带您 Go to New World.✨🍁 🦄 个人主页——libin9iOak的博客🎐 🐳 《面试题大全》 文章图文并茂🦕生动形象🦖简单易学!欢迎大家来踩踩~🌺 🌊 《IDEA开发秘籍》学会IDEA常用操作,工作效率翻倍~💐 🪁🍁 希望本文能够给您带来一定的帮助🌸文章粗浅,敬

    2024年02月11日
    浏览(39)
  • 【MIT 6.S081】Lab7: Multithreading

    本Lab比较简单,就是为xv6添加一个用户级的多线程功能,然后熟悉一下Linux下多线程编程。 笔者用时约2h 这一部分的代码不涉及内核代码,所以也比较简单,根据提示修改 user/uthread.c 中的代码即可。仿照内核中进程转换函数 swtch 的实现即可。首先,添加一个 context 上下文结

    2023年04月09日
    浏览(24)
  • MIT6.S081学习笔记--lec 1

    abstract H/W 抽象化硬件 multiplex 多路复用 isolation 隔离性 sharing 共享(进程通信,数据共享) security / access control 安全性/权限控制 performance 性能/内核开销 range of applications 多应用场景 操作系统应该提供的功能:1. 多进程支持 2. 进程间隔离 3. 受控制的进程间通信 xv6 :一种在本

    2024年02月16日
    浏览(27)
  • 【软考高级信息系统项目管理师--第八章:项目整合管理】

    🚀 作者 :“码上有前” 🚀 文章简介 :软考高级–信息系统项目管理师 🚀 欢迎小伙伴们 点赞👍、收藏⭐、留言💬 1、资源分配; 2、平衡竞争性需求; 3、研究各种备选方法; 4、裁减过程以实现各种方法; 5、管理各个项目管理知识领域之间的依赖关系。 编写一份正式批准项

    2024年02月19日
    浏览(33)
  • 【vue2第八章】工程化开发和使用脚手架和文件结构

    vue工程化开发 使用脚手架VUE CLI: 1,核心包传统开发模式:基于js/html/css直接引入核心包开发vue。 2,工程化开发。基于构建工具如(webpack)的环境中开发vue。 vue cli是什么: vue cli是一个vue官方提供的一个全局的命令工具. 可以帮助我们快速的创建一个开发vue项目的标准化基础

    2024年02月10日
    浏览(30)
  • 【新版系统架构】第八章-系统质量属性与架构评估(软件系统质量属性、系统架构评估)

    软考-系统架构设计师知识点提炼-系统架构设计师教程(第2版) 第一章-绪论 第二章-计算机系统基础知识(一) 第二章-计算机系统基础知识(二) 第三章-信息系统基础知识 第四章-信息安全技术基础知识 第五章-软件工程基础知识(一) 第五章-软件工程基础知识(需求工

    2024年02月11日
    浏览(29)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包