Skip to content

fs.c

TIP

文件系统实现。 xv6 的文件系统被组织成一个分层结构,方便管理和扩展。 从底层到高层,依次是: + 块 (Blocks): 磁盘块分配器,负责管理物理磁盘块的分配和回收。 + 日志 (Log): 提供崩溃恢复功能。对于需要多个写操作的更新,日志系统可以保证这些操作的原子性, 防止系统在更新过程中崩溃导致文件系统不一致。 + 文件 (Files): 管理 inode,包括分配、读写数据和元数据。每个文件在文件系统中都由一个 inode 表示。 + 目录 (Directories): 一种特殊的 inode,其内容是目录项(dirent)的列表,每个目录项包含一个文件名和对应的 inode 编号。 + 路径名 (Names): 用户友好的文件路径,如 "/usr/rtm/xv6/fs.c"。路径名解析层将路径转换为对应的 inode。 此文件实现了底层的块、日志、文件和目录操作。 更高层次的、与文件描述符相关的系统调用(如 open, read, write)在 sysfile.c 中实现。

#include "types.h"
#include "riscv.h"
#include "defs.h"
#include "param.h"
#include "stat.h"
#include "spinlock.h"
#include "proc.h"
#include "sleeplock.h"
#include "fs.h"
#include "buf.h"
#include "file.h"

#define min(a, b) ((a) < (b) ? (a) : (b))

TIP

全局超级块。理论上每个磁盘设备都应有一个超级块, 但在 xv6 中我们只使用一个设备,所以定义一个全局变量即可。

struct superblock sb; 

TIP

从指定的设备读取超级块。 dev: 设备号 sb: 一个指向用于存放超级块数据的结构体的指针

static void
readsb(int dev, struct superblock *sb)
{
  struct buf *bp;

TIP

超级块位于磁盘的第 1 号块 (磁盘块从0开始编号,0号是引导区)。

  bp = bread(dev, 1);

TIP

将磁盘块中的数据拷贝到内存中的超级块结构体。

  memmove(sb, bp->data, sizeof(*sb));

TIP

释放缓冲区。

  brelse(bp);
}

TIP

初始化文件系统。 dev: 设备号

void
fsinit(int dev) {

TIP

读取超级块信息。

  readsb(dev, &sb);

TIP

检查文件系统幻数是否正确,以确认这是一个合法的 xv6 文件系统。

  if(sb.magic != FSMAGIC)
    panic("invalid file system");

TIP

初始化日志系统,日志区的位置和大小由超级块指定。

  initlog(dev, &sb);
}

TIP

将一个磁盘块的内容清零。 dev: 设备号 bno: 块号

static void
bzero(int dev, int bno)
{
  struct buf *bp;

TIP

读取指定的块到缓冲区。

  bp = bread(dev, bno);

TIP

将缓冲区的数据全部设置为 0。

  memset(bp->data, 0, BSIZE);

TIP

将写操作记录到日志中,确保即使发生崩溃,清零操作也能完成。

  log_write(bp);

TIP

释放缓冲区。

  brelse(bp);
}

TIP

块管理 (Block management)

TIP

分配一个空闲的、内容已清零的磁盘块。 dev: 设备号 成功时返回新分配的块号,如果磁盘空间耗尽则返回 0。

static uint
balloc(uint dev)
{
  int b, bi, m;
  struct buf *bp;

  bp = 0;

TIP

遍历磁盘的所有块,以查找位图中的一个空闲位。 sb.size 是磁盘总块数。 BBLOCK 宏计算给定块号 b 所在的位图块的块号。

  for(b = 0; b < sb.size; b += BPB){
    bp = bread(dev, BBLOCK(b, sb));
    for(bi = 0; bi < BPB && b + bi < sb.size; bi++){
      m = 1 << (bi % 8);
      if((bp->data[bi/8] & m) == 0){
        bp->data[bi/8] |= m;
        log_write(bp);
        brelse(bp);
        bzero(dev, b + bi);
        return b + bi;
      }
    }
    brelse(bp);
  }
  printf("balloc: 无块可用\n");
  return 0;
}

TIP

释放一个指定的磁盘块。 dev: 设备号 b: 要释放的块号

static void
bfree(int dev, uint b)
{
  struct buf *bp;
  int bi, m;

TIP

读取包含块 b 对应位图信息的块。

  bp = bread(dev, BBLOCK(b, sb));
  bi = b % BPB;
  m = 1 << (bi % 8);

TIP

检查该块是否确实已被分配。

  if((bp->data[bi/8] & m) == 0)
    panic("bfree: 释放自用块");

TIP

将位图中的对应位清零,标记为可用。

  bp->data[bi/8] &= ~m;

TIP

将对位图的修改写入日志。

  log_write(bp);
  brelse(bp);
}

TIP

