访问 FAT32 文件系统 (选做)
编程实验 (基于 Linux)
截止时间:2025年1月17日 23:59
提交内容:
- 代码源文件
fat32.c
:注意代码中应有必要的注释。- 实验报告
学号.pdf
:简要描述实验中遇到的关键问题及解决方案、或者印象最深的 bugs 即可 (1~2 页)。如无特殊需要描述的,本次实验可以不提交实验报告。由于本次实验为选做实验,因此不提供诚信分,最终实验得分将以加分的方式记入实验成绩。
提交方式:TBD
FAT (File Allocation Table) 是 MS-DOS 和早期版本的 Windows 等操作系统中默认使用的文件系统。目前 FAT 家族已从 FAT12、FAT16 发展道 FAT32 和 exFAT 等,该文件系统在设计和实现上的简单性也使其在 USB 存储卡等移动存储介质上得到了广泛应用。 在本次实验中,我们需要编写 C 语言代码来实现对 FAT32 文件系统的访问,包括对文件的打开、读取和关闭等操作,以此来更好地理解真实文件系统的设计和实现。
运行如下命令下载本实验的框架代码:
git clone https://git.nju.edu.cn/oslab/fat32.git
cd fat32
🗒️ 实验内容
FAT 文件系统的布局 (File System Layout) 由如下四个部分组成:
- Reserved Region:存储 FAT 文件系统的各项属性信息 (Super Block),其中第一个扇区 (sector) 存储了 FAT 文件系统的 BPB (BIOS Parameter Block) 结构;
- FAT Region:存储文件分配表 FAT 结构,通常包括两个表,其中一个作为备份;
- Root Directory Region:存储根目录内容,注意 FAT32 没有 Root Directory Region;
- Data (File and Directory) Region:存储具体文件数据,以数据块 (Data Block) 来作为逻辑存储单元,其中每个数据块由物理存储上的若干连续扇区 (sectors) 构成。FAT 文件系统将每个数据块称为一个簇 (cluster),每个簇对应一个 FAT 表项。
我们可以使用 mmap
来将一个 FAT 文件系统镜像映射到进程虚拟地址空间中,随后就可以像操作内存中的某个数据结构一样来读取和分析文件系统中各个部分的内容:只要能计算得到正确的 “指针” (例如,第 K 个 cluster 或 sector 的起始地址),你就能访问文件系统中的特定数据。
为了知道你想访问的数据位于整个文件系统的什么地方,你需要参考 FAT 文件系统的规格说明。该手册内容并不是很多,是了解真实文件系统具体设计的一个很好的出发点。特别地,你需要注意查看:
- Sections 3.1 和 3.3:了解 BPB 结构体中包含的内容、以及各项的含义;
- Section 3.5:了解如何计算得到 Data Region 的起始地址;
- Section 4:了解 FAT 表的结构、以及如何根据簇号计算得到该簇在 FAT 表中的位置;
- Section 6:了解目录项的具体结构及其中各项的含义、以及如何计算得到编号为 K 的簇的第一个 sector 的起始地址。
框架代码和实现需求
我们提供的框架代码中包括如下三个文件:
fat32.h
:该头文件提供了 FAT32 的 BPB 结构Fat32BPB
和目录项结构DirEntry
等;fat32.c
:该文件包含了你需要实现的 API 接口定义,包括挂载文件系统镜像,打开、关闭和读取普通文件、以及读取目录文件;main.c
:该文件包含一个简单的测试用例 (首先挂载文件系统,然后打开并读取根目录下的一个文件),我们对提交代码的测试也会以类似的方式进行。
我们在框架代码中也提供了一个大小为 64MB 的 FAT32 文件系统镜像 fat32.img
供大家进行测试。
基于框架代码中已经设计好的结构体,你需要实现 fat32.c
中的如下 API:
1. 挂载 FAT 磁盘镜像:int fat_mount(const char *path)
该 API 接收待挂载文件系统镜像路径 path
为输入,挂载成功返回 0
,否则返回 -1
(例如,挂载不存在的文件系统、或者重复挂载)。
框架代码中已经实现了对 Fat32BPB
结构体的读取,你需要对照 FAT 规格说明手册了解其中各项的具体含义,然后可以在此基础上进行修改和定制 (例如,计算得到 Data Region
的起始地址)。
2. 打开一个普通文件:int fat_open(const char *path)
该 API 接收一个普通文件的绝对路径 path
为输入 (例如,/DIR_1/111.txt
),打开成功时返回一个 int
类型的文件描述符 (从 0
开始编号),打开失败返回 -1
(例如,打开一个不存在的文件、或打开一个目录文件)。
在这里你需要自行维护一个打开文件表 (即返回的文件描述符应是该打开文件表的索引,而不是当前操作系统上的文件描述符),并支持同时打开至少 128 个文件。如果已经打开了 128 个文件,再次调用 fat_open()
时直接返回 -1
。
3. 关闭一个已打开文件:int fat_close(int fd)
该 API 接收一个已打开文件的文件描述符 fd
为输入,关闭成功返回 0
,否则返回 -1
(例如,关闭一个未打开的文件)。
4. 读取普通文件内容:int fat_pread(int fd, void *buffer, int count, int offset)
该 API 将读取文件描述符 fd
对应文件从 offset
开始、长度为 count
的内容,并将其复制到缓冲区 buffer
,读取成功时返回真实读取的数据长度,这里的 offset
、count
和返回值均以字节为单位。
在读取到文件末尾时,返回值可能会小于 count
;如果输入参数 count
值为 0
或者 offset
超过了文件末尾,则应返回 0
。对于其它读取失败的情况,返回 -1
。
5. 读取目录项:struct FilesInfo* fat_readdir(const char *path)
该 API 接收一个目录文件的绝对路径 path
为输入 (根目录为 /
),返回该目录文件中的所有文件信息 (每个文件的文件名和大小,包括普通文件和目录文件,文件名格式为 FAT 系统中存储的格式,如文件 a.txt
在系统中存储的格式应为 A TXT
),若读取失败则返回 null
(例如,读取的目录文件路径不存在、或者为非目录文件)。注意在目录文件中会存在空闲的目录项 (例如,文件被删除),这些空闲目录项不应该包含在该 API 的返回结果中。
为了简化上述 API 的实现:
- 你可以仅考虑 FAT32 而不需要对其它 FAT 文件系统格式进行兼容;
- 仅需要支持短文件名,即可以假设文件系统中所有文件都采用
8.3
格式来命名 (即最长 8 个字符文件名 + 3 个字符扩展名),且不区分大小写; - 可以假设
path
输入参数中不包含.
或..
,即不会有/a/b/../c/d.txt
的情况; - 不需要考虑多个线程/进程同时打开和读取文件的情况。
📊 结果评估
我们会使用若干不同的 FAT32 文件系统镜像以及不同的 API 调用序列来对你实现的 fat32.c
的正确性进行测试。 由于本次实验为选做实验,因此测试将主要针对上述 API 的常见使用场景:
- 在大部分测试中,我们都将以正确的顺序、使用合法的参数调用
fat_pread()
和fat_readdir()
等 API,以此来检查代码是否能正常读取文件和目录内容 (考虑不同的文件和目录路径、以及不同的文件和目录大小)。 - 有少部分测试关注一些基本的错误处理逻辑 (均在实现需求中有明确说明)。例如,没有挂载就打开或读取文件、没有打开文件就进行读取、打开一个目录文件、多次关闭同一个文件等。
注意提交的代码不能更改框架代码 fat32.c
中引用的头文件、或增加新的头文件,否则会直接返回错误信息。
💭 Tips
为了查看我们提供的 FAT32 文件系统镜像的内容,并测试代码实现的正确性,你可以使用 Linux 中的 mount
命令来将 fat32.img
挂载到你当前的文件系统中:
sudo mount fat32.img /mnt/
然后你就可以通过熟悉的 cd
和 ls
等来在 /mnt/
中查看 fat32.img
中包含的具体文件信息。你也可以尝试编写代码来遍历 /mnt/
中的所有文件和目录,并和你实现的 API 的执行结果来进行自动化比对。
为了创建更多类似 fat32.img
这样的 FAT32 文件系统镜像用于测试,你可以先使用 dd
(data duplicator) 来创建一个特定大小的空文件 (这里利用了 /dev/zero
这个 “零” 设备来作为数据复制的原始地址):
dd if=/dev/zero of=fs.img bs=1M count=64
然后使用 mkfs.fat
来将其快速格式化为一个 FAT32 文件系统 (其中,-F
指明 FAT 类型,-S
指明 sector 大小,-s
指明每个 cluster 由多少个 sectors 组成):
mkfs.fat -F 32 -S 512 -s 8 fs.img
在此之后你就可以 mount
该文件系统,然后在其中按需创建各类文件和目录。本次实验用于测试的文件系统就是使用类似上述的方式来制作。