MIT6.s081_Lab9 fs: File system
MIT6.s081 Lab9:file system
不太顺利的一个Lab,Large files写的还行,写Symbolic links卡了很久,全是细节。
代码
这里补充一点,xv6里的cache似乎是在disk中,我理解的是在内存中,好像现在有那种带缓存的固态和不带的缓存的固态,可能区别在这里。
1. Large files
每个inode中有文件的一些信息,真正存放文件的地方放在inode的addr中,addr存放块的块号,xv6的addr数组大小为13,其中12个为直接索引,1个位间接索引,要创建更大的文件,增加块数即可。实验要求是将其中一个直接索引换成二级间接索引。
-
修改参数。
#define NDIRECT 11 #define NINDIRECT (BSIZE / sizeof(uint)) #define NNINDIRECT ((BSIZE / sizeof(uint)) * (BSIZE / sizeof(uint))) #define MAXFILE (NDIRECT + NINDIRECT + NNINDIRECT) struct dinode { …… uint addrs[NDIRECT+2]; // Data block addresses }; struct inode { …… uint addrs[NDIRECT+2]; }; -
修改
bmap,增加映射空间,这里耽误了一点时间,忘记了将bp写入log,还是细节问题,对文件系统不够熟悉。static uint bmap(struct inode *ip, uint bn) { …… if(bn < NINDIRECT){ …… } bn -= NINDIRECT; if(bn < NNINDIRECT){ if((addr = ip->addrs[NDIRECT + 1]) == 0) ip->addrs[NDIRECT + 1] = addr = balloc(ip->dev); bp = bread(ip->dev, addr); int indirect = (int)(bn / NINDIRECT); a = (uint*)bp->data; if((addr = a[indirect]) == 0){ a[indirect] = addr = balloc(ip->dev); log_write(bp); } brelse(bp); bp = bread(ip->dev, addr); bn %= NINDIRECT; a = (uint*)bp->data; if((addr = a[bn]) == 0){ a[bn] = addr = balloc(ip->dev); log_write(bp); } brelse(bp); return addr; } panic("bmap: out of range"); } -
修改
itrunc,主要是问了清除文件的时候也能将大文件的二级映射也清除,逻辑和前面清除一级的很像。void itrunc(struct inode *ip) { int i, j, k; struct buf *bp, *ibp; uint *a,*b; for(i = 0; i < NDIRECT; i++){ …… } if(ip->addrs[NDIRECT]){ …… } if(ip->addrs[NDIRECT + 1]){ bp = bread(ip->dev, ip->addrs[NDIRECT + 1]); a = (uint*)bp->data; for(j = 0; j < NINDIRECT; j++){ ibp = bread(ip->dev, a[j]); b = (uint*)ibp->data; for(k = 0; k < NINDIRECT; k++) { if(b[k]) bfree(ip->dev, b[k]); } brelse(ibp); bfree(ip->dev, a[j]); } brelse(bp); bfree(ip->dev, ip->addrs[NDIRECT + 1]); ip->addrs[NDIRECT + 1] = 0; } ip->size = 0; iupdate(ip); }
2. Symbolic links
这个问题需要对fs.c中的函数了解有一定程度,否则写起来比较吃力,也不算困难,但是每次错误的释放都很关键。就像实验开头说的,可以理解路径名查找的工作原理。
-
先添加一些参数和函数信息(做到这里应该是比较简单了)。
Makefile$U/_symlinktest\kernel/fcntl.h#define O_NOFOLLOW 0x800kernel/stat.h#define T_SYMLINK 4 // Symlinkkernel/syscall.cextern uint64 sys_symlink(void); [SYS_symlink] sys_symlink,kernel/syscall.h#define SYS_symlink 22user/user.hint symlink(char *target, char *path);user/usys.plentry("symlink"); -
剩下的就是增加这个系统调用的函数本体,以及修改
open这个系统调用了,刚开始不熟悉,去看了看源码,然后回来重写的,一直在死锁,以为是sys_symlink实现的有问题,没想到两次都出现在open的问题上。sys_symlink实现,target是链接的目标路径,存放在文件的第一个块中,path就是创建的软链接路径,name是文件的名字。这里来来回回改了几遍,也算比较熟悉了。uint64 sys_symlink(void) { char path[MAXPATH], target[MAXPATH], name[DIRSIZ]; struct inode *ip, *dp; int n; //提取参数 if((n = argstr(0, target, MAXPATH)) < 0 || argstr(1, path, MAXPATH) < 0) return -1; begin_op();//创建事务 if((dp = nameiparent(path, name)) == 0){ //获取父目录inode,和名字name,这里增加了父目录的引用个数 end_op();//这里获取父目录失败了,直接结束事务 return -1; } ilock(dp);//对父目录上锁, if((ip = dirlookup(dp, name, 0)) != 0){//查看目录下是否有同名文件 iunlockput(dp);//释放父目录锁并减少引用计数 iput(ip);//有同名文件就增加了一次引用,并不需要,所以减少一次 end_op();//别忘记结束事务 return -1; } if((ip = ialloc(dp->dev, T_SYMLINK)) == 0){ //分配一个inode,这里的dinode信息是不加载到内存中的,同时valid是0 iunlockput(dp); // 分配失败,记得释放 dp,减少引用 end_op();//结束事务 return -1; } ilock(ip);//对inode上锁,如果valid为0,加载dinode信息到内存 ip->major = 0; ip->minor = 0; ip->nlink = 1; if(writei(ip, 0, (uint64)target, 0, n) <= 0){//写入路径到第一个块 ip->nlink = 0;//写入目标路径失败,释放文件 iupdate(ip);//更新log中的dinode,主要有logwrite这一步,写到磁盘 iunlockput(ip);//解锁减少引用 iunlockput(dp); end_op(); return -1; } if(dirlink(dp, name, ip->inum) < 0){//将文件(name,inode号)放到目录中 ip->nlink = 0; iupdate(ip); iunlockput(ip); iunlockput(dp); end_op(); return -1; } iupdate(ip);//这个update放到ip->nlink = 1;之后也可以 iunlockput(ip); iunlockput(dp); end_op(); return 0; }修改
sys_open,因为现在需要打开软链接的文件,有两种情况,一种是直接打开软链接,数据就是target path,另一种是打开软链接指向的文件,这里需要处理一下,打开软链接指向的文件。增加下面这一段
if(ip->type == T_SYMLINK && !(omode & O_NOFOLLOW)) { //判断不是nofollow,就代表要读取指向的文件 struct buf *bp; uint accessed_inode[30];//记录访问过的dinode,防止出现循环。这里只设置了30 int i = 0; accessed_inode[i++] = ip->inum;//先记录当前的文件的dinode号 while(ip->type == T_SYMLINK) {//循环到最后的指向的文件 uint addr = ip->addrs[0];//获取第一个块。 bp = bread(ip->dev, addr);//读取块的内容到buf,同时获取睡眠锁 iunlockput(ip);//释放ip的锁,并减少引用,因为下面要更新ip了 if((ip = namei((char*)bp->data)) == 0){//获取路径的inode brelse(bp); end_op(); return -1; } brelse(bp); //注意释放buf的引用,这里就除了问题,如果不释放的话,这个buf就会一直锁死 ilock(ip);//获取新的inode的锁,防止其他文件访问 for(int j = 0; j < i; j++) {//判断是否有循环出现,出现了就退出 if(ip->inum == accessed_inode[j]){//这里注意资源释放,第二次出问题 //之前直接返回-1,没有对已经上锁的inode释放锁,也没有结束事务,导致死锁 iunlockput(ip); end_op(); return -1; } } accessed_inode[i++] = ip->inum;//增加访问记录 } }主要还是细节问题,对
file system有一定的了解后还是不难的。
3. 附录:fs.c主要函数
先说明一下,
inode是内存中的文件存在形式,dinode才是存放在磁盘中的数据。inum一般是dinode在磁盘中的位置。下面的都是AI写的,自己手写的懒得打了,而且文笔没有AI好。lab9_note_write自看。
xv6 kernel/fs.c 函数详解
fs.c 文件负责管理文件系统的磁盘布局。它处理底层的块分配、inode 分配,并实现从inode 读写数据等核心功能。
关键磁盘数据结构 (引用自 kernel/fs.h)
在分析函数之前,必须了解它们操作的核心磁盘结构:
struct superblock sb: 文件系统的“元数据”,位于磁盘的第1块。它记录了文件系统的总大小、数据块数量、inode 数量以及日志区的起始位置。struct dinode: inode 在磁盘 (Disk) 上的表示形式。它包含了文件的类型、大小、硬链接数以及指向数据块的地址数组 (addrs[])。- 位图 (Bitmaps): 用于跟踪 inode 和数据块的分配情况。某一位是
1表示对应的块/inode 已被使用,0表示空闲。
主要函数分析
1. readsb(int dev, struct superblock *sb)
- 作用: 读取超级块 (Read Superblock)。
- 参数:
int dev: 设备号,指明要从哪个磁盘设备读取。struct superblock *sb: 一个指向内存中superblock结构体的指针,函数会将从磁盘读取的数据填充到这里。
- 核心逻辑:
- 调用
bread(dev, 1)从设备的第1块(超级块所在的位置)读取数据到缓冲区。 - 使用
memmove()将缓冲区中的数据拷贝到sb指针指向的结构体中。 - 释放缓冲区 (
brelse)。
- 调用
- 引用:
bread(),brelse()(来自bio.c)。
2. fsinit(int dev)
- 作用: 初始化文件系统 (File System Initialize)。
- 参数:
int dev: 要初始化的文件系统所在的设备号。
- 核心逻辑:
- 调用
readsb()读取超级块,了解文件系统的整体布局。 - 调用
initlog(dev, &sb)初始化日志系统,将超级块中的日志信息传递给日志层。
- 调用
3. balloc(uint dev)
- 作用: 分配一个空闲的数据块 (Block Allocate)。
- 参数:
uint dev: 设备号。
- 核心逻辑:
- 在一个循环中,遍历磁盘上所有的块位图 (block bitmap) 块。
- 对于每一个位图块,调用
bread()将其读入内存。 - 在位图块内部,逐位检查,寻找一个值为
0的位(表示空闲)。 - 一旦找到,将该位置
1(标记为已使用)。 - 调用
log_write()将修改后的位图块写入日志。 - 释放位图块的缓冲区 (
brelse)。 - 返回新分配的数据块的块号。
- 引用:
BBLOCK()(计算位图块位置的宏),bread(),log_write(),brelse()。
4. bfree(int dev, uint b)
- 作用: 释放一个数据块 (Block Free)。
- 参数:
int dev: 设备号。uint b: 要释放的数据块的块号。
- 核心逻辑:
- 根据块号
b,使用BBLOCK()宏计算出它在哪个位图块中。 bread()读取该位图块。- 检查块
b对应的位是否为1(如果是0则说明发生了错误,panic)。 - 将该位置
0(标记为空闲)。 log_write()将修改后的位图块写入日志。brelse()释放缓冲区。
- 根据块号
5. ialloc(uint dev, short type)
- 作用: 分配一个 inode (Inode Allocate)。
- 参数:
uint dev: 设备号。short type: 要创建的 inode 的类型(如T_FILE,T_DIR)。
- 核心逻辑:
- 与
balloc类似,遍历所有的 inode 位图块,寻找一个空闲的 inode 位。 - 找到后,获取其inode 编号 (inum)。
- 调用
iget(dev, inum)从 inode 缓存中获取一个内存中的inode结构体(此时是新分配的,内容为空)。 - 初始化这个
inode的元数据:设置type,nlink=1,size=0等。 - 调用
iupdate(ip)将这个新的 inode 信息写回磁盘。 - 返回这个内存中的
inode指针。
- 与
- 引用:
I_BUSY(inode 状态),iget(),iupdate()(来自inode.c)。
6. iupdate(struct inode *ip)
- 作用: 将内存中 inode 的改动写回磁盘 (Inode Update)。
- 参数:
struct inode *ip: 指向内存中被修改过的inode。
- 核心逻辑:
- 根据
ip->inum计算出这个 inode 在磁盘上属于哪个块。 bread()读取该块。- 在块内找到对应的
dinode结构体的位置。 - 将内存
inode(ip) 的内容拷贝到磁盘dinode结构体中。 log_write()将修改后的块写入日志。brelse()释放缓冲区。
- 根据
7. itrunc(struct inode *ip)
- 作用: 截断一个文件,即释放其所有数据块 (Inode Truncate)。
- 参数:
struct inode *ip: 要被截断的文件的 inode。
- 核心逻辑:
- 遍历 inode 的
addrs[]数组中的所有直接数据块指针,对每一个有效的指针调用bfree()。 - 如果存在间接块,
bread()读取它,然后遍历其中的所有指针,对每一个都调用bfree()。最后再bfree()这个间接块本身。 - 将 inode 的
size设置为0。 - 调用
iupdate()将 inode 的变动(size 和 addrs 都被清空)写回磁盘。
- 遍历 inode 的
8. stati(struct inode *ip, struct stat *st)
- 作用: 获取 inode 的状态信息 (Status of Inode)。
- 参数:
struct inode *ip: 源 inode。struct stat *st: 目标结构体,用于返回给用户空间。
- 核心逻辑: 这是一个简单的结构体拷贝过程,将
ip中的dev,inum,type,nlink,size等信息,拷贝到st结构体对应的字段中。
9. readi(struct inode *ip, int user_dst, uint64 dst, uint off, uint n)
- 作用: 从 inode 读取数据 (Read from Inode)。
- 参数:
struct inode *ip: 从哪个 inode 读取。int user_dst: 标志位,1表示dst是用户虚拟地址,0表示是内核地址。uint64 dst: 目标地址。uint off: 从文件的哪个偏移量开始读。uint n: 要读取的字节数。
- 核心逻辑:
- 在一个循环中,根据当前的偏移量
off计算出它属于文件的第几个逻辑块。 - 调用
bmap(ip, ...)将逻辑块号翻译成物理磁盘块号。 - 如果
bmap成功,bread()读取该数据块。 - 计算出在块内的起始读取位置和本次能读取的字节数。
- 调用
either_copyout()(如果user_dst为真)或memmove()(如果为假)将数据从缓冲区拷贝到目标地址dst。 - 更新
off和n,继续循环直到读取完毕。
- 在一个循环中,根据当前的偏移量
10. writei(struct inode *ip, int user_src, uint64 src, uint off, uint n)
- 作用: 向 inode 写入数据 (Write to Inode)。
- 参数:
struct inode *ip: 写入哪个 inode。int user_src: 标志位,1表示src是用户虚拟地址。uint64 src: 源数据地址。uint off: 写入文件的哪个偏移量。uint n: 要写入的字节数。
- 核心逻辑:
- 与
readi类似,在一个循环中根据off计算逻辑块号。 - 调用
bmap(ip, ...)翻译块号。关键区别:如果bmap发现该逻辑块尚未分配,它会内部调用balloc()分配一个新块并更新inode。 bread()读取该数据块。- 调用
either_copyin()或memmove()将源数据src拷贝到缓冲区中。 log_write()将修改后的数据块写入日志。- 更新
off和n,继续循环。 - 循环结束后,如果文件大小
ip->size增加了,调用iupdate()更新 inode。
- 与
11. bmap(struct inode *ip, uint bn)
- 作用: 将
inode内的逻辑块号映射到物理块号 (Block Map)。 - 参数:
struct inode *ip: 在哪个inode中查找。uint bn: 逻辑块号 (Logical Block Number)。
- 核心逻辑:
- 如果
bn在直接块的范围内,直接从ip->addrs[bn]读取物理块号。如果该块尚未分配,则调用balloc()分配一个,更新ip->addrs[bn],并标记inode为脏。 - 如果
bn在间接块的范围内,先读取间接块,然后在间接块中查找对应的物理块号。如果间接块或最终的数据块未分配,同样调用balloc()进行分配。 - 返回最终的物理块号。
- 如果
12. skipelem(char *path, char *name) (位于 kernel/path.c)
- 作用: 这是一个路径解析的辅助工具。它的功能是从一个完整的路径字符串中,跳过并提取出第一个路径元素(文件名或目录名)。
- 参数:
char *path: 指向当前正在解析的完整路径字符串的指针。例如"/usr/bin/cat"。char *name: 一个输出缓冲区,函数会将提取出的路径元素(不含/)拷贝到这里。
- 核心逻辑:
- 跳过前导斜杠: 首先,它会跳过
path字符串开头的所有/字符。 - 提取元素: 接着,它开始逐字符地将
path的内容拷贝到name缓冲区,直到遇到下一个/或者字符串的结尾 (\0) 为止。 - 返回剩余路径: 函数的返回值是一个指针,指向
path字符串中紧跟在被提取元素之后的位置。
- 跳过前导斜杠: 首先,它会跳过
- 示例:
- 如果调用
skipelem("/usr/bin/cat", name):name缓冲区会被填充为"usr"。- 函数会返回一个指向
"bin/cat"的指针。
- 如果再次用返回的指针调用
skipelem("bin/cat", name):name会被填充为"bin"。- 函数会返回一个指向
"cat"的指针。
- 如果调用
- 引用: 这个函数是路径解析函数
namex()的核心组成部分,本身不直接引用其他文件系统函数。
13. dirlookup(struct inode *dp, char *name, uint *poff) (位于 kernel/dir.c)
- 作用: 在一个指定的目录中,查找一个具有特定名称的文件或子目录,并返回其对应的内存 inode。
- 参数:
struct inode *dp: 指向要被搜索的目录 (directory) 的 inode 指针。d代表 directory。char *name: 要查找的文件或目录的名称,例如"cat"。uint *poff: 一个可选的输出参数。如果非空,函数在找到目录项时,会把该目录项在目录文件中的字节偏移量存入poff指向的变量。
- 核心逻辑:
- 权限检查: 首先,函数会检查
dp->type是否确实为T_DIR,确保我们正在搜索的是一个目录而不是一个普通文件。 - 遍历目录项: 目录的内容是一个
struct dirent(目录项) 的列表。函数在一个循环中,使用readi()逐块读取目录的数据。 - 比较名称: 在每个块中,它会遍历所有的
dirent结构。对于每一个有效的目录项 (de.inum != 0),它会使用strncmp()将其名称de.name与要查找的name进行比较。 - 找到匹配: 如果找到了一个名称匹配的目录项:
- 如果
poff非空,就记录下当前的偏移量。 - 使用
iget(dp->dev, de.inum)根据找到的 inode 编号,获取其在内存中的inode结构。 - 返回这个
inode指针。
- 如果
- 未找到: 如果遍历完所有目录项都没有找到匹配的名称,函数返回
0(NULL)。
- 权限检查: 首先,函数会检查
- 引用:
readi()(来自fs.c),iget()(来自inode.c)。
14. dirlink(struct inode *dp, char *name, uint inum) (位于 kernel/dir.c)
- 作用: 在一个指定的目录中,创建一个新的链接。这个操作会在目录的数据中增加一个新的目录项,将一个名称和一个 inode 编号关联起来。这是
create,mkdir,link等系统调用的基础。 - 参数:
struct inode *dp: 指向要添加新链接的目录的 inode 指针。char *name: 要创建的新链接的名称。uint inum: 该名称应该链接到的 inode 编号。
- 核心逻辑:
- 检查名称是否已存在: 函数首先会调用
dirlookup()来检查name是否已经存在于目录dp中。如果存在,说明发生了错误(比如重复创建文件),函数会返回失败。 - 寻找空闲槽位: 接着,函数会再次遍历目录
dp的所有数据块,这次是为了寻找一个空的目录项(即de.inum == 0的条目)。 - 创建新链接: 一旦找到一个空槽位:
- 它会将
name拷贝到de.name中。 - 它会将
inum赋值给de.inum。 - 它会调用
writei()将这个被修改过的、包含了新目录项的数据块写回磁盘。 - 成功后,返回
0。
- 它会将
- 目录已满: 如果遍历完整个目录都没有找到空槽位,说明目录已满,无法创建新文件,函数返回失败。
- 检查名称是否已存在: 函数首先会调用
- 引用:
dirlookup(),writei()(来自fs.c)。
15. namex(char *path, int nameiparent, char *name) (内部核心函数)
- 作用: 这是一个通用的路径解析引擎。它的功能是根据给定的
path,从一个起始点(根目录或当前工作目录)开始,逐级地在目录中查找,直到解析完整个路径。根据nameiparent标志,它最终会返回路径末端目标或其父目录的 inode。 - 参数:
char *path: 要解析的完整路径字符串,例如"/a/b/c.txt"。int nameiparent: 一个标志位。- 如果为
0,函数会解析到路径的最后一个元素并返回其 inode(例如,返回c.txt的 inode)。 - 如果为
1,函数会解析到路径的倒数第二个元素(即父目录),并返回父目录的 inode(例如,返回b的 inode)。
- 如果为
char *name: 一个输出缓冲区。- 如果
nameiparent为1,函数会将路径的最后一个元素("c.txt")拷贝到name中。
- 如果
- 核心逻辑: 这是一个迭代查找的过程。
- 确定起点: 函数首先判断路径是绝对路径(以
/开头)还是相对路径。- 如果是绝对路径,它从根目录 inode (
iget(ROOTDEV, ROOTINO)) 开始查找。 - 如果是相对路径,它从当前进程的工作目录 (
myproc()->cwd) 开始查找。
- 如果是绝对路径,它从根目录 inode (
- 进入循环: 函数进入一个
while循环,只要路径还没解析完,循环就继续。 - 提取路径元素: 在循环的每一次迭代中,它调用
skipelem(path, name)来从当前路径中提取出下一个要查找的目录或文件名(例如,第一次是"a",第二次是"b")。 - 加锁与权限检查:
- 它调用
ilock(ip)锁定当前目录的 inode,以防止并发访问冲突。 - 它检查当前 inode 是否确实是一个目录 (
ip->type==T_DIR),如果不是,说明路径非法(比如/a/b中a是一个文件),解析失败。
- 它调用
- 查找下一级: 它调用
dirlookup(ip, name, 0)在当前目录中查找刚刚提取出的name,以获取下一级的 inode。 - 释放旧锁,更新状态: 查找到下一级 inode (
next) 后,它调用iunlockput(ip)来解锁并减少对当前目录 inode 的引用计数。然后,它将ip更新为next,准备下一次循环。 - 处理
nameiparent: 如果nameiparent标志为1,并且路径已经解析到最后一个元素,循环会提前终止,此时ip指向的就是父目录。 - 循环结束: 当
skipelem无法再提取出新的路径元素时,循环结束。
- 确定起点: 函数首先判断路径是绝对路径(以
- 返回值: 成功时返回一个已锁定的目标 inode(目标本身或其父目录)。失败时返回
0。 - 引用:
skipelem()(来自path.c),iget(),ilock(),iunlockput()(来自inode.c),dirlookup()(来自dir.c)。
16. namei(char *path)
-
作用: "Name to Inode" 的缩写。这是一个简化的接口,它的唯一目的是根据一个完整的路径,返回路径最后一个元素所对应的 inode。
-
参数:
char *path: 要查找的完整路径,例如"/a/b/c.txt"。
-
核心逻辑: 它是一个对
namex的简单封装。它直接调用namex,并将nameiparent参数设置为0,同时忽略name输出参数。struct inode* namei(char *path) { char name[DIRSIZ]; return namex(path, 0, name); // nameiparent=0, 表示要找到路径的末端 } -
使用场景: 任何需要根据完整路径获取文件或目录自身的系统调用,比如
open()(当不带O_CREATE标志时),stat(),chdir()。
17. nameiparent(char *path, char *name)
-
作用: "Name to Inode of Parent" 的缩写。这是另一个简化的接口,它的目的是找到一个路径的父目录,并把路径的最后一个文件名提取出来。
-
参数:
char *path: 要查找的完整路径,例如"/a/b/c.txt"。char *name: 一个输出缓冲区,用于存放路径的最后一个元素(例如"c.txt")。
-
核心逻辑: 它也是对
namex的封装,但它将nameiparent参数设置为1,明确表示它需要的是父目录的 inode。

浙公网安备 33010602011771号