Inodes inode 描述了一个文件或目录,但不包含其名字。 磁盘上的 inode 结构 (struct dinode) 存储了文件的元数据: 类型 (文件、目录、设备)、大小、链接数以及指向文件数据块的指针数组。 所有的 inode 都存储在磁盘上从 sb.inodestart 开始的一片连续区域中。 每个 inode 都有一个唯一的编号 (inum),这个编号就是它在 inode 区的索引。 内核在内存中维护一个 inode 缓存 (itable),用于缓存活跃的 inode。 这个缓存层为多个进程并发访问 inode 提供了同步点。 内存中的 inode (struct inode) 包含了一些磁盘上没有的簿记信息, 主要是 ip->ref (引用计数) 和 ip->valid (有效位)。 一个 inode 实例从创建到销毁会经历以下几种状态: * 已分配 (Allocated): 磁盘上的 inode 类型 (type) 非零。ialloc() 分配一个 inode, 当 iput() 发现引用计数和链接计数都为零时,会释放它。 * 在缓存中被引用 (Referenced in table): 如果 ip->ref 为零,表示 itable 中的这个槽位是空闲的。 否则,ip->ref 记录了有多少个内存指针指向这个 inode (例如,打开的文件、当前工作目录等)。 iget() 查找或创建一个 itable 条目并增加引用计数;iput() 减少引用计数。 * 有效 (Valid): 当 ip->valid 为 1 时,表示内存 inode 中的数据(类型、大小等)是从磁盘同步过来的,是有效的。 ilock() 会在需要时从磁盘读取 inode 数据并设置 ip->valid。 如果 ip->ref 降为零,iput() 会清除 ip->valid。 * 锁定 (Locked): 文件系统代码在访问或修改 inode 的元数据或内容之前,必须先锁定该 inode。 锁定可以防止并发访问导致的数据竞争和不一致。 因此,一个典型的 inode 操作序列如下: ip = iget(dev, inum); // 获取 inode 的内存引用 ilock(ip); // 锁定 inode,并从磁盘读取数据(如果需要) ... 检查和修改 ip->xxx ... iupdate(ip); // 将修改写回磁盘 iunlock(ip); // 解锁 inode iput(ip); // 释放对 inode 的内存引用 ilock() 和 iget() 是分开的,这使得系统调用可以长时间持有一个 inode 的引用 (比如一个打开的文件),但只在需要读写时才短暂地锁定它。 这种设计有助于避免在路径名查找等复杂操作中发生死锁。 iget() 增加 ip->ref,确保 inode 留在缓存中,指针持续有效。 许多文件系统内部函数都假定调用者已经持有了相关 inode 的锁, 这使得调用者可以将多个操作组合成一个原子操作。 itable.lock (自旋锁) 保护 itable 结构本身,特别是 inode 的分配和查找过程。 对 ip->ref, ip->dev, ip->inum 的访问必须在持有 itable.lock 的情况下进行。 ip->lock (休眠锁) 保护 inode 的所有其他字段(如 type, size, addrs 等)以及文件内容。 读写这些字段前必须持有 ip->lock。

TIP

内存中的 inode 缓存区

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

TIP

初始化 inode 缓存 (itable)。

void
iinit()
{
  int i = 0;
  
  initlock(&itable.lock, "itable");
  for(i = 0; i < NINODE; i++) {
    initsleeplock(&itable.inode[i].lock, "inode");
  }
}

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

TIP

在设备 dev 上分配一个新的 inode。 分配时会将其类型设置为 type。 返回一个未锁定但已被引用 (ref=1) 的 inode。 如果磁盘上没有空闲的 inode,则返回 NULL (0)。

struct inode*
ialloc(uint dev, short type)
{
  int inum;
  struct buf *bp;
  struct dinode *dip;

TIP

遍历磁盘上的所有 inode,查找一个空闲的(类型为0)。

  for(inum = 1; inum < sb.ninodes; inum++){
    bp = bread(dev, IBLOCK(inum, sb));
    dip = (struct dinode*)bp->data + inum%IPB;
    if(dip->type == 0){
      memset(dip, 0, sizeof(*dip));
      dip->type = type;
      log_write(bp);
      brelse(bp);
      return iget(dev, inum);
    }
    brelse(bp);
  }
  printf("ialloc: 无可用inodes\n");
  return 0;
}

TIP

将内存中已修改的 inode 写回磁盘。 任何对 inode 磁盘字段 (ip->xxx) 的修改之后都必须调用此函数。 调用者必须持有 ip->lock。

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

  bp = bread(ip->dev, IBLOCK(ip->inum, sb));
  dip = (struct dinode*)bp->data + ip->inum%IPB;

TIP

