讲解 xv6 文件系统实现
“正常” 的项目都有遵循正常人能理解的代码组织
你可以问出很多问题
make -nB qemu | grep mkfsmake -nB qemu | grep fs\.imgmkfs 和 kernel 共享了一部分代码
磁盘上的数据结构
[ boot | superblock | log | inodes | free bitmap | data blocks ]
参数
BSIZE, ROOTINO (/ 的 inode 编号), LOGSIZE, NINODE, ...重要的数据结构
struct superblock (magic, size, ...), mkfs 填入struct dinode (type, nlink, size, addrs)NDIRECT (12) 个直接索引,一个间接索引struct dirent (inum, name)彩蛋: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
(真的都是数据结构操作)
ialloc: 分配 inode (从 1 开始编号)
ialloc() 返回的是 / 的 inodeiappend: 向 inode 对应的文件 (包括目录文件) 写入数据
.” 和 “..” 对应的 dirent,都指向 rootino (1)足够了
每次访问 block 的一部分都读一次、写一次磁盘 = 性能崩盘。
Buffer Cache
核心数据结构:struct buf (buf.h, bio.c)
sys_write
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();
begin_op 和 end_op 包裹的代码
begin_op 不会导致 log 空间用完,就可以继续end_op 提交后唤醒bwrite 仅 log.c 调用)例子:写入数据
bp = bread(ip->dev, bmap(ip, off/BSIZE));
log_write(bp);
begin_op 和 end_op 之间所有的写入都仅发生在内存end_op 时才发生 commitstatic 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 > 0refcnt == 0 维护这一不变式的例子:log_write
i == log.lh.n 时才执行 bpin(b) 增加引用计数典型的阅读路径
sys_open)create, 被 sys_open, sys_mkdir 调用)sys_link)xv6 文件系统实现
Take-away messages