Large files (moderate)
要求
在此作业中,您将增加 xv6 文件的最大大小。目前 xv6 文件限制为 268 个块,或 268*BSIZE 字节(在 xv6 中 BSIZE 为 1024)。此限制来自这样一个事实,即 xv6 inode 包含 12 个“直接”块号和一个“单间接”块号,它指的是最多可容纳 256 个块号的块,总共 12+256=268 个块。
bigfile
命令创建它所能创建的最长文件,并报告该大小:
$ bigfile
..
wrote 268 blocks
bigfile: file is too small
$
测试失败,因为 bigfile
希望能够创建包含 65803 个块的文件,但未修改的 xv6 将文件限制为 268 个块。您将更改 xv6 文件系统代码以支持每个 inode 中的“双间接”块,其中包含 256 个单间接块地址,每个地址最多可以包含 256 个数据块地址。结果将是文件将能够包含多达 65803 个块,或 256*256+256+11 块(11 而不是 12,因为我们将牺牲一个直接块号来换取双间接块)。
准备工作
mkfs
程序创建 xv6 文件系统磁盘映像,并确定文件系统总共有多少块; 此大小由kernel/param.h
中的 FSSIZE
控制。你将看到此实验存储库中的 FSSIZE
设置为 200,000 个块。您应该在 make 输出中看到以下来自 mkfs/mkfs
的输出:nmeta 70 (boot, super, log blocks 30 inode blocks 13, bitmap blocks 25) blocks 199930 total 200000
这一行描述了 mkfs/mkfs
构建的文件系统:它有 70 个元数据块(用于描述文件系统的块)和 199,930 个数据块,总计 200,000 个块。
如果在实验期间的任何时候,您发现自己必须从头开始重建文件系统,则可以运行 make clean
,哪些强制重建 fs.img
。
磁盘索引节点的格式由 fs.h
中的 struct dinode
定义。你应该对 NDIRECT、NINDIRECT、MAXFILE
和 struct dinode
的 addrs[]
元素特别感兴趣。查看 xv6 文本中的图 8.3,了解标准 xv6 索引节点的示意图。
在磁盘上查找文件数据的代码位于 fs.c
的 bmap()
中。看看它,确保你明白它在做什么。bmap()
在读取和写入文件时都会被调用。写入时,bmap()
根据需要分配新块来保存文件内容,如果需要保存块地址,还会分配间接块。
bmap()
处理两种块号。bn
参数是一个“逻辑块号”——文件中相对于文件开头的块号。ip->addrs[]
中的块号和 bread()
的参数是磁盘块号。您可以将 bmap()
视为将文件的逻辑块号映射到磁盘块号。
你的工作
修改bmap()
,使其实现一个双间接块,以及直接块和一个单间接块。您只有 11 个直接块,而不是 12 个,因为需要为您的新双间接块腾出空间; 不允许更改磁盘上 inode 的大小。ip->addrs[]
的前 11 个元素应该是直接块;第 12 个应该是一个单间接块(就像当前的块一样);第 13 个应该是您的新双间接块。当bigfile
写入 65803 个块并且usertests -q
成功运行时,您就完成了本练习:
$ bigfile
..................................
wrote 65803 blocks
done; ok
$ usertests -q
...
ALL TESTS PASSED
$
bigfile
至少需要运行一分半钟才能。
提示
- 确保您了解
bmap()
。画出ip->addrs[]
、间接块、双间接块和它指向的单间接块与数据块之间的关系图。确保您了解为什么添加双间接块会使最大文件大小增加 256*256 个块(实际上是 -1,因为您必须将直接块的数量减少 1)。 - 考虑如何使用逻辑块号为双间接块及其指向的间接块编制索引。
- 如果你更改了
NDIRECT
的定义,你可能不得不在file.h
的struct inode
中更改addrs[]
的声明。确保struct inode
和struct dinode
在其addrs[]
数组中具有相同数量的元素。 - 如果更改
NDIRECT
的定义,请确保创建一个新的fs.img
,因为mkfs
使用NDIRECT
来构建文件系统。 - 如果您的文件系统进入错误状态,可能是崩溃,请删除
fs.img
(从 Unix 而不是 xv6 执行此操作)。make
将为您构建一个新的干净文件系统映像。 - 不要忘记
brelse()
你bread()
的每个块。 - 您应该仅根据需要分配间接块和双重间接块,就像原始的
bmap()
一样。 - 确保
itrunc
释放文件的所有块,包括双间接块。 usertests
的运行时间比以前的实验更长,因为对于此实验,FSSIZE
更大,文件更大。
实现
思路
- 关系图
bmap()
原本的逻辑:- 判读索引是否小于
NDIRECT
。若小于说明是直接索引,因此可直接返回addrs[]
中的地址(若该地址为0则需要先分配balloc()
) - 若大于则说明是间接索引。将原本的索引减去直接索引数量后得到在间接索引表中的索引。然后通过
addrs[]
获取间接索引表地址(若地址为0则需要分配),该地址需要bread()
读取后存入struct buf b
中。通过读取到的内容和之前得到的间接表索引可以得到真正的地址(若该地址为0则需要分配)。最后不要忘记brelse()
释放读取的缓存。
- 判读索引是否小于
bigfile
后应当增加的逻辑:- 若索引比一级间接索引的范围(256)还要大,说明到了二级索引的范围中。然后需要仿照一级索引读取方式通过
bread()
读取二级索引表,进而读取真正地址。
- 若索引比一级间接索引的范围(256)还要大,说明到了二级索引的范围中。然后需要仿照一级索引读取方式通过
itrunc()
释放函数需要增加对二级间接索引的释放逻辑:仿照一级索引释放,但在释放真正地址之前再通过bread()
读取释放....
实现
由于直接索引数量和最大数量变了,更改一下原来定义的宏:
#define NDIRECT 11 // 原本12 #define NINDIRECT (BSIZE / sizeof(uint)) #define MAXFILE (NDIRECT + NINDIRECT + NINDIRECT * NINDIRECT) // 原本没第三项
改变索引后需要改变一些声明和定义:
struct dinode
中addrs[]
大小由NDIRECT + 1
变为NDIRECT + 2
struct inode
中addrs[]
大小由NDIRECT + 1
变为NDIRECT + 2
bmap()
添加二级索引映射:static uint bmap(struct inode* ip, uint bn) { uint addr, *a; struct buf* bp; // 直接索引不变 if (bn < NDIRECT) { ... } bn -= NDIRECT; // 一级间接索引也不变 if (bn < NINDIRECT) { ... } // 若超出一级索引范围则需要减去一级索引数量才得到二级索引 bn -= NINDIRECT; // 二级索引逻辑 if (bn < NINDIRECT * NINDIRECT) { // 获取索引表地址,若没有则分配 if ((addr = ip->addrs[NDIRECT + 1]) == 0) { addr = balloc(ip->dev); if (addr == 0) return 0; ip->addrs[NDIRECT + 1] = addr; } // 通过地址读取索引表 bp = bread(ip->dev, addr); a = (uint*)bp->data; // bn/NINDIRECT得到在第一个索引表中的索引,若没有则分配 if ((addr = a[bn / NINDIRECT]) == 0) { addr = balloc(ip->dev); if (addr) { // 分配成功后将地址写入该缓存中 a[bn / NINDIRECT] = addr; log_write(bp); } else { // 失败后释放,然后返回0地址 brelse(bp); return 0; } } brelse(bp); // 得到第二个索引表地址后可以释放第一个索引表了 // 通过第二个索引表获取真正的地址 bp = bread(ip->dev, addr); a = (uint*)bp->data; // 可通过bn%NINDIRECT获取在第二个索引表中的索引 if ((addr = a[bn % NINDIRECT]) == 0) { // 若没有则分配。分配成功就写入缓存,不成功之后返回0地址 addr = balloc(ip->dev); if (addr) { a[bn % NINDIRECT] = addr; log_write(bp); } } brelse(bp); return addr; } panic("bmap: out of range"); }
itrunc()
添加释放二级间接索引的逻辑代码:void itrunc(struct inode* ip) { int i, j; // 添加定义用于释放二级间接索引 struct buf *bp, *bpp; uint *a, *aa; // 释放直接索引 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; } // 释放二间接级索引 if (ip->addrs[NDIRECT + 1]) { bp = bread(ip->dev, ip->addrs[NDIRECT + 1]); a = (uint*)bp->data; for (i = 0; i < NINDIRECT; ++i) { if (a[i]) { // 原本直接释放的一级间接索引, // 此时需要再次读取、判断并释放 bpp = bread(ip->dev, a[i]); aa = (uint*)bpp->data; for (j = 0; j < NINDIRECT; ++j) { if (aa[j]) bfree(ip->dev, aa[j]); } brelse(bpp); bfree(ip->dev, a[i]); } } brelse(bp); bfree(ip->dev, ip->addrs[NDIRECT + 1]); } ip->size = 0; iupdate(ip); }
结果
- 运行结果
- 测试结果
Symbolic links (moderate)
在本练习中,您将向 xv6 添加符号链接。符号链接(或软链接)按路径名引用链接的文件; 打开符号链接时,内核会跟随指向引用文件的链接。符号链接类似于硬链接,但硬链接仅限于指向同一磁盘上的文件,而符号链接可以跨磁盘设备。尽管 xv6 不支持多个设备,但实现此系统调用是了解路径名查找工作原理的好练习。
要求
您将实现符号symlink(char *target, char *path)
系统调用,该调用在path
处创建一个新的符号链接,该链接引用target
文件。有关更多信息,请参见手册页symlink
。要进行测试,请将符号链接测试添加到Makefile
并运行它。当测试生成以下输出(包括用户测试成功)时,您的解决方案就完成了。
$ symlinktest
Start: test symlinks
test symlinks: ok
Start: test concurrent symlinks
test concurrent symlinks: ok
$ usertests -q
...
ALL TESTS PASSED
$
提示
- 首先,为
symlink
创建一个新的系统调用序号,在user/usys.pl
,user/user.h
中添加一个条目,并在kernel/sysfile.c
中实现一个空sys_symlink
。 - 将新的文件类型 (
T_SYMLINK
) 添加到kernel/stat.h
以表示符号链接。 - 向
kernel/fcntl.h
添加一个新标志 (O_NOFOLLOW
),该标志可用于open
系统调用。请注意,传递给open
的标志是使用按位或运算符组合的,因此新标志不应与任何现有标志重叠。这将允许您在将user/symlinktest.c
添加到Makefile
后对其进行编译。 - 实现
symlink(target, path)
系统调用,以在path
处创建目标为target
的新符号链接。请注意,target
不需要存在,系统调用就会成功。您需要选择某个位置来存储符号链接的目标路径,例如,在 inode 的数据块中。symlink
应返回一个整数,表示成功(0)或失败(-1),类似于link
和unlink
。 - 修改
open
系统调用以处理路径引用符号链接的情况。如果该文件不存在,则open
必须失败。当进程在标志中指定要open
O_NOFOLLOW
时,open
应打开符号链接(而不是遵循符号链接)。 - 如果链接的文件也是符号链接,则必须递归地跟随它,直到到达非链接文件。如果链接形成循环,则必须返回错误代码。如果链接深度达到某个阈值(例如10),您可以通过返回错误代码来近似这一点。
- 其他系统调用(例如,
link
和unlink
)不得遵循符号链接; 这些系统调用在符号链接本身上运行。 - 本实验中您不必处理指向目录的符号链接。
实现
定义
symlink
系统调用- 在
user.h
中声明 - 在
usys.pl
中添加条目 - 在
syscall.h
中定义系统调用序号 - 在
syscall.c
中声明sys_symlink()
并将其指针放入数组 - 最后在
sysfile.c
中完成sys_symlink()
的定义
- 在
sys_symlink()
的定义。该系统调用的功能是在path
生成一个类型为T_SYMLINK
的文件,并在文件内容中写上target
目标路径。由此可分为三步:(1)获取参数;(2)生成文件;(3)写入内容。uint64 sys_symlink(void) { char target[MAXPATH], path[MAXPATH]; struct inode *ip; int len; // (1) 获取字符参数 if ((len = argstr(0, target, MAXPATH)) < 0 || argstr(1, path, MAXPATH) < 0) { return -1; } // (2) 创建symlink文件 begin_op(); if ((ip = create(path, T_SYMLINK, 0, 0)) == 0) { end_op(); return -1; } // (3) 写入目标路径 // ilock(ip); 加上就会死锁,因为create中已经获取🔒 len = writei(ip, 0, (uint64)target, 0, len); iunlockput(ip); end_op(); return len > 0 ? 0 : -1; }
在
sys_open()
中添加对symlink文件的处理逻辑:sys_open()
原本的处理是:如果是O_CREATE
模式就创建在path
处的inode,否则就获取path
处inode。一个symlink文件一定是在后者的选择分支中,因此可在获取inode后添加对symlink文件处理的代码。uint64 sys_open() { ... if (omode & O_CREATE) { // 创建inode ... } else { // 打开inode if((ip = namei(path)) == 0) { end_op(); return -1; } ilock(ip); // 添加迭代读取symlink文件的逻辑代码 if (ip->type == T_SYMLINK && (omode & O_NOFOLLOW) == 0) { // opensymlink函数迭代读取直到获取真实inode或获取失败 if ((ip = opensymlink(ip)) == 0) { end_op(); return -1; } } if(ip->type == T_DIR && omode != O_RDONLY) { iunlockput(ip); end_op(); return -1; } } }
通过迭代获取真实inode的函数
struct inode* opensymlink(struct inode*)
struct inode* opensymlink(struct inode* ip) { char target[MAXPATH]; const int max_times = 10; // 最大链接次数 int i, times, nums[max_times]; // 通过循环max_times次来控制最大链接次数 for (times = 0; times < max_times; ++times) { nums[times] = ip->inum; // 保存inode num用于判断是否已经迭代过 // 读取target的路径,若失败释放🔒后返回0 if (readi(ip, 0, (uint64)target, 0, MAXPATH) < 0) { iunlockput(ip); return 0; } iunlockput(ip); // 若成功读取,就可以释放该inode。且下一步要切换ip // 打开target路径文件,若失败则返回0 if ((ip = namei(target)) == 0) { return 0; } // 通过inode num判断是否已经被迭代过,若有则说明构成环要及时返回 for (i = 0; i < times; ++i) { if (nums[i] == nums[times]) { printf("links form a cycle!\n"); return 0; } } // 函数进来和循环开始时是上🔒状态,因此这里也需要上锁后再退出函数或下一轮循环 ilock(ip); // 若新打开的inode非T_SYMLINK类型说明获取了真正的inode,返回 if (ip->type != T_SYMLINK) { return ip; } } printf("open: too deep!\n"); return 0; }
结果
- 运行结果
- 测试结果
总结
make grade
个人收获
- xv6的文件系统包括七层,从底层的磁盘层到顶层的描述符层,分别负责不同的功能
- 文件系统通过
struct inode
组织每个文件(在磁盘中为struct dinode
)。该结构体包括文件种类、大小、链接数量以及文件保存数据的数据块地址。具体结构如图 - Unix一切皆文件。无论是设备、目录、链接还是真正的文件,都可以表示为
inode
,只是在访问和读写时操作有所区别。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。