从内存 inode 拷贝元数据到磁盘 inode 结构。

  dip->type = ip->type;
  dip->major = ip->major;
  dip->minor = ip->minor;
  dip->nlink = ip->nlink;
  dip->size = ip->size;
  memmove(dip->addrs, ip->addrs, sizeof(ip->addrs));
  log_write(bp);
  brelse(bp);
}

TIP

在 inode 缓存 (itable) 中查找指定设备和 inode 编号的 inode。 如果找到,增加其引用计数并返回。 如果没找到,会从 itable 中找一个空闲槽位分配给它,并返回。 这个函数不锁定 inode,也不从磁盘读取数据。

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

  acquire(&itable.lock);

TIP

遍历 itable,查找 inode 是否已在缓存中。

  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)
      empty = ip;
  }

TIP

缓存中没有,需要分配一个新的 itable 条目。

  if(empty == 0)
    panic("iget: no inodes");

TIP

使用找到的空闲槽位。

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

  return ip;
}

TIP

增加 inode ip 的引用计数。 返回 ip 本身,方便链式调用,如 ip = idup(ip1);

struct inode*
idup(struct inode *ip)
{
  acquire(&itable.lock);
  ip->ref++;
  release(&itable.lock);
  return ip;
}

TIP

锁定给定的 inode。 在返回前,会确保 inode 的内容在内存中是有效的(即 ip->valid == 1)。 如果无效,会从磁盘读取 inode 数据。

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;

TIP

从磁盘 dinode 复制元数据到内存 inode。

    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");
  }
}

TIP

解锁给定的 inode。

void
iunlock(struct inode *ip)
{
  if(ip == 0 || !holdingsleep(&ip->lock) || ip->ref < 1)
    panic("iunlock");

  releasesleep(&ip->lock);
}

TIP

减少对内存中 inode 的引用计数。 如果这是最后一个引用 (ip->ref == 1),并且 inode 没有链接 (ip->nlink == 0), 则会释放磁盘上的 inode 及其所有数据块。 如果这是最后一个引用,但仍有链接,inode 缓存条目可以被回收,但磁盘数据保留。 所有对 iput 的调用都应在一个事务中,以防需要释放 inode。

void
iput(struct inode *ip)
{
  acquire(&itable.lock);

  if(ip->ref == 1 && ip->valid && ip->nlink == 0){

TIP

inode 没有链接,也没有其他内存引用:可以安全释放。

TIP

因为 ip->ref == 1,意味着没有其他进程持有这个 inode 的引用, 所以这里的 acquiresleep 不会阻塞或死锁。

    acquiresleep(&ip->lock);

    release(&itable.lock);

    itrunc(ip);
    ip->type = 0;
    iupdate(ip);
    ip->valid = 0;

    releasesleep(&ip->lock);

    acquire(&itable.lock);
  }

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

TIP

一个方便的组合操作:先解锁 inode,然后释放对它的引用。

void
iunlockput(struct inode *ip)
{
  iunlock(ip);
  iput(ip);
}

TIP

Inode content 每个 inode 关联的数据内容存储在一系列磁盘块中。 前 NDIRECT 个数据块的地址直接存储在 ip->addrs[] 数组中。 之后的 NINDIRECT 个数据块的地址存储在一个间接块中,该间接块的地址是 ip->addrs[NDIRECT]。

TIP

返回 inode ip 的第 bn 个逻辑数据块对应的物理磁盘块号。 如果该逻辑块尚未分配,bmap 会为其分配一个新的物理块。 如果分配失败(如磁盘已满),则返回 0。

static uint
bmap(struct inode *ip, uint bn)
{
  uint addr, *a;
  struct buf *bp;

  if(bn < NDIRECT){
    if((addr = ip->addrs[bn]) == 0){
      addr = balloc(ip->dev);
      if(addr == 0)
        return 0;
      ip->addrs[bn] = addr;
    }
    return addr;
  }
  bn -= NDIRECT;

  if(bn < NINDIRECT){

TIP

加载间接块,如果间接块本身不存在,则先分配。

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

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

TIP

截断一个 inode (释放其所有数据块)。 调用者必须持有 ip->lock。

void
itrunc(struct inode *ip)
{
  int i, j;
  struct buf *bp;
  uint *a;

TIP

释放所有直接块

  for(i = 0; i < NDIRECT; i++){
    if(ip->addrs[i]){
      bfree(ip->dev, ip->addrs[i]);
      ip->addrs[i] = 0;
    }
  }

TIP

释放所有间接块指向的数据块

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

TIP

释放间接块本身

    bfree(ip->dev, ip->addrs[NDIRECT]);
    ip->addrs[NDIRECT] = 0;
  }

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

TIP

从 inode 复制状态信息到 stat 结构体。 调用者必须持有 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;
}

TIP

从 inode 读取数据。 调用者必须持有 ip->lock。 user_dst: 1 表示 dst 是用户空间地址,0 表示是内核地址。 dst: 目标地址 off: 读取的起始偏移量 n: 要读取的字节数 返回成功读取的字节数,如果出错则返回 -1。

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;
  if(off + n > ip->size)
    n = ip->size - off;

  for(tot=0; tot<n; tot+=m, off+=m, dst+=m){
    uint addr = bmap(ip, off/BSIZE);
    if(addr == 0)
      break;
    bp = bread(ip->dev, addr);
    m = min(n - tot, BSIZE - off%BSIZE);

TIP

从缓冲区复制数据到目标地址

    if(either_copyout(user_dst, dst, bp->data + (off % BSIZE), m) == -1) {
      brelse(bp);
      tot = -1;
      break;
    }
    brelse(bp);
  }
  return tot;
}

TIP

向 inode 写入数据。 调用者必须持有 ip->lock。 user_src: 1 表示 src 是用户空间地址,0 表示是内核地址。 src: 源地址 off: 写入的起始偏移量 n: 要写入的字节数 返回成功写入的字节数。如果返回值小于 n,表示发生了错误(如磁盘空间不足)。

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;

  for(tot=0; tot<n; tot+=m, off+=m, src+=m){
    uint addr = bmap(ip, off/BSIZE);
    if(addr == 0)
      break;
    bp = bread(ip->dev, addr);
    m = min(n - tot, BSIZE - off%BSIZE);

TIP

从源地址复制数据到缓冲区

    if(either_copyin(bp->data + (off % BSIZE), user_src, src, m) == -1) {
      brelse(bp);
      break;
    }
    log_write(bp);
    brelse(bp);
  }

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

TIP

即使文件大小没有改变,也需要更新 inode 到磁盘, 因为 bmap() 可能分配了新的数据块,修改了 ip->addrs[]。

  iupdate(ip);

  return tot;
}

