Large files
这个实验的核心目标是修改 xv6 的文件系统,引入 二级间接块 (Doubly-Indirect Block),从而打破文件最大只能有 268 个块(约 268KB)的限制,使其支持约 65803 个块(约 65MB)。
原始结构:12 个直接块 + 1 个一级间接块 = 13 个槽位。总计 12+256=268 个块
新结构:11 个直接块 + 1 个一级间接块 + 1 个二级间接块 = 13 个槽位。总共256*256+256+11 =65803 个块
我们需要修改三个主要部分:头文件定义、映射函数 bmap 和截断函数 itrunc。
修改fs.h
1 2 3 4 5 6 7 8
| #define NDIRECT 11 // 从 12 改为 11 #define NINDIRECT (BSIZE / sizeof(uint)) // 修改 MAXFILE 的计算公式,增加二级间接块的支持 #define MAXFILE (NDIRECT + NINDIRECT + NINDIRECT * NINDIRECT) struct dinode { // ... 其他字段 uint addrs[NDIRECT+2]; // 这里的 +1 原本是指一级间接块 };
|
修改file.h
1 2 3 4
| struct inode { // ... 其他字段 uint addrs[NDIRECT+2]; // 保持与 fs.h 中一致,总共 13 个 };
|
在fs.c里实现bmap函数,该函数的作用是:给定一个文件的逻辑块号 bn,找到它在磁盘上对应的物理块号。如果该块不存在,则分配它。
逻辑如下:
- 直接块 (0-10): 处理方式不变。
- 一级间接块 (11-266): 处理方式不变,只是
bn 的偏移量变了。
- 二级间接块 (267+): 新增逻辑。
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 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| static uint bmap(struct inode *ip, uint bn) { uint addr, *a; struct buf *bp;
// 1. 处理直接块 (0 ~ 10) if(bn < NDIRECT){ if((addr = ip->addrs[bn]) == 0) ip->addrs[bn] = addr = balloc(ip->dev); return addr; } bn -= NDIRECT; // 减去直接块的数量
// 2. 处理一级间接块 (11) if(bn < NINDIRECT){ // 如果一级间接块本身不存在,先分配 if((addr = ip->addrs[NDIRECT]) == 0) ip->addrs[NDIRECT] = addr = balloc(ip->dev); // 读取一级间接块的内容 bp = bread(ip->dev, addr); a = (uint*)bp->data; // 查找目标块,如果不存在则分配 if((addr = a[bn]) == 0){ a[bn] = addr = balloc(ip->dev); log_write(bp); // 记录日志,因为修改了块内容 } brelse(bp); // 释放 buffer return addr; } bn -= NINDIRECT; // 减去一级间接块的数量
// 3. 处理二级间接块 (12) if(bn < NINDIRECT * NINDIRECT){ // index_1: 在二级块表中的索引 (指向哪个一级块) // index_2: 在一级块表中的索引 (指向哪个数据块) uint index_1 = bn / NINDIRECT; uint index_2 = bn % NINDIRECT;
// A. 检查/分配二级间接块本身 (ip->addrs[NDIRECT+1]) if((addr = ip->addrs[NDIRECT+1]) == 0) ip->addrs[NDIRECT+1] = addr = balloc(ip->dev); // 读取二级间接块 bp = bread(ip->dev, addr); a = (uint*)bp->data;
// B. 检查/分配中间的一级间接块 if((addr = a[index_1]) == 0){ a[index_1] = addr = balloc(ip->dev); log_write(bp); } brelse(bp); // 用完二级块了,释放
// 读取中间的一级间接块 bp = bread(ip->dev, addr); a = (uint*)bp->data;
// C. 检查/分配最终的数据块 if((addr = a[index_2]) == 0){ a[index_2] = addr = balloc(ip->dev); log_write(bp); } brelse(bp); // 用完一级块了,释放 return addr; }
panic("bmap: out of range"); }
|
在fs.c里实现itrunc函数
itrunc 用于释放文件占用的所有块(例如删除文件时)。如果不修改这里,虽然能写大文件,但删除文件时会导致磁盘空间泄露(block leak)。
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 49 50 51 52 53 54 55 56 57 58
| void itrunc(struct inode *ip) { int i, j; struct buf *bp, *bp2; // 需要两个 buffer 指针 uint *a, *a2;
// 1. 释放直接块 (保持不变) for(i = 0; i < NDIRECT; i++){ if(ip->addrs[i]){ bfree(ip->dev, ip->addrs[i]); ip->addrs[i] = 0; } }
// 2. 释放一级间接块 (保持不变) 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; }
// 3. 释放二级间接块 —— 新增部分 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]){ // 读取这个一级块 bp2 = bread(ip->dev, a[i]); a2 = (uint*)bp2->data; // 遍历一级块中的每一个条目 (它们指向数据块) for(j = 0; j < NINDIRECT; j++){ if(a2[j]) bfree(ip->dev, a2[j]); } brelse(bp2); // 释放一级块 buffer bfree(ip->dev, a[i]); // 释放一级块本身 } } brelse(bp); // 释放二级块 buffer bfree(ip->dev, ip->addrs[NDIRECT+1]); // 释放二级块本身 ip->addrs[NDIRECT+1] = 0; }
ip->size = 0; iupdate(ip); }
|
测试通过
Symbolic links
现有的硬链接
1 2 3 4 5 6 7
| +----------------+ +-----------------------------+ | 文件名: a.txt |--------->| Inode #10 | +----------------+ | (类型: T_FILE) | | | +----------------+ | [ 数据块内容 ] | | 文件名: b.txt |--------->| "Hello World..." | +----------------+ +-----------------------------+
|
符号链接
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| +-----------------------------+ | 目标文件 (Inode #10) | +----------------+ | (类型: T_FILE) | | 文件名: a.txt |--------->| | +----------------+ | [ 数据块 ] | | "Hello World..." | +-----------------------------+ ^ | (系统 open 时会自动去读 这个路径并跳转) | +----------------+ +---------|-------------------+ | 文件名: b.txt |--------->| 软链文件 (Inode #20) | +----------------+ 这是一个全新的 Inode | (类型: T_SYMLINK) | | | | [ 数据块 ] | | "a.txt" | 存的是路径字符串! +-----------------------------+
|
思路如下:定义常量 -> 实现创建软链 (sys_symlink)–> 实现跟随软链 (sys_open)。
代码如下:
首先在makefile里添加$U/_symlinktest\ ,然后修改 user/usys.pl、user/user.h、kernel/syscall.h 和 kernel/syscall.c添加symlink,
添加文件类型 (kernel/stat.h):
1 2 3 4
| #define T_DIR 1 // Directory #define T_FILE 2 // File #define T_DEVICE 3 // Device #define T_SYMLINK 4 // 【新增】符号链接
|
添加 open 标志 (kernel/fcntl.h)
1 2 3 4 5 6
| #define O_RDONLY 0x000 #define O_WRONLY 0x001 #define O_RDWR 0x002 #define O_CREATE 0x200 #define O_TRUNC 0x400 #define O_NOFOLLOW 0x800 // 【新增】不跟随软链接
|
下面实现sys_symlink(kernel/sysfile.c)
创建一个类型为 T_SYMLINK 的新 inode,并把 target 路径写进这个 inode 的数据块里。
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
| uint64 sys_symlink(void) { char target[MAXPATH], path[MAXPATH]; struct inode *ip;
// 1. 获取参数 if(argstr(0, target, MAXPATH) < 0 || argstr(1, path, MAXPATH) < 0) return -1;
begin_op();
// 2. 创建 inode (类似 mkdir, 但类型是 T_SYMLINK) // create 会返回已锁定的 inode if((ip = create(path, T_SYMLINK, 0, 0)) == 0){ end_op(); return -1; }
// 3. 向 inode 的数据块中写入 target 路径 // writei(ip, src, offset, n) if(writei(ip, 0, (uint64)target, 0, strlen(target)) != strlen(target)) { // 写入失败处理 iunlockput(ip);//在调用 create 时,系统给了一把锁(ilock) end_op(); return -1; }
// 4. 收尾 iunlockput(ip); // 解锁并减少引用计数 end_op(); return 0; }
|
下面修改sys_open
当用户 open 一个路径时,如果发现它是软链接,我们需要“递归”地去查找它指向的目标。在 namei 找到 inode 之后,先检查一下是不是 T_SYMLINK,如果是,且用户没有设置 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 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
| uint64 sys_open(void) { char path[MAXPATH]; int fd, omode; struct file *f; struct inode *ip; int n;
// 获取 open 的两个参数:路径 path 和 标志位 omode argint(1, &omode); if((n = argstr(0, path, MAXPATH)) < 0) return -1;
begin_op();
if(omode & O_CREATE){ // 如果是创建文件模式,逻辑不变 ip = create(path, T_FILE, 0, 0); if(ip == 0){ end_op(); return -1; } } else { // 如果是打开现有文件,需要处理软链接 int depth = 0; // 递归深度计数器
while(1) { // 1. 根据路径查找 inode if((ip = namei(path)) == 0){ end_op(); return -1; } ilock(ip); // 锁定 inode 以便检查类型和读取内容
// 2. 检查是否需要处理软链接 // 如果不是软链接,或者用户指定了 O_NOFOLLOW (不跟随),则停止查找 if(ip->type != T_SYMLINK || (omode & O_NOFOLLOW)){ break; }
// 3. 检查递归深度,防止死循环 (A -> B -> A) if(depth >= 10){ iunlockput(ip); // 释放当前锁定的 inode end_op(); return -1; }
// 4. 读取软链接中存储的目标路径 // 直接读取到 path 缓冲区中,准备下一轮 namei 使用 int len = readi(ip, 0, (uint64)path, 0, MAXPATH); if(len < 0){ iunlockput(ip); end_op(); return -1; } path[len] = 0; // 切断字符串,丢弃后面的垃圾数据
// 5. 释放当前软链接 inode,准备进入下一轮循环 iunlockput(ip); depth++; }
// 检查是否试图写入目录(原有逻辑) if(ip->type == T_DIR && omode != O_RDONLY){ iunlockput(ip); end_op(); return -1; } }
// ... 下面是原有的分配文件描述符逻辑 ... // if((f = filealloc()) == 0) ... }
|
测试通过
1 2 3 4 5
| $ symlinktest Start: test symlinks test symlinks: ok Start: test concurrent symlinks test concurrent symlinks: ok
|