Lab地址:pdos.csail.mit.edu/6.828/2021/…
git代码地址:github.com/cardchoosen…
Lab: file system
Large files
In this assignment you'll increase the maximum size of an xv6 file. Currently xv6 files are limited to 268 blocks, or 268*BSIZE bytes (BSIZE is 1024 in xv6). This limit comes from the fact that an xv6 inode contains 12 "direct" block numbers and one "singly-indirect" block number, which refers to a block that holds up to 256 more block numbers, for a total of 12+256=268 blocks.
在这个作业中,你将增加 xv6 文件的最大大小。目前,xv6 文件被限制在 268 个块,即 268×BSIZE 字节(在 xv6 中,BSIZE 是 1024)。这个限制源于这样一个事实:一个 xv6 索引节点包含 12 个 “直接” 块编号和一个 “单间接” 块编号,后者指向一个块,该块最多可容纳 256 个更多的块编号,总共为 12 + 256 = 268 个块。
类似FAT文件系统,xv6的文件系统中的inode结构体采用了混合索引的方式记录数据所在的具体磁盘块号。每个文件所占用的前12个磁盘块为直接索引,记录在inode中(每个磁盘快1024字节),对于任何小于12KB的文件,都可以直接访问inode得到磁盘块号。称为直接记录盘块。
对于超出12KB部分的文件,会分配一个额外的一级索引表(1024Byte),用于存储这部分数据所在的盘块号。一个盘块号占用4个字节,所以一级索引表可以包含1024 / 4 = 256个盘块号,再加上inode中12个直接索引,一个文件最多可以使用12+256=268个盘块,也就是268KB。
inode结构如下,NDIRECT = 12:
// kernel/fs.h
// On-disk inode structure
struct dinode {
short type; // File type
short major; // Major device number (T_DEVICE only)
short minor; // Minor device number (T_DEVICE only)
short nlink; // Number of links to inode in file system
uint size; // Size of file (bytes)
uint addrs[NDIRECT+1]; // Data block addresses
};
显然268KB的文件大小限制对于操作系统来说太严苛了,我们需要扩展dinode结构,增加一个二级索引来扩大能够支持的文件大小。
首先修改struct inode 和struct dinode,这里说明一下inode和dinode的区别。
功能上:
- inode通常是内存中的索引节点数据结构,代表了文件系统中的一个文件或目录的元数据,这些元数据包括但不限于文件的大小、文件类型(普通文件、目录、符号链接等)、文件的权限、文件的所有者、文件的时间戳(创建时间、修改时间、访问时间)、指向文件数据块的指针等。
- dinode通常是存储在磁盘上的索引节点的数据结构,是磁盘上文件元数据的持久存储形式。它存储了文件的基本信息,这些信息在文件系统创建文件或目录时被写入磁盘,并且在文件系统的生命周期内持续存在。
存储位置上:
- inode 主要存储在内存中,是操作系统为了方便文件操作而在内存中维护的数据结构。
- dinode 存储在磁盘上,是文件元数据的持久化存储形式。
结构上:
- inode 可能包含一些内存管理和性能优化相关的信息,这些信息在文件系统运行时使用,但不会存储在磁盘上,例如缓存的数据、文件的使用状态等。
- dinode 通常只包含文件的基本信息,确保在磁盘上以最精简的方式存储文件的重要信息,以节省磁盘空间和提高文件系统的存储效率。
生命周期上:
- inode 的生命周期通常是从文件被打开或操作开始,直到文件关闭或系统需要回收内存资源。
- dinode 的生命周期与文件的存在周期相关,只要文件存在于文件系统中,其对应的 dinode 就会存储在磁盘上。
将NDIRECT 从12减少为11,MAXFILE宏增大NINDIRECT * NINDIRECT,腾出inode中的空间来存储二级索引的索引表盘块号。
// kernel/fs.h
#define NDIRECT 11
#define NINDIRECT (BSIZE / sizeof(uint))
#define MAXFILE (NDIRECT + NINDIRECT + NINDIRECT * NINDIRECT)
// On-disk inode structure
struct dinode {
short type; // File type
short major; // Major device number (T_DEVICE only)
short minor; // Minor device number (T_DEVICE only)
short nlink; // Number of links to inode in file system
uint size; // Size of file (bytes)
uint addrs[NDIRECT+2]; // Data block addresses
};
// kernel/file.h
// in-memory copy of an inode
struct inode {
uint dev; // Device number
uint inum; // Inode number
int ref; // Reference count
struct sleeplock lock; // protects everything below here
int valid; // inode has been read from disk?
short type; // copy of disk inode
short major;
short minor;
short nlink;
uint size;
uint addrs[NDIRECT+2];
};
修改bmap和itrunc,bmap用于获取inode中第bn(bn为逻辑块号)个块的物理块号。itrunc用于释放inode中所有使用的数据块。
这里说明一下逻辑块号和物理块号的区别,可以把直接索引+一级索引+二级索引所构成的所有索引块,想象成一个块数组,逻辑块号对应的就是数组下标,通过逻辑块号来判断是直接索引或是一级索引,或是二级索引,然后从索引中,取出真正的物理块号。而物理块号对应磁盘块上真正的物理存储区域。inode中的addrs[NDIRECT+2]可以理解成是逻辑块号-物理块号的映射表。
xv6中的代码现在只能获取直接索引和一级索引的磁盘块,itrunc也是,只需要按照它的代码扩展即可,让bmap和itrunc意识到二级索引的存在。
// fs.c
// Inode content
//
// The content (data) associated with each inode is stored
// in blocks on the disk. The first NDIRECT block numbers
// are listed in ip->addrs[]. The next NINDIRECT blocks are
// listed in block ip->addrs[NDIRECT].
// Return the disk block address of the nth block in inode ip.
// If there is no such block, bmap allocates one.
static uint
bmap(struct inode *ip, uint bn)
{
uint addr, *a;
struct buf *bp;
// 对于小于 NDIRECT 的逻辑块号,使用直接映射
if(bn < NDIRECT){
// 检查该逻辑块对应的物理块地址是否为 0,如果为 0,则调用 balloc 函数分配一个新的物理块,并更新 inode 的地址表
if((addr = ip->addrs[bn]) == 0)
ip->addrs[bn] = addr = balloc(ip->dev);
return addr;
}
// 逻辑块号超出直接映射范围,进入间接映射部分,更新逻辑块号
bn -= NDIRECT;
// 对于小于 NINDIRECT 的逻辑块号,使用一级间接映射
if(bn < NINDIRECT){
// 检查一级间接块的地址是否为 0,如果为 0,则调用 balloc 函数分配一个新的物理块作为一级间接块,并更新 inode 的地址表
if((addr = ip->addrs[NDIRECT]) == 0)
ip->addrs[NDIRECT] = addr = balloc(ip->dev);
// 读取一级间接块的数据
bp = bread(ip->dev, addr);
a = (uint*)bp->data;
// 检查间接块中存储的物理块地址是否为 0,如果为 0,则调用 balloc 函数分配一个新的物理块,并更新间接块中的地址
if((addr = a[bn]) == 0){
a[bn] = addr = balloc(ip->dev);
log_write(bp);
}
// 释放一级间接块的 buf 结构
brelse(bp);
return addr;
}
// 逻辑块号超出一级间接映射范围,进入二级间接映射部分,更新逻辑块号
bn -= NINDIRECT;
// 对于小于 NINDIRECT*NINDIRECT 的逻辑块号,使用二级间接映射
if(bn < NINDIRECT*NINDIRECT){
// 检查二级间接块的地址是否为 0,如果为 0,则调用 balloc 函数分配一个新的物理块作为二级间接块,并更新 inode 的地址表
if((addr = ip->addrs[NDIRECT+1]) == 0)
ip->addrs[NDIRECT+1] = addr = balloc(ip->dev);
// 读取二级间接块的数据
bp = bread(ip->dev, addr);
a = (uint*)bp->data;
// 检查二级间接块中存储的一级间接块的地址是否为 0,如果为 0,则调用 balloc 函数分配一个新的物理块作为一级间接块,并更新二级间接块中的地址
if((addr = a[bn/NINDIRECT]) == 0){
a[bn/NINDIRECT] = addr = balloc(ip->dev);
log_write(bp);
}
// 释放二级间接块的 buf 结构
brelse(bp);
// 计算在一级间接块中的偏移量
bn %= NINDIRECT;
// 读取一级间接块的数据
bp = bread(ip->dev, addr);
a = (uint*)bp->data;
// 检查一级间接块中存储的物理块地址是否为 0,如果为 0,则调用 balloc 函数分配一个新的物理块,并更新一级间接块中的地址
if((addr = a[bn]) == 0){
a[bn] = addr = balloc(ip->dev);
log_write(bp);
}
// 释放一级间接块的 buf 结构
brelse(bp);
return addr;
}
// 如果逻辑块号超出二级间接映射范围,程序出现错误,调用 panic 函数
panic("bmap: out of range");
}
...
// Truncate inode (discard contents).
// Caller must hold ip->lock.
void
itrunc(struct inode *ip)
{
int i, j;
struct buf *bp;
uint *a;
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(j = 0; j < NINDIRECT; j++){
if(a[j]) {
struct buf *bp2 = bread(ip->dev, a[j]);
uint *a2 = (uint*)bp2->data;
for(int k = 0; k < NINDIRECT; k++){
if(a2[k])
bfree(ip->dev, a2[k]);
}
brelse(bp2);
bfree(ip->dev, a[j]);
}
}
brelse(bp);
bfree(ip->dev, ip->addrs[NDIRECT+1]);
ip->addrs[NDIRECT + 1] = 0;
}
ip->size = 0;
iupdate(ip);
}
Make clean重置xv6的文件映像fs.img,然后make qemu验证
init: starting sh
$ bigfile
..................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................
wrote 65803 blocks
bigfile done; ok
Symbolic links
Symbolic links即符号链接,在linux中,符号链接使用如下:
ln -s /path/to/target /path/to/symlink
target是目标文件或目录的路径,symlink是要创建的符号链接的路径,之后当用户或程序访问符号链接时,会自动重定向到它所指向的文件或目录,在这里的表现就是,访问symlink实际在访问target。
符号链接也被称为软链接soft link,是一种特殊的文件类型。符号链接可以跨越不同的文件系统,因为它只存储了一个路径,而不是像硬链接那样直接指向文件的inode。
这里继续说明符号链接和硬链接的区别:
硬链接直接指向文件的 inode,多个硬链接指向同一个文件实际上是多个目录项指向同一个 inode,它们共享文件的数据和属性,并且它们的地位是等同的,删除其中一个硬链接不会影响文件的存在,只有当所有硬链接和原始文件都被删除,文件的数据才会被删除,硬链接只能链接到文件,不能链接到目录,并且不能跨越文件系统,因为不同文件系统有不同的 inode 空间。
符号链接只是存储了一个指向目标文件或目录的路径,它有自己独立的 inode,删除符号链接不会影响目标文件或目录的存在,只删除了这个指向的关系。符号链接可以指向文件或目录,并且可以跨越文件系统,因为它不依赖于目标文件的 inode,而是依赖于路径。
这里需要我们为xv6添加symlink系统调用,关于添加系统调用的过程在Lab2中有详细描述,这里只给出过程。
user/user.h中注册symlink
// user.h
int symlink(char *target, char *path);
user/usys.pl中添加系统调用存根
// usys.pl
entry("symlink");
在kernel/syscall.h中定义系统调用号
// syscall.h
#define SYS_symlink 22
在kernel/syscall.c中使用extern定义系统调用函数,并将其调用号-调用函数指针的映射关系加入syscalls中
// syscall.c
extern uint64 sys_symlink(void);
static uint64 (*syscalls[])(void) = {
[SYS_fork] sys_fork,
...
[SYS_symlink] sys_symlink,
};
添加需要使用的宏定义
// kernel/fcntl.h
#define O_NOFOLLOW 0x800
// kernel/stat.h
#define T_SYMLINK 4 // Symbolic link
在sysfile.c中实现sys_symlink系统调用,新建一个inode结构体ip,获取目标path和链接path,将目标path写入inode第0个块中。
// kernel/sysfile.c
uint64
sys_symlink(void)
{
struct inode *ip;
char target[MAXPATH], path[MAXPATH];
if (argstr(0, target, MAXPATH) < 0 || argstr(1, path, MAXPATH) < 0)
return -1;
begin_op();
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;
}
修改sys_open,使其在遇到符号链接时,可以递归处理符号链接,且在符号链接出现环路时报错退出,这里的判断方式比较简单,当层级超过10时,直接退出。
// kernel/sysfile.c
uint64
sys_open(void)
{
...
if (omode & O_CREATE)
{
...
}
else
{
int symlink_depth = 0;
while (1)
{
if ((ip = namei(path)) == 0)
{
end_op();
return -1;
}
ilock(ip);
if (ip->type == T_SYMLINK && (omode & O_NOFOLLOW) == 0)
{
if (++symlink_depth > 10)
{
iunlockput(ip);
end_op();
return -1;
}
if (readi(ip, 0, (uint64)path, 0, MAXPATH) < 0)
{
iunlockput(ip);
end_op();
return -1;
}
iunlockput(ip);
}
else
{
break;
}
}
if (ip->type == T_DIR && omode != O_RDONLY)
{
iunlockput(ip);
end_op();
return -1;
}
}
...
iunlock(ip);
end_op();
return fd;
}
Makefile中添加symlinktest
UPROGS=\
...
$U/_symlinktest\
重新编译验证
$ symlinktest
Start: test symlinks
test symlinks: ok
Start: test concurrent symlinks
test concurrent symlinks: ok
实验完成,make grade验证(验证过程可能会有些长,耐心):
$ make qemu-gdb
running bigfile: OK (141.0s)
== Test running symlinktest ==
$ make qemu-gdb
(1.3s)
== Test symlinktest: symlinks ==
symlinktest: symlinks: OK
== Test symlinktest: concurrent symlinks ==
symlinktest: concurrent symlinks: OK
== Test usertests ==
$ make qemu-gdb
usertests: OK (231.8s)
== Test time ==
time: OK
Score: 100/100