TIP

Directories

TIP

比较两个目录项名称是否相同。

int
namecmp(const char *s, const char *t)
{
  return strncmp(s, t, DIRSIZ);
}

TIP

在目录 dp 中查找名为 name 的目录项。 如果找到,返回对应的 inode,并通过 *poff 返回该目录项在目录文件中的偏移量。 如果找不到,返回 0。 调用者在使用返回的 inode 后必须调用 iput() 来释放它。

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");

TIP

遍历目录文件中的所有目录项

  for(off = 0; off < dp->size; off += sizeof(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){

TIP

找到了匹配的目录项

      if(poff)
        *poff = off;
      inum = de.inum;
      return iget(dp->dev, inum);
    }
  }

  return 0;
}

TIP

将一个新的目录项 (name, inum) 写入目录 dp。 成功返回 0,失败返回 -1(例如,名称已存在或目录已满)。

int
dirlink(struct inode *dp, char *name, uint inum)
{
  int off;
  struct dirent de;
  struct inode *ip;

TIP

检查名称是否已存在。

  if((ip = dirlookup(dp, name, 0)) != 0){
    iput(ip);
    return -1;
  }

TIP

查找一个空的目录项 (de.inum == 0) 来写入新条目。

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

TIP

写入新的目录项

  strncpy(de.name, name, DIRSIZ);
  de.inum = inum;
  if(writei(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
    return -1;

  return 0;
}

TIP

Paths

TIP

从路径字符串 path 中提取出第一个路径元素到 name。 返回一个指向路径字符串中剩余部分的指针。 返回的路径指针不会有前导斜杠。 调用者可以检查 *path == '\0' 来判断 name 是否是最后一个元素。 如果没有名称可提取,则返回 0。 示例: skipelem("a/bb/c", name) -> "bb/c", name = "a" skipelem("///a//bb", name) -> "bb", name = "a" skipelem("a", name) -> "", name = "a" skipelem("", name) -> 0 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;
}

TIP

逐级解析路径,返回最终的 inode。 如果 nameiparent 为 1,则查找并返回路径的父目录的 inode, 并将最后一个路径元素复制到 name 中。 这个函数必须在一个事务中被调用。

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

TIP

循环解析路径的每个部分

  while((path = skipelem(path, name)) != 0){
    ilock(ip);
    if(ip->type != T_DIR){
      iunlockput(ip);
      return 0;
    }
    if(nameiparent && *path == '\0'){

TIP

如果是查找父目录,并且已经到达路径的最后一个元素, 那么当前 ip 就是要找的父目录,直接返回。

      iunlock(ip);
      return ip;
    }

TIP

在当前目录中查找下一个路径元素

    if((next = dirlookup(ip, name, 0)) == 0){
      iunlockput(ip);
      return 0;
    }
    iunlockput(ip);
    ip = next;
  }
  if(nameiparent){
    iput(ip);
    return 0;
  }
  return ip;
}

TIP

解析完整路径并返回其对应的 inode。

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

TIP

解析路径的父目录,并返回其 inode。 路径的最后一个文件名部分被复制到 name 中。

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