实验概要
本次实验将深入探讨文件系统中的大文件处理和符号链接实现。在任务1中,我们将了解文件在存储设备上的存储方式,并通过修改bmap函数,实现将一个直接块改为双重间接块,以支持大文件的处理。任务2将引导我们了解软硬链接的知识、文件类型的代码实现以及打开文件的逻辑,我们将实现sys_symlink函数用于创建符号链接,并修改open系统调用以正确打开符号链接文件。通过这两个任务,我们将对文件系统的内部机制和操作有更深入的理解和实践。
任务一
首先,我们需要明确当前的问题:在xv6文件系统中,文件大小受到了限制,最大只能有268个数据块,这是因为一个inode包含12个"直接"块号和一个"单间接"块号,它指向一个最多可以容纳256个块号的块,总共就是12+256=268个块。现在我们的目标是修改xv6文件系统的代码,以支持每个inode中包含一个"双间接"块,这个块包含256个单间接块的地址,每个单间接块又可以包含最多256个数据块的地址。这样一来,文件的最大大小将扩展到65803个块。
我们需要仔细阅读xv6书籍中关于文件系统的章节,并研究相应的代码。接着,我们要深入研究struct dinode在fs.h中的定义,特别关注NDIRECT、NINDIRECT和MAXFILE,以及struct dinode中的addrs[]元素。同时,我们需要仔细查看fs.c中的bmap()函数,确保理解其工作原理。bmap()函数在读取和写入文件时都会被调用,当写入文件时,bmap()会根据需要分配新的块来存储文件内容,并在需要时分配一个间接块来存储块地址。
我们的任务是修改bmap()函数,使其实现双重间接块,除了直接块和单间接块之外。为了腾出空间来存放新的双重间接块,我们需要将ip->addrs[]的前11个元素作为直接块,第12个元素作为单间接块,第13个元素作为新的双重间接块。
步骤
修改 inode 数据结构
减少个直接地址块数量, 增加一个间接地址块,
具体地,修改以下的宏定义,将12改为11
#define NDIRECT 11
为了保持dinode指向的数据块数不变, 修改dinode结构体,将+1改为+2,总数为13
uint addrs[NDIRECT+2];
为了方便后续判断给出的逻辑数据块号是在直接块,还是在一级间接块,还是在二级间接块,增加以下的宏定义
#define NDOUBLYINDIRECT (NINDIRECT * NINDIRECT)
顺便修改文件的最大块数
#define MAXFILE (NDIRECT + NINDIRECT + NDOUBLYINDIRECT)
根据提示:
如果你改变了
NDIRECT
的定义,你可能需要在file.h
中改变struct inode
中addrs[]
的声明。确保struct inode
和struct dinode
中的addrs[]
数组有相同数量的元素。
为了保持inode指向的数据块数不变, 修改dinode结构体,将+1改为+2,总数为13
uint addrs[NDIRECT+2];
修改 kernel/fs.c
中的 bmap()
函数
bmap()
函数是一个文件系统中用于映射逻辑块到物理块地址的函数。根据给定的inode和块号,它返回对应的磁盘块地址。如果该块不存在,则会进行分配。
具体来说:
首先检查bn是否小于NDIRECT,如果是,则直接从ip->addrs[]中获取对应的地址,如果不存在则分配一个新的磁盘块。 如果bn大于或等于NDIRECT且小于NINDIRECT,则需要通过间接寻址获取到对应的地址。首先检查是否存在间接块,如果不存在则分配一个新的块,并将其地址保存到ip->addrs[NDIRECT]中。然后通过间接块找到对应的地址,如果不存在则分配一个新的磁盘块,并更新相应的数据和日志。 如果bn超出了NINDIRECT的范围,则报错,提示超出范围。
现在模仿间接块的分配方法, 如果逻辑块号超出了间接块范围,但在双层间接块范围内(小于NDOUBLYINDIRECT),则类似地进行处理,首先获取双层间接块的地址,然后根据逻辑块号的偏移计算出一级间接块的地址,然后再根据二级间接块的地址获取实际的磁盘块地址。如果地址为0,则依次进行分配磁盘块、写入日志等操作。
static uint
bmap(struct inode *ip, uint bn)
{
uint addr, *a; // 定义变量addr和指针a
struct buf *bp; // 定义指向缓冲区结构的指针bp
if(bn < NDIRECT){ // 如果bn小于NDIRECT
if((addr = ip->addrs[bn]) == 0) // 如果ip->addrs[bn]等于0
ip->addrs[bn] = addr = balloc(ip->dev); // 分配一个磁盘块并将其地址赋给ip->addrs[bn]和addr
return addr; // 返回地址
}
bn -= NDIRECT; // 减去NDIRECT,得到间接块的偏移量
if(bn < NINDIRECT){ // 如果bn小于NINDIRECT
// Load indirect block, allocating if necessary.
if((addr = ip->addrs[NDIRECT]) == 0) // 如果ip->addrs[NDIRECT]等于0
ip->addrs[NDIRECT] = addr = balloc(ip->dev); // 分配一个磁盘块并将其地址赋给ip->addrs[NDIRECT]和addr
bp = bread(ip->dev, addr); // 从磁盘读取块,并将其赋给缓冲区指针bp
a = (uint*)bp->data; // 将缓冲区数据转换为无符号整型指针
if((addr = a[bn]) == 0){ // 如果a[bn]等于0
a[bn] = addr = balloc(ip->dev); // 分配一个磁盘块并将其地址赋给a[bn]和addr
log_write(bp); // 写入日志
}
brelse(bp); // 释放缓冲区
return addr; // 返回地址
}
bn -= NINDIRECT;
if(bn < NDOUBLYINDIRECT) {
if((addr = ip->addrs[NDIRECT + 1]) == 0) {
ip->addrs[NDIRECT + 1] = addr = balloc(ip->dev);
}
bp = bread(ip->dev, addr);
a = (uint*)bp->data;
if((addr = a[bn / NINDIRECT]) == 0) {
a[bn / NINDIRECT] = addr = balloc(ip->dev);
log_write(bp);
}
brelse(bp);
bp = bread(ip->dev, addr);
a = (uint*)bp->data;
bn %= NINDIRECT;
if((addr = a[bn]) == 0) {
a[bn] = addr = balloc(ip->dev);
log_write(bp);
}
brelse(bp);
return addr;
}
panic("bmap: out of range"); // 报错,超出范围
}
根据提示
确保
itrunc
释放文件的所有块,包括双重间接块。
需要修改 kernel/fs.c 中的 itrunc() 函数,该函数的作用是释放 inode 的数据块。由于引入了二级间接块的结构,因此在函数中也需要添加对这部分数据块的释放代码。释放方式与一级间接块类似,只需使用两重循环来遍历二级间接块和其中的一级间接块即可。
void
itrunc(struct inode *ip)
{
int i, j, k; // 用于循环计数的变量
struct buf *bp, *bp2; // 缓冲区指针,用于读取和写回磁盘块
uint *a, *a2; // 用于将缓冲区中的数据转换为unsigned int指针
// 释放直接块
for(i = 0; i < NDIRECT; i++){
if(ip->addrs[i]){ // 如果直接块地址存在
bfree(ip->dev, ip->addrs[i]); // 释放该直接块
ip->addrs[i] = 0; // 将对应的直接块地址置为0
}
}
// 释放一级间接块
if(ip->addrs[NDIRECT]){
bp = bread(ip->dev, ip->addrs[NDIRECT]); // 从磁盘读取一级间接块到缓冲区
a = (uint*)bp->data; // 将缓冲区中的数据转换为unsigned int指针
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; // 将一级间接块的地址置为0
}
// 释放双间接块
if(ip->addrs[NDIRECT + 1]) {
bp = bread(ip->dev, ip->addrs[NDIRECT + 1]); // 从磁盘读取双间接块到缓冲区
a = (uint*)bp->data; // 将缓冲区中的数据转换为unsigned int指针
for(j = 0; j < NINDIRECT; ++j) {
if(a[j]) {
bp2 = bread(ip->dev, a[j]); // 从磁盘读取一级间接块到缓冲区
a2 = (uint*)bp2->data; // 将缓冲区中的数据转换为unsigned int指针
for(k = 0; k < NINDIRECT; ++k) {
if(a2[k]) {
bfree(ip->dev, a2[k]); // 释放直接块
}
}
brelse(bp2); // 释放一级间接块的缓冲区
bfree(ip->dev, a[j]); // 释放一级间接块的地址
a[j] = 0; // 将对应的一级间接块地址置为0
}
}
brelse(bp); // 释放双间接块的缓冲区
bfree(ip->dev, ip->addrs[NDIRECT + 1]); // 释放双间接块的地址
ip->addrs[NDIRECT + 1] = 0; // 将双间接块的地址置为0
}
// 将文件大小重置为0
ip->size = 0;
iupdate(ip); // 更新inode信息到磁盘
}
结果
任务二
为了实现符号链接系统调用,需要对文件系统、系统调用和路径名查找等方面有深入的理解。首先,需要分配新的系统调用号并修改相关头文件以支持符号链接系统调用。同时,还需要定义新的文件类型表示符号链接,并在打开文件时处理符号链接的情况。此外,需要递归地跟踪符号链接直到达到非链接文件为止,并防止链接形成循环。最后,还需要确保其他系统调用不会跟随符号链接。
步骤
为symlink
创建一个新的系统调用
添加有关 symlink
系统调用的定义声明. 包括 kernel/syscall.h
, kernel/syscall.c
, user/usys.pl
和 user/user.h
.
添加新的文件类型 T_SYMLINK
到 kernel/stat.h
中,以表示符号链接.
添加新的文件标志位 O_NOFOLLOW
到 kernel/fcntl.h
中.
在kernel/fcntl.h
中添加一个新的标志(O_NOFOLLOW
),可与open
系统调用一起使用. 注意,传递给open
的标志使用按位OR运算符组合,因此新标志不应与任何现有标志重叠, 这里使用二进制100.
在 kernel/sysfile.c
中实现 sys_symlink()
函数
首先从用户空间获取目标文件路径和符号链接路径, 然后通过create()函数创建符号链接路径对应的inode结构,并使用T_SYMLINK类型来与普通文件进行区分。然后通过writei()将链接的目标文件路径写入inode的数据块中.
需要注意的是,create()函数创建inode结构时会持有该inode结构的锁,后续writei()需要在持锁的情况下才能写入.在结束操作后,无论成功与否,都需要调用iunlockput()来释放inode的锁和inode本身。
uint64
sys_symlink(void)
{
char path[MAXPATH], target[MAXPATH];
struct inode *ip;
// 读取参数
if(argstr(0, target, MAXPATH) < 0) // 从用户空间获取目标文件路径
return -1;
if(argstr(1, path, MAXPATH) < 0) // 从用户空间获取符号链接路径
return -1;
// 开启事务
begin_op(); // 开启文件系统操作事务,以确保原子性
// 为这个符号链接新建一个 inode
if((ip = create(path, T_SYMLINK, 0, 0)) == 0) { // 创建一个新的符号链接inode
end_op(); // 如果创建失败,结束文件系统操作事务
return -1; // 返回错误
}
// 在符号链接的 data 中写入被链接的文件
if(writei(ip, 0, (uint64)target, 0, MAXPATH) < MAXPATH) { // 将被链接文件的路径写入符号链接的数据块中
iunlockput(ip); // 解锁并释放inode
end_op(); // 结束文件系统操作事务
return -1; // 返回错误
}
// 提交事务
iunlockput(ip); // 解锁并释放inode
end_op(); // 结束文件系统操作事务
return 0; // 返回成功
}
修改 kernel/sysfile.c
的open
系统调用,以处理路径引用符号链接的情况。
根据提示
如果链接的文件也是一个符号链接,你必须递归地跟随它,直到到达一个非链接文件。如果链接形成一个循环,你必须返回一个错误代码。你可以通过在链接深度达到某个阈值时返回一个错误代码来近似处理这个问题(例如,10)。
和
当进程在
open
的标志中指定O_NOFOLLOW
时,open
应该打开符号链接(而不是遵循符号链接)。
在sys_open函数中添加处理软链接文件的代码:
在获取路径对应的inode指针之后,我们可以使用一个循环来处理软链接文件。循环条件可以是根据文件的type以及打开权限是否为O_NOFOLLOW。同时,设置一个循环深度计数器,如果超过10次则跳出循环,以防止循环软链接。
循环中,读取当前inode中的内容(长度为MAXPATH),并将其作为新的路径再次进行处理。这样我们可以不断追踪软链接直到找到最终的目标文件。
在每一次循环中,需要释放原始ip所指向的inode指针,以避免内存泄漏。
当跳出循环后,ip就指向了最终要打开的文件的inode。
每次循环还要检查路径是否存在,如果不存在则跳出循环。
// 处理符号链接
int depth = 0; // 初始化深度为0,用于记录符号链接的层数
while (ip->type == T_SYMLINK && !(omode & O_NOFOLLOW)) { // 当 inode 的类型为符号链接,并且打开模式不包括 O_NOFOLLOW 标志时执行循环
if (depth++ >= 10) { // 如果深度超过10层,放弃处理,解锁 inode 并结束操作,返回错误
iunlockput(ip);
end_op();
return -1;
}
if (readi(ip, 0, (uint64)path, 0, MAXPATH) < MAXPATH) { // 从符号链接的 inode 中读取数据到 path 缓冲区,如果读取的数据长度小于 MAXPATH,则解锁 inode 并结束操作,返回错误
iunlockput(ip);
end_op();
return -1;
}
iunlockput(ip); // 解锁当前 inode
if ((ip = namei(path)) == 0) { // 根据读取到的路径名查找对应的 inode,若失败则结束操作,返回错误
end_op();
return -1;
}
ilock(ip); // 锁定新的 inode,准备处理下一层符号链接
}
最后在 Makefile
中添加对测试文件 symlinktest.c
的编译.
结果
实验总结
在本次实验中,我们深入学习了文件在存储设备上的存储方式,特别是通过dinode结构体来保存数据块地址的方式。我们了解到,通过将一个数据块划分成256个数据地址,每个地址指向一个数据块,可以增大可存储的数据大小。更进一步,我们可以通过双重间接块的方式,将一个直接数据块划分出256个数据地址,每个地址再次指向一个数据块,以此类推,从而进一步增大最大文件大小。
在任务二中,我们学习了软硬链接的知识,并基于此为xv6系统添加了创建符号链接的功能。我们通过新增的系统调用,接收用户态传递的两个文件路径字符串参数,在一个文件路径下创建一个符号链接文件,指向另一个目标文件路径。为了区分其他文件类型,我们新增了一种文件类型。
在实现创建符号链接的系统调用后,为了能够正确地打开这种新类型的文件,我们需要修改打开文件的系统调用逻辑。由于符号链接文件的特殊性,它有两种不同的打开方式:一种是直接打开符号链接文件本身,另一种是根据链接找到目标文件并打开。因此,在打开文件时,我们需要根据O_NOFOLLOW字段的情况来选择相应的inode结构体地址。
通过本次实验,我们不仅学习了文件存储方式的基本原理,还深入了解了软硬链接的概念,并掌握了如何在操作系统中添加新的系统调用和修改文件打开逻辑的方法。