从你的描述来看,这段话是给你的一些 预备知识 和 mkfs工具的使用 提示,帮助你了解如何构建和管理文件系统,特别是关于 xv6 文件系统的一些基本操作。
我会通过比喻和通俗化的方式逐步解释:
预备知识:xv6 文件系统的基本结构
-
文件系统的构建:
文件系统就像一个巨大的 书库,里面存放着你所有的文件(数据块)。在开始实现文件系统的功能之前,你需要了解这个书库是如何构建的。就像你需要了解书架的设计、每一层存放了多少书、怎么存放书籍,文件系统也是在操作这些基本的“块”(blocks)。 -
mkfs 程序:
mkfs
程序就像是 构建书架 的工具,它根据指定的参数建立一个文件系统(类似创建一个空的书库)。在mkfs
执行时,它会给你一些信息,比如文件系统有多少个“书架”(数据块)可以使用。 -
FSSIZE 参数:
FSSIZE
就是设定书库的 大小,它告诉文件系统最多可以有多少个块(书架)。在你给出的例子中,FSSIZE
被设置为 200,000 个块,这意味着整个文件系统最多可以拥有 200,000 个块来存储数据。 -
输出信息: 当你运行
mkfs
时,它会输出文件系统的基本信息,告诉你文件系统的构建情况。例如:nmeta 70 (boot, super, log blocks 30 inode blocks 13, bitmap blocks 25) blocks 199930 total 200000
这里的意思是,文件系统的总共有 200,000 个块,其中:
- 70 个块用于元数据(这些是描述文件系统本身的数据块)
- 199,930 个块用于实际存储数据(数据块)
你可以把这看作是文件系统构建后,书架和书箱的分配情况:70 个书架用于存放“书库”的管理信息,其余的都用来存放实际的书籍(数据)。
-
重建文件系统:
如果在开发过程中你需要重新构建文件系统(比如修改了文件系统的设计),你可以运行make clean
,这会删除之前的文件系统,强制重新构建一个新的fs.img
。
通俗化解释:
- 你在做这个作业时,可以把 文件系统 想象成一个书库,每本书就是一个数据块。
- mkfs 就像是一个工具,帮助你搭建这个书库,它决定书库一共会有多少个书架(块)。
- 在文件系统创建过程中,FSSIZE 就是控制书库的总容量(有多少个书架)。比如,200,000 块表示书库有 200,000 个“书架”,你可以用它们来存放数据。
- 元数据 是文件系统本身的管理信息,类似书库的管理者放置在特殊书架上的信息,比如如何分类、哪些书架还空着等。
- 数据块 就是实际存放文件内容的块,也就是你可以用来存放实际“书”的地方。
通过这样的理解,你会更清楚文件系统构建的基本框架,以及如何使用 mkfs
来创建一个文件系统。
格式指南
项目的结构如下图所示。你也可以使用 ls
命令检查你的目录结构是否正确。目录结构不匹配将导致扣分。
对于本次作业,你不需要为附加题(Bonus Part)单独创建文件夹。source
文件夹应包含以下四个文件:
fs.c
file.h
fs.h
sysfile.c
如果要提交第 4 次作业的附加题实现,请单独提交以下文件:
fs_ec.h
fs_ec.c
示例目录结构如下:
main@ubuntu:~/Desktop/Assignment_4_120010001$ ls
Report.pdf source/
(一个文件夹和一个 PDF 文件)
在 source
文件夹下:
main@ubuntu:~/Desktop/Assignment_4_120010001/source$ ls
file.h fs.c fs.h sysfile.c
(两个 .c 文件和两个 .h 文件)
请将包含所有所需文件的文件夹压缩成一个 zip 文件,文件名格式应为你的学号,具体如下:
示例文件名:Assignment_4_120010001.zip
报告(Report)应以 PDF 格式提交,并与源代码文件一起包含在压缩包中。格式不符将导致扣分。
压缩代码的示例步骤:
main@ubuntu:~/Desktop$ zip -q -r Assignment_4_120010001.zip Assignment_4_120010001
main@ubuntu:~/Desktop$ ls
Assignment_4_120010001
Assignment_4_120010001.zip
以下是对这段说明的翻译和整理:
指导说明
-
限制范围:
你的实现范围仅限于以下四个文件:fs.c
file.h
fs.h
sysfile.c
代码需要从标记为
"TODO:"
的注释部分开始实现。 -
测试程序入口:
测试程序的入口文件是bigfile.c
和symlinktest.c
,它们位于xv6-labs-2022/user
目录下。
(这是你开始了解测试如何运行的地方。) -
带有 (*) 的部分:
- 这些部分是引导性内容,介绍了工具和函数,帮助你理解系统的功能以及这些组件如何协同工作。
- 这些部分的代码你需要仔细阅读和理解,但不要修改,除非是标记了
"TODO:"
的部分。
指导建议:
- 先理解引导部分的函数是如何工作的,以及如何使用这些函数。
- 在开始实现作业之前,确保你已经对相关内容有了基本的理解。
- 我们认为这些引导部分的内容已经足够完成本次作业。
-
可选内容(兴趣拓展):
- 如果你对 xv6 系统感兴趣并希望深入学习,欢迎阅读《xv6-book》以获取更多细节:
xv6-book PDF
- 如果你对 xv6 系统感兴趣并希望深入学习,欢迎阅读《xv6-book》以获取更多细节:
-
没有 (*) 的部分:
- 这些部分是需要你实现的
"TODO:"
部分,包含逻辑说明。 - 在这些部分,你需要根据引导部分介绍的逻辑和提供的 API 来实现函数。
- 这些部分是需要你实现的
-
实现提示:
- 示例代码不会提供。
- 你需要根据引导部分提供的逻辑和 API 自行推导和实现代码功能。
预先准备与设计(吐槽:这部分作业比3050的指导清晰多了)
任务 1:支持大文件
1. 当前 xv6 文件系统的文件大小限制
- 现状:
- 目前 xv6 文件的最大大小是 268 个块,即 268 × BSIZE 字节(
BSIZE
在 xv6 中是 1024 字节,即一个块的大小)。 - 这个限制来源于 xv6 的 inode 结构:
- 每个 inode 包含 12 个直接块指针 和 1 个单重间接块指针。
- 单重间接块指针 指向一个块,该块中可以存储 256 个数据块的地址。
- 计算文件最大块数:
12 + 256 = 268
。
- 目前 xv6 文件的最大大小是 268 个块,即 268 × BSIZE 字节(
2. 当前测试程序问题:bigfile
命令
- 测试失败原因:
bigfile
命令尝试创建一个 尽可能大的文件 并报告其大小。- 提供的模板程序无法写满 256 块,因为测试期望文件能够支持 65803 块。
- 原因是:未修改的 xv6 系统将文件限制在 268 块,而不是测试期望的 65803 块。
3. 目标:实现双重间接块
-
修改需求:
- 修改 xv6 文件系统,使其支持 双重间接块。
- 每个 inode 添加一个 双重间接块指针。具体逻辑:
- 双重间接块指针 指向一个块(双重间接块),该块包含 256 个单重间接块的地址。
- 每个单重间接块包含 256 个数据块的地址。
-
修改后的最大块数:
- 文件最多可以有:
256 × 256 + 256 + 11 = 65,803 块。- 解释:
- 256 × 256:双重间接块支持的块数(256 个单重间接块,每个单重间接块支持 256 个数据块)。
- 256:单重间接块本身支持的块数。
- 11:剩下的直接块(从原来的 12 减去一个用于双重间接块指针)。
- 解释:
- 文件最多可以有:
4. 可选任务:实现三重间接块(额外加分)
-
三重间接块的工作方式:
- 每个 三重间接块 包含 256 个双重间接块的地址。
- 每个双重间接块又指向 256 个单重间接块,每个单重间接块再指向 256 个数据块。
- 结果是一个文件的块数可以大幅提升,达到: 256 × 256 × 256 + 256 × 256 + 256 + 11。
-
加分实现建议:
- 三重间接块的实现逻辑与双重间接块类似,主要是增加一层指针解析。
- 对文件系统性能要求更高,可以在完成双重间接块后尝试实现。
总结
- 目标任务:
- 修改文件系统的 inode 结构,增加对 双重间接块 的支持,从而提升文件系统的最大文件大小到 65803 块。
- 额外加分任务:
- 在双重间接块的基础上实现 三重间接块,进一步提升文件系统的支持能力。
以下是这段说明的翻译和解析,帮助你理解所需的定义和结构的作用:
定义与说明
阅读参考:
- 如需详细信息,可参阅《xv6-book》第 8.10 节。
- 根据以上提示和定义,代码中提供了修改后的结构,请仔细阅读代码中的注释。
修改后的定义(kernel/fs.h
中的宏定义)
- 宏定义解释:
-
#define NDIRECT 11
- 直接块的数量从 12 减少到 11。
- 这是因为第 12 个直接块的位置被牺牲,改为存储 双重间接块指针。
-
#define NINDIRECT (BSIZE / sizeof(uint))
- 表示单重间接块中可以存储的块指针数量。
- 计算公式:
BSIZE / sizeof(uint)
,即 1024 / 4 = 256。
-
#define DNINDIRECT (NINDIRECT * NINDIRECT)
- 表示双重间接块可以存储的数据块数量。
- 计算公式:
256 × 256 = 65536
。
-
#define MAXFILE (NDIRECT + NINDIRECT + DNINDIRECT)
- 表示一个文件的最大块数(直接块 + 单重间接块 + 双重间接块)。
- 计算公式:
11 + 256 + 65536 = 65803
。
-
修改后的结构(kernel/fs.h
中的 dinode
)
struct dinode
的定义:- 作用:表示磁盘上的 inode 结构,包含文件的元信息和块指针。
- 关键字段解释:
short type
:文件类型。short major
:主设备号(仅适用于设备文件)。short minor
:次设备号(仅适用于设备文件)。short nlink
:文件链接数(硬链接数量)。uint size
:文件的大小(以字节为单位)。uint addrs[NDIRECT + 2]
:addrs
是块地址数组:- 前 11 个地址是直接块指针。
- 第 12 个地址是单重间接块指针。
- 第 13 个地址是双重间接块指针(新增)。
修改后的结构(kernel/file.h
中的 inode
)
struct inode
的定义:- 作用:表示内存中的 inode 结构,是
dinode
的内存副本。 - 关键字段解释:
uint dev
:设备号。uint inum
:inode 编号。int ref
:引用计数。struct sleeplock lock
:锁,用于保护结构中其他字段的并发访问。int valid
:是否已从磁盘加载。uint addrs[NDIRECT + 2]
:- 同
dinode
中的addrs
,用于存储直接块、单重间接块和双重间接块的指针。
- 同
- 作用:表示内存中的 inode 结构,是
修改注意事项
-
修改说明:
- 结构不可更改:上述的结构(
dinode
和inode
)已提供,请勿修改。 - 仅在
TODO
标记部分实现逻辑。
- 结构不可更改:上述的结构(
-
关键变化总结:
- 直接块减少到 11:因为要为双重间接块腾出位置。
- 新增双重间接块支持:在
addrs
的最后一个位置存储双重间接块的指针。
//TODO
这一块主要是定义了文件系统中 inode 结构 和一些与块分配相关的宏,用于支持你在作业中实现对大文件的管理。下面是逐步解释,让你更容易理解它们的作用:
什么是 inode?
inode
(索引节点)是文件系统中用来描述文件的一个核心数据结构。它不是存储文件内容本身,而是存储文件的元信息,比如:
- 文件的类型(普通文件、目录、设备文件等)
- 文件的大小
- 文件的块地址(指向实际存储数据的位置)
在这个作业中,你要修改 xv6 的 inode
结构以支持 双重间接块,从而使文件系统能管理更大的文件。
宏定义的作用
-
NDIRECT
- 表示直接块的数量。
- 直接块是
inode
中直接指向数据块的指针。 - 默认情况下是 12,但为了支持双重间接块,作业中改成了 11,把最后一个位置用于双重间接块指针。
-
NINDIRECT
- 表示单重间接块中可以存储的块地址数量。
- 单重间接块是
inode
指向的一个中间块,这个块不存放数据,而是存储其他块的地址。 - 计算公式是:
NINDIRECT = BSIZE / sizeof(uint)
其中BSIZE = 1024
,sizeof(uint) = 4
,所以NINDIRECT = 1024 / 4 = 256
。
-
DNINDIRECT
- 表示双重间接块中可以存储的块地址数量。
- 双重间接块是一个两级指针:
- 第一层存储指向单重间接块的地址(256 个)。
- 每个单重间接块又存储 256 个数据块的地址。
- 计算公式是:
DNINDIRECT = NINDIRECT * NINDIRECT = 256 × 256 = 65536
。
-
MAXFILE
- 表示一个文件能使用的最大块数,包括:
- 11 个直接块。
- 256 个单重间接块。
- 65536 个双重间接块。
- 计算公式是:
MAXFILE = NDIRECT + NINDIRECT + DNINDIRECT = 11 + 256 + 65536 = 65803
。
- 表示一个文件能使用的最大块数,包括:
dinode
和 inode
结构
1. dinode
(磁盘上的 inode)
- 作用:这是保存在磁盘上的
inode
结构,记录文件的元信息和块指针。 - 字段解释:
uint addrs[NDIRECT + 2]
:- 存储文件块的指针数组:
- 前 11 个是直接块指针。
- 第 12 个是单重间接块指针。
- 第 13 个是双重间接块指针。
- 存储文件块的指针数组:
2. inode
(内存中的 inode)
- 作用:这是加载到内存中的
inode
结构,用于操作文件时临时存储。 - 字段解释:
uint addrs[NDIRECT + 2]
:- 同样存储直接块、单重间接块和双重间接块指针。
总结:这部分的重点
-
你需要了解的变化:
NDIRECT
从 12 改为 11,用于给双重间接块腾出空间。addrs[NDIRECT + 2]
:- 现在包含了双重间接块指针(第 13 个位置)。
-
这些定义和结构不需要修改:
- 它们是预先提供的基础代码,已经为你实现了双重间接块所需的结构调整。
-
你的任务:
- 在作业中,围绕这些结构实现双重间接块的逻辑,包括数据块分配、释放、读取和写入。
块管理相关 API 的详细说明
Block Management(块管理)
文件系统中的块管理是核心部分,用来分配、释放、读取和写入磁盘块。这些 API 主要分为以下几类:
- 定义在
fs.c
中的块操作函数 - 定义在
bio.c
中的缓冲区操作函数 - 定义在
log.c
中的日志记录函数
1. 块管理函数(定义在 fs.c
)
1.1 bzero(int dev, int bno)
- 作用:将指定的磁盘块清零(所有内容置为 0)。
- 参数:
dev
:设备号,表示在哪个设备上执行操作。bno
:块号,表示要清零的块。
- 比喻:这就像你借了一本旧书(磁盘块),需要先擦掉上面的字迹(清零)再开始使用。
1.2 uint balloc(uint dev)
- 作用:分配一个新的磁盘块,并将其内容清零。
- 返回值:
- 成功时返回新分配块的块号。
- 如果磁盘空间不足,返回 0。
- 比喻:这就像向图书管理员申请一本空白的新书。如果图书馆满了(磁盘空间不足),就无法分配。
1.3 void bfree(int dev, uint b)
- 作用:释放一个指定的磁盘块,使其可以被其他文件使用。
- 参数:
dev
:设备号。b
:要释放的块号。
- 比喻:这就像你还书(释放块),让图书管理员把它放回书架,其他人可以借用。
2. 缓冲区管理函数(定义在 bio.c
)
2.1 struct buf* bread(uint dev, uint blockno)
- 作用:从磁盘读取指定块到缓冲区,并返回一个已锁定的缓冲区对象。
- 返回值:返回一个指向缓冲区对象的指针。
- 比喻:这就像从仓库(磁盘)把一本书拿到借阅区(缓冲区),并锁定它以防止其他人同时访问。
2.2 void brelse(struct buf *b)
- 作用:释放缓冲区对象,将其移动到最近使用列表的头部,表示可以被其他进程使用。
- 参数:
b
:指向缓冲区对象的指针。
- 比喻:这就像你看完书后,把书放回借阅区的书架上,让其他人可以继续借阅。
3. 日志管理函数(定义在 log.c
)
3.1 void log_write(struct buf *b)
- 作用:将缓冲区中的数据标记为已修改,并记录日志以便稍后写入磁盘。
- 典型用法:
- 调用
bread()
获取缓冲区。 - 修改缓冲区中的数据。
- 调用
log_write()
记录修改。 - 调用
brelse()
释放缓冲区。
- 调用
- 比喻:这就像你拿到一本书后,在书上做笔记,然后告诉管理员这些修改需要最终保存到书里。
API 使用场景
这些 API 是实现文件系统操作的基础模块,具体用法如下:
-
分配和释放磁盘块:
- 使用
balloc()
分配一个新的磁盘块,并自动清零。 - 使用
bfree()
释放一个不再需要的磁盘块。
- 使用
-
读取和操作磁盘块:
- 使用
bread()
从磁盘读取块到缓冲区。 - 修改缓冲区数据后,使用
log_write()
标记为已修改。 - 使用
brelse()
释放缓冲区。
- 使用
-
清零磁盘块:
- 使用
bzero()
将一个磁盘块的内容清零。
- 使用
总结:API 的关系与比喻
-
分配块:
balloc()
就像借一本新书,同时自动清空书页内容。bfree()
就像还书,让其他人可以使用。
-
操作块:
bread()
就像从仓库把书拿到借阅区,锁定它以防止冲突。- 修改书后,使用
log_write()
记录你的修改,并稍后保存到仓库。 brelse()
就是看完书后,放回借阅区供其他人使用。
下面是包含具体代码的内容以及每行代码的注释和比喻说明:
块管理代码
定义在 fs.c
中
// 将指定的磁盘块清零(将块内容全部置为 0)
static void bzero(int dev, int bno) {struct buf *bp = bread(dev, bno); // 从磁盘读取块 bno 到缓冲区memset(bp->data, 0, BSIZE); // 将缓冲区中的数据清零log_write(bp); // 标记缓冲区为已修改,记录日志brelse(bp); // 释放缓冲区
}
逐行说明:
bread(dev, bno)
:从设备号dev
中读取块号bno
的内容到缓冲区bp
。- 比喻:从仓库(磁盘)取出书(块)到借阅区(缓冲区)。
memset(bp->data, 0, BSIZE)
:将缓冲区的数据清零。- 比喻:把书的内容擦掉,变成一本空白的书。
log_write(bp)
:记录这次清零操作,稍后写回磁盘。- 比喻:告诉管理员这本书已经被清零了,需要保存到仓库。
brelse(bp)
:释放缓冲区。- 比喻:把书放回借阅区的书架上,供其他人使用。
// 分配一个新的磁盘块,并清零其内容
static uint balloc(uint dev) {for (int b = 0; b < FSSIZE; b++) { // 遍历磁盘所有块if (block_is_free(dev, b)) { // 如果块 b 是空闲的bzero(dev, b); // 将块 b 的内容清零mark_block_used(dev, b); // 将块 b 标记为已使用return b; // 返回新分配的块号}}return 0; // 如果没有可用的块,返回 0
}
逐行说明:
- 遍历块:通过循环遍历磁盘中的每个块。
- 比喻:管理员在整个仓库中寻找空白的书架。
block_is_free(dev, b)
:检查块是否是空闲的。- 比喻:检查这个书架是否没有放书。
bzero(dev, b)
:将空闲块的内容清零。- 比喻:如果书架是空的,先清空上面的灰尘。
mark_block_used(dev, b)
:将块标记为已使用。- 比喻:在记录本上标注这本书(块)已经借出去。
- 返回块号:如果找到空闲块,返回其编号。
// 释放一个磁盘块
static void bfree(int dev, uint b) {clear_block_usage(dev, b); // 将块 b 标记为未使用
}
逐行说明:
clear_block_usage(dev, b)
:将块号 b 标记为未使用。- 比喻:把书架上的书还给图书馆,标记这个书架可以再次使用。
定义在 bio.c
中
// 从磁盘读取指定块到缓冲区,并返回缓冲区对象
struct buf* bread(uint dev, uint blockno) {struct buf *bp = buffer_cache_find(dev, blockno); // 在缓存中查找块if (!bp) { // 如果缓存中没有bp = buffer_cache_allocate(dev, blockno); // 分配一个新的缓冲区disk_read(dev, blockno, bp->data); // 从磁盘读取块数据到缓冲区}lock_buffer(bp); // 锁定缓冲区,防止其他进程访问return bp; // 返回缓冲区
}
逐行说明:
buffer_cache_find(dev, blockno)
:先查找缓存中是否已有该块。- 比喻:管理员先在借阅区(缓存)找这本书,看是否已经有人借出。
buffer_cache_allocate(dev, blockno)
:如果缓存中没有,则分配一个新的缓冲区。- 比喻:如果书不在借阅区,就分配一个新空位来存储这本书。
disk_read(dev, blockno, bp->data)
:从磁盘读取块号为blockno
的数据到缓冲区。- 比喻:从仓库(磁盘)搬运书到借阅区。
lock_buffer(bp)
:锁定缓冲区,防止其他进程访问。- 比喻:管理员锁定书本,防止被多个人同时借走。
// 释放缓冲区
void brelse(struct buf *b) {unlock_buffer(b); // 解锁缓冲区move_to_recent_list(b); // 将缓冲区放到最近使用列表的头部
}
逐行说明:
unlock_buffer(b)
:解锁缓冲区。- 比喻:管理员解锁书本,允许其他人查看。
move_to_recent_list(b)
:将缓冲区移到最近使用列表头部。- 比喻:把书放到书架前排,方便下次快速找到。
定义在 log.c
中
// 记录缓冲区的修改,稍后写入磁盘
void log_write(struct buf *b) {pin_buffer(b); // 增加缓冲区的引用计数,防止被替换mark_buffer_dirty(b); // 标记缓冲区为已修改log_commit_buffer(b); // 将修改记录到日志中
}
逐行说明:
pin_buffer(b)
:增加引用计数,防止缓冲区被替换。- 比喻:管理员标注这本书暂时不能借出,还在修改中。
mark_buffer_dirty(b)
:标记缓冲区内容为脏数据,表示需要写回磁盘。- 比喻:书已经被修改,管理员需要保存笔记。
log_commit_buffer(b)
:将缓冲区的修改记录到日志。- 比喻:管理员记录修改内容,稍后存档。
总结
- 这些 API 构成了文件系统中 磁盘块管理和缓存机制 的基础逻辑。
- 它们的主要功能是:
- 分配、释放块(
fs.c
):申请和回收磁盘上的存储空间。 - 缓存管理(
bio.c
):从磁盘读取数据到内存,并提供缓存机制。 - 日志记录(
log.c
):跟踪缓冲区的修改,确保数据一致性。
- 分配、释放块(