Intro
在这个实验中,我们需要让xv6支持更大的文件和软链接。实验总体不是特别难,不过需要我们理解好文件系统是如何工作的。Lab8 lock
中的Buffer Cache也是文件系统的一部分,不过它位于文件系统的下层,这里我们需要处理的更多在上层,偏应用层。
Large files
如何扩大单个文件的大小上限?
要求我们扩大单个文件的大小,实现机制就是通过修改inode的块索引,将原来的12个直接索引和一个一级间接索引变为11个直接索引、一个一级间接索引和一个二级间接索引。
在xv6中,一个数据块的大小为1KB
,由于间接块中存放的是下一个块的地址(其实个人认为更准确的说是块号),使用的类型是uint
,那么一个间接块能存放这样的地址共1KB / sizeof(uint) = 1KB / 4B = 256
个。
这样的话,单个文件大小最大为12 * 1KB + 256 * 1KB = 268KB
变为了11 * 1KB + 256 * 1KB + 256 * 256 * 1KB = 65803KB
。
修改inode结构体
其数据块地址中:有11个直接块地址,1个一级间接块地址,1个二级间接块地址
在原本的代码中,表示直接块数量的宏为NDIRECT
,其定义在fs.h
中,我们需要修改其值为11,并且由于我们扩大了文件大小上限,所以也要修改对应的宏MAXFILE
,扩大后的应该为:NDIRECT + NINDIRECT + NINDIRECT * NINDIRECT
;同时还要修改inode结构体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #define NDIRECT 11 #define NINDIRECT (BSIZE / sizeof(uint)) #define MAXFILE (NDIRECT + NINDIRECT + NINDIRECT * NINDIRECT) struct dinode { short type; short major; short minor; short nlink; uint size; uint addrs[NDIRECT+2 ]; };
内存中保存的inode结构体也需要修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 struct inode { uint dev; uint inum; int ref; struct sleeplock lock ; int valid; short type; short major; short minor; short nlink; uint size; uint addrs[NDIRECT+2 ]; };
修改bmap函数
bmap
函数会根据传入参数的数据块号来返回其对应的地址
我们在bmap中看到如果数据块还没有分配的时候,会使用balloc
来分配,并返回分配的块的块号 。
这个函数我一开始看的时候对于如何分配的比较奇怪,后来在AI的帮助下慢慢理解了。在xv6中的硬盘布局是这样的:
1 [ boot block | super block | log | inode blocks | free bit map | data blocks]
在学习操作系统时,我们有学到过一个叫位图的东西,将多个块合并作为一个位图,其中的每一位(bit)用来唯一表示一个数据块是否被使用了(例如0表示为使用,1表示使用)。在xv6中,我们在需要分配数据块时,会去位图中寻找可用的数据块的编号,在去获取对应的数据块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 static uintballoc (uint dev) { int b, bi, m; struct buf *bp ; bp = 0 ; for (b = 0 ; b < sb.size; b += BPB){ bp = bread(dev, BBLOCK(b, sb)); for (bi = 0 ; bi < BPB && b + bi < sb.size; bi++){ m = 1 << (bi % 8 ); if ((bp->data[bi/8 ] & m) == 0 ){ bp->data[bi/8 ] |= m; log_write(bp); brelse(bp); bzero(dev, b + bi); return b + bi; } } brelse(bp); } printf ("balloc: out of blocks\n" ); return 0 ; }
要实现大文件的bmap,我们可以仿照原本的bmap是如何处理一级间接块的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 static uintbmap (struct inode *ip, uint bn) { ... bn -= NDIRECT; if (bn < NINDIRECT){ if ((addr = ip->addrs[NDIRECT]) == 0 ){ addr = balloc(ip->dev); if (addr == 0 ) return 0 ; ip->addrs[NDIRECT] = addr; } bp = bread(ip->dev, addr); a = (uint*)bp->data; if ((addr = a[bn]) == 0 ){ addr = balloc(ip->dev); if (addr){ a[bn] = addr; log_write(bp); } } brelse(bp); return addr; } ... }
同样的,二级间接块也是类似的操作手法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 static uintbmap (struct inode *ip, uint bn) { ... 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; } int level1 = bn / NINDIRECT; int level2 = bn % NINDIRECT; bp = bread(ip->dev, addr); a = (uint *)bp->data; if ((addr = a[level1]) == 0 ){ a[level1] = addr = balloc(ip->dev); log_write(bp); } brelse(bp); bp = bread(ip->dev, addr); a = (uint *)bp->data; if ((addr = a[level2]) == 0 ) { a[level2] = addr = balloc(ip->dev); log_write(bp); } brelse(bp); return addr; } }
多的步骤就是二级间接索引需要多一次读取数据块和查找:
假设传入bmap的块号为524,那么其不在直接块中,也不在一级间接块中,而是在二级间接块中。通过bmap中的前两个if之后对于bn的减法操作,此时的bn = 524 - 11 - 256 = 257。那么在二级间接块的第一层中(level1),索引为257 / 256 = 1,在第二层中(level2)索引为257 % 256 = 1 。如下图所示:
Symbolic links
首先要搞清楚软硬链接的区别:
软链接(符号链接) :
软链接是一种特殊的文件,它包含指向另一个文件或目录的路径。它类似于Windows中的快捷方式。
存储的是目标文件或目录的路径信息。
硬链接 :
硬链接是文件系统中的一个普通文件名,只是它与其他文件名指向同一个物理文件数据块。
存储的是目标文件的数据本身,不包含路径信息。
那么要我们实现软链接,我们需要做的事情有两个:
实现软链接的系统调用
处理使用open打开一个软链接文件
系统调用的实现
实验中让我们实现的系统调用接受连个参数(char *target, char *path)
,它在path
处创建了一个软链接,该链接指向文件名为target
的文件。
如何注册系统调用啥的这里就不赘述了。
首先我们需要准备一些宏:
1 2 3 4 5 #define T_SYMLINK 4 #define O_NOFOLLOW 0x800
实现sys_symlink
系统调用:
我们创建一个软链接时需要使用create
新建一个inode,因为软链接也是一个文件;然后调用writei
将目标路径的字符串target写入软链接文件的数据块。
需要注意的是在完成这两步之后需要使用iunlockput(ip)
来释放在create
中加上的锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 uint64 sys_symlink (void ) { char target[MAXPATH], path[MAXPATH]; if (argstr(0 , target, MAXPATH) < 0 || argstr(1 , path, MAXPATH) < 0 ) { return -1 ; } begin_op(); struct inode *ip = create(path, T_SYMLINK, 0 , 0 ); if (ip == 0 ) { end_op(); return -1 ; } if (writei(ip, 0 , (uint64)target, 0 , strlen (target)) < 0 ) { end_op(); return -1 ; } iunlockput(ip); end_op(); return 0 ; }
处理open一个软链接的情况
这里可能是这个lab的一个难点了。为什么我们需要处理这样的情况呢?
还记得我们刚刚准备的一个宏O_NOFOLLOW
吗,在Linux中,它有这样些特点:
符号链接检查 :当O_NOFOLLOW
标志被设置时,如果指定的文件名是一个软链接,打开操作将不会跟随这个链接,而是会失败,并返回一个错误。
错误返回 :如果尝试打开一个软链接,并且O_NOFOLLOW
标志被设置,系统会返回ELOOP
错误,表示遇到了太多的符号链接。
用途 :这个标志通常用于安全目的,防止应用程序无意中打开一个软链接,这可能被恶意用户用来绕过安全限制。
例如:
1 int fd = open("/path/to/symlink" , O_RDONLY | O_NOFOLLOW);
如果/path/to/symlink
是一个软链接,这个调用将失败。
我们需要在sys_open
中处理:当需要打开的文件类型是软链接类型并且没有使用O_NOFOLLOW
标志时,递归找到最终的目标文件。为什么说是“递归”呢,因为可能有这样一种情况:一个软链接指向了另一个软链接,甚至如此以往。为了避免“子子孙孙无穷无尽”的情况,我们需要设置一个递归层数的限制,当然,我们这里使用循环来代替递归。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 uint64 sys_open (void ) { ... if (ip->type == T_DEVICE && (ip->major < 0 || ip->major >= NDEV)){ ... } if (ip->type == T_SYMLINK && !(omode & O_NOFOLLOW)) { for (int i = 0 ; i < 10 ; i++) { if (readi(ip, 0 , (uint64)path, 0 , strlen (path)) < 0 ) { iunlockput(ip); end_op(); return -1 ; } iunlockput(ip); ip = namei(path); if (ip == 0 ) { end_op(); return -1 ; } ilock(ip); if (ip->type != T_SYMLINK) { break ; } } if (ip->type == T_SYMLINK) { iunlockput(ip); end_op(); return -1 ; } } if ((f = filealloc()) == 0 || (fd = fdalloc(f)) < 0 ){ ... } ... }
Code Detail
代码实现详情请见Github:
Reference
https://pdos.csail.mit.edu/6.S081/2023/labs/fs.html
https://xv6.dgs.zone/labs/answers/lab9.html
Summary
文件系统是一个有意思的东西,同时也很复杂,xv6中设计了一个简化的文件系统,并采用了分层的设计。虽然这里将实验做完了,但是我觉得自己还有很多细节没有搞懂。例如其中的日志层,其是如何实现crash之后能恢复过来的代码是如何设计编写的,还有其中的锁的获取与释放等等。希望之后能结合实际再分析分析。