背景

文件系统的 “理论” 部分


那么一个真正的文件系统到底是如何实现的?

本次课内容与目标

讲解 xv6 文件系统实现

  • 磁盘镜像 (数据结构)
  • 文件系统实现
    • buffer cache
    • journaling
    • 系统调用实现

xv6 文件系统:数据结构

开始阅读代码之前

“正常” 的项目都有遵循正常人能理解的代码组织

  • 你们觉得呢?
    • 查看一下 xv6 的目录结构
      • 可以从 kernel 目录的相关文件看起
      • 可以从 mkfs 看起

你可以问出很多问题

  • 文件系统镜像是如何生成的?
    • make -nB qemu | grep mkfs
  • 文件系统镜像是如何被传入 qemu 的?
    • make -nB qemu | grep fs\.img

开始阅读代码

mkfs 和 kernel 共享了一部分代码

  • 这个设计非常重要 (你们实现 lab 时候也需要)
  • 哪个头文件比较重要?
    • types.h
    • fs.h
    • stat.h
    • param.h

磁盘上的数据结构

[ boot | superblock | log | inodes | free bitmap | data blocks ]

xv6 文件系统:基础数据结构

参数

  • BSIZE, ROOTINO (/ 的 inode 编号), LOGSIZE, NINODE, ...

重要的数据结构

  • struct superblock (magic, size, ...), mkfs 填入
  • struct dinode (type, nlink, size, addrs)
    • NDIRECT (12) 个直接索引,一个间接索引
    • 最大文件大小: $12 + \frac{1024}{4}=268$KB
      • 验证你的想法:动手做个实验
  • struct dirent (inum, name)
    • 14 字节文件名
      • 试试创建长文件名;也许你的 lab 就直接 crash/panic 了

mkfs 工具:创建数据结构

彩蛋:static_assert (C++11/C11)

奇怪的 xint (does nothing)

uint xint(uint x) {
  uint y;
  uchar *a = (uchar*)&y;
  a[0] = x; a[1] = x >> 8; a[2] = x >> 16; a[3] = x >> 24;
  return y;
}

清零文件系统

  • for(i = 0; i < FSSIZE; i++) wsect(i, zeroes);

初始化 superblock

  • nmeta 46 (boot, super, log blocks 30 inode blocks 13, bitmap blocks 1) blocks 954 total 1000

数据结构操作

(真的都是数据结构操作)


ialloc: 分配 inode (从 1 开始编号)

  • 第一次 ialloc() 返回的是 / 的 inode

iappend: 向 inode 对应的文件 (包括目录文件) 写入数据

  • 纯粹的数据结构操作
  • 写入了 “.” 和 “..” 对应的 dirent,都指向 rootino (1)

足够了

  • mkfs 只需要创建根目录,并向根目录里添加文件

xv6 内核系统实现

Block 管理

每次访问 block 的一部分都读一次、写一次磁盘 = 性能崩盘。

Buffer Cache

  • 代理操作系统中所有对磁盘的访问
  • 所有访问的 block 都在内存中有 write-back 的副本
  • reference-counted
    • cache hit 时可以避免读磁盘、延迟写磁盘
    • “batch” 对同一块 (例如 inode 块) 的多次写入
  • 不论有多少设备,系统都统一管理 LRU 的 buffer cache

核心数据结构:struct buf (buf.h, bio.c)

  • 内核中的数据结构比 mkfs 要复杂一些

简单的文件操作

sys_write

  • 仅修改一个存在的 inode 和文件数据
    • inode 必须先载入内存才能使用
  • 不涉及文件系统元数据操作 (例如目录遍历)

begin_op();
ilock(f->ip);
if ((r = writei(f->ip, 1, addr + i, f->off, n1)) > 0)
  f->off += r;
iunlock(f->ip);
end_op();

无处不在的 Journaling

begin_opend_op 包裹的代码

  • called at the start/end of each FS system call
  • 一个非常有趣的并发控制 (条件变量)
    • 如果 begin_op 不会导致 log 空间用完,就可以继续
    • 否则睡眠,等待 end_op 提交后唤醒
  • 完全在 buffer cache 机制上实现 (bwritelog.c 调用)

例子:写入数据

bp = bread(ip->dev, bmap(ip, off/BSIZE));
log_write(bp);
  • begin_opend_op 之间所有的写入都仅发生在内存
  • end_op 时才发生 commit

教科书式的 Commit

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

看似简单,实则复杂的生存期管理

两个看似简单的规则

  • 只要未来可能使用,必须 refcnt > 0
  • 只要未来不会被使用,必须 refcnt == 0

维护这一不变式的例子:log_write

  • 仅在 i == log.lh.n 时才执行 bpin(b) 增加引用计数

元数据管理

典型的阅读路径

  • 路径解析 (sys_open)
  • 创建文件/目录 (create, 被 sys_open, sys_mkdir 调用)
  • 创建链接 (sys_link)

总结

总结

xv6 文件系统实现

  • 磁盘镜像 (数据结构)
  • 文件系统实现
    • buffer cache, journaling 和系统调用实现

Take-away messages

  • Happy hacking! 欣赏好的代码
  • 记得回去调试