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,找到它在磁盘上对应的物理块号。如果该块不存在,则分配它。

逻辑如下:

  1. 直接块 (0-10): 处理方式不变。
  2. 一级间接块 (11-266): 处理方式不变,只是 bn 的偏移量变了。
  3. 二级间接块 (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);
}

测试通过

现有的硬链接

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.pluser/user.hkernel/syscall.hkernel/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