[MIT 6.s081] Xv6 Lab10 File System 实验记录
upd@2022/9/14:最近把实验的代码放到 github 上了,如果需要参考可以查看这里:
https://github.com/ttzytt/xv6-riscv
里面不同的分支就是不同的实验。
Lab10: file system
Large files
描述
在 xv6 的底层实现中,文件是由 struct dinode 来描述的,如下:
1 | struct dinode { |
这里我们主要关注结构体中的 addrs 属性。维护了此文件的实际储存位置。其中有 addrs 的前十二个直接指向文件储存的块,最后一个是间接的块,即其指向的块中储存了别的指针,这些指针才指向了实际储存数据的块。听起来有点绕,大概是下面这个示意图的样子:

那我们可以计算一下 xv6 能支持的最大文件大小。已知一个 struct dinode 有 64B 的大小,一个磁盘块能储存 1024B 的数据。
那么前 12 个直接指向数据块的 addrs 就能储存
而最后一个间接的数据指针指向一个存满了指针(指向别的磁盘块)的块,这个块能存放 个地址。
而这里的每个地址都是一个块,因此,这个间接 addrs 能提供 的储存空间。
那么他们加起来就是 的储存空间,等于
这样的储存空间显然是非常小的,所以在这个 lab 中我们需要给 inode 加入一个二级的间接块指针来解决这个问题。
一个一级的间接块指针就是刚刚提到的,inode 中 addrs 的最后一个,其指向一个块,而这个块中储存的块指针又指向别的数据块。
在二级块指针中,addrs 中指针指向的块中的指针会指向另外的,储存指针的块,以此加大储存空间(有点像是多级页表了),如下:

可以计算一下这个二级间接块指针能提供的空间,已知一个块能储存 256 个块指针,那么 addrs 指向的那个块能储存 256 个块指针块的块号,所以总数就是 个块。除以 1024 为 64MB,可见提升非常巨大。
思路
需要修改 bmap() 和 itrunc() 这两个函数,不过没有什么特别难以思考的地方,所以具体的解释还是放到代码部分。
代码
因为加入了二级间接索引,所以要先对一些宏定义进行修改:
1 |
同时也需要修改 struct dinode 和 struct inode。其中,dinode 是实际储存在磁盘上的,而 inode 在 dinode 的基础上加入了很多方便处理 inode 的元数据:
1 | //in fs.h |
1 | // in file.h |
bmap():
这个函数接收 inode 指针和 bn,表示 inode 中的第几个块,返回对应的块号。
我们需要在这个函数中添加对二级间接块的支持。为了取得二级的间接块,我们可以先获取到一级的间接块。
代码中很多写法可以参考前面对一级间接块的处理。
1 | // in fs.c |
itrunc():
此函数会清理 inode 中的所有块,或者可以理解成删除一个文件。这个函数内实际上是在不停的调用 brelse() 和 bfree()。
其中 brelse() 释放一个块缓存,而 bfree() 则通过修改磁盘上 bitmap 块的数据来释放磁盘上的一个块。
和 bmap() 相同,很多地方可以参考一级间接索引的实现。主要的思路类似递归,先遍历每个一级块,检查里面是否有数据,如果有,就去遍历这个一级块里的二级块。
1 | // in fs.c |
Symbolic links
实验描述
这个实验需要我们实现符号链接,或者说软链接(说实话我现在还不是很清楚软硬链接的本质区别),有点像 windows 中的快捷方式。
实现起来其实很简单,不过这个 lab 中的提示给的(对我来说)不是很足,所以做的时候还是有点懵逼的,最后看了别人的博客才做出来。
首先软链接就像是一个文件的“指针”,如果我们打开某个软链接,实际打开的是那个链接指向的文件,这样就可以实现某个目录打开实际储存在不同目录的文件。
思路
那么我们要如何实现这个软链接呢?软链接的本质其实也是一个文件,我们只要在这个文件(其实是 inode)中储存此链接指向的文件的路径就行了。
为了实现链接的效果,在 open() 函数中,需要去根据链接中储存的路径,递归的找到最终指向的文件(可能会有一个软链接指向另一个软链接)。
可是万一我们想打开的是这个软连接本身呢?这就需要新定义的 open() 标志位了,这些标志位用于指定打开文件描述符的一些设置。那我们可以添加一个 O_NOFOLLOW 的标志位,意味不去递归打开软连接里的路径,而打开软连接本身。
1 | //in fcntl.h |
同时 inode 本身是对磁盘中储存的各种数据的一种“抽象”,为了得知 inode 里面具体放的是什么,需要定义一个新的 inode 类型:
1 | //in stat.h |
注意这个实验中比较烦人的一点是,sys_symlink() 这个系统调用是没有注册好的,需要和 lab2 一样,在各种文件中加入这个系统调用,我假设看这个文章的人都是做过 lab2 的,所以不赘述,如果你没有,可以看我的这篇文章。
代码
sys_symlink():
前面说软连接的本质其实是一种文件,不过这个文件其实又是一个 inode,那么在写代码的时候就需要注意各种操作都是对 inode 进行的。然后还有就是在各种文件相关的系统调用中,我们都需要使用 open_op() 和 end_op() 把这些系统调用包裹起来。其代表,在这个区间内的任何操作会先被记录到日志系统中(不熟悉可用参考 xv6 的书以及 lecture)。
1 | uint64 sys_symlink(){ |
sys_open():
下面这段 sys_open() 开头的代码打开或者创建了用户传进来路径所对应文件的 inode,记录在 ip 中。而 sys_open() 的后续代码会处理这个 ip 来完成打开的操作,我们先不用管。
1 | \\ in sysfile.c |
那对于一个符号链接来说,用户传进来路径对应的 ip 并不是其想要打开的 ip,所以我们需要递归的跟随符号链接中指向的文件来修改这个 ip。注意最终这个 ip 必须是上锁的。
如下(这部分代码添加在上面代码的后面):
1 | \\ in sysfile.c |
这里要特别特别注意一个点,在递归跟随软链接时,我们碰到一个不是软链接的文件需要停下来。这也要求我们去访问 inode 的 type 属性。那么判断这个属性一定要在 ilock(ip) 的后面,我调了好久才发现这个 bug。
我们先看下 ilock() 的代码:
1 | // Lock the given inode. |
可以发现,会先检查 ip->valid,这个 valid 属性表示当前 inode 的数据是否从磁盘中加载过。如果是没有,那么会先读取磁盘,然后把数据加载进这个 inode 中。
也就是说,如果在执行 ilock() 之前先访问了 inode,意味着这个 inode 很可能是空的,自然读到的东西也没意义(这也再一次提醒了我们访问线程间共享数据时,一定要加锁)。
做完这些后,就可以愉快的 AC 了,也祝在做这个 lab 的人尽快 AC:

提醒一点,如果你发现你的程序在 qemu 中跑测试没问题,但是 make grade 过不了的话,很可能是因为超时了(估计是我电脑性能太垃了),这个时候需要去 python 的计分程序 grade-lab-fs 中改下时限。
总结
数组越界,内存泄漏实在是非常可怕的事情——实际的错误和系统报的错没有任何的相关性,调都调不出来。
这里大概讲下我做这个 lab 时犯的一些傻逼到极致的错误吧,关键是调了两个下午才调出来。
最开始我在进行 symlinktest 的时候,会报 panic,信息是 virtio_disk_intr status。那这种跟虚拟磁盘有关的东西我肯定是不会处理的,于是单步了以下,找到了 symlinktest 中具体是哪一步出了问题。结果如下:
1 | r = symlink("/testsymlink/4", "/testsymlink/3"); |
这里,symlinktest 调用 close(fd2) 之后就直接 panic 了。
然后我又单步了以下,大概发现,发生问题时的调用过程是这样的:
1 | sys_close() -> fileclose() -> iput() -> itrunc() -> bread(): |
我一想是 itrunc() 写错了,还直接新开了个分支,抄了别人的 itrunc() 然后还是不行。
后来又想,不会是什么玄学问题把,于是直接把那个 panic() 给注释掉了,又发现有新的 panic(),这次报的错是 freeing free block:
1 | static void |
后来又发现,在 itrunc() 中,根本没有释放一级间接索引的块,而是直接释放了二级间接索引(因为 addrs[12] 非零)。这肯定是不合理的,一定是一级的用完了再用二级的。结合 freeing free block 的 panic() 信息,我基本确定了问题可能是由某种越界引起的。
最后发现,居然是 struct inode 这里出了问题:
1 | struct inode { |
我把 dinode 的 addrs[NDIRECT + 1] 改成了 addrs[NDIRECT + 2],但是忘了改 inode 的。。。
这就造成了,我在访问 addrs[12] 时,访问的实际是下一个 inode 的 dev 属性。那么事情就离谱起来了,你说一个 inode 的二级间接索引块怎么可能会在一号块(超级块)呢。。。我其实还挺好奇的,itrunc() 的时候怎么没有把超级块给释放了,又是如何引起虚拟磁盘的 panic() 的。我是懒得调了,有兴趣的可以试试看。
不说了,破大防了。。。




![[Stanford CS144] Lab4 实验记录](/img/CS144/tcp%E7%8A%B6%E6%80%81%E6%B5%81%E8%BD%AC%E5%9B%BE.jpg)