文件系统 — 把一条字节长河切成有名有姓的文件¶
这一章讲什么: 到此为止,MikanOS 的内核「认识」鼠标、键盘、定时器、PCI, 却不认识自己脚下磁盘上的任何东西。本章补上这块: 文件系统本质上是三张表——开头一块参数(BPB)记卷的布局, 目录项把文件名映射到「起始簇号」,文件分配表(下一章)把簇串成文件。 表还没认全,ls 命令就能跑了:能报名字,是因为名字全躺在目录项数组里。
1. 先看现象:hexdump 验尸一个 U 盘¶
主线例子,一个 128 MiB 的 FAT32 卷镜像:
$ dd if=/dev/zero of=fat_disk bs=1M count=128
$ mkfs.fat -n 'MIKAN OS' -s 2 -f 2 -R 32 -F 32 fat_disk
-s 2 每簇 2 块 -f 2 两份 FAT -R 32 保留 32 块 -F 32 FAT32
写入 cafe.txt(内容 deadbeef)后再 hexdump,能看见:
· 卷名 "MIKAN OS" 躺在开头(保留区里)
· "CAFE....TXT" 和 "CAFE TXT" 各出现一次(短名 + 卷标区)
· 偏移 0x00102400 处躺着 deadbeef —— 文件内容
再写入 HeloWorld.data,名字超 8 字符 → 自动缩成 "HELOWO~1DAT"
—— 8+3 短名的铁律,亲眼看见它咬人
2. 顶层全景¶
17.1 概念:块设备只认块号;文件系统 = 名称↔块号对应表;选 FAT(公开、UEFI 自带、易做)
17.2 BPB:卷的头 512 字节里记布局:每块字节数、每簇块数、FAT 大小与份数、根目录簇号
17.3 目录项:32 字节一条(名 11 字节 / 属性 / 起始簇 / 文件大小);首字节 0xE5 空闲、0x00 到此为止
17.4 读卷:引导加载器用 UEFI Block I/O 把卷搬进内存
(两阶段:有 fat_disk 文件读文件,否则读引导介质前 16MiB),作为第 4 个参数交给内核
17.5 ls:根目录 = 目录项数组;簇号 → 字节地址 = (簇-2)×每簇块数×块字节 + 数据区起点
图说:先验尸(看清布局),再搬运(读进内存),最后点名(ls)。
3. 核心原理¶
3.1 文件是什么,文件系统管什么¶
文件 = 任意字节序列 + 名称(可能还有元数据)。 块设备(U 盘、SSD)只会按块号读写;裸用块的问题是「指定数据要靠数字编号, 大了放不下、小了浪费」1。文件系统的核心能力就是那张 名称 ↔ 块编号对应表:文件变大动态加块、变小回收块2。 选型理由很实际:ext4/btrfs/NTFS/APFS 都太重,而 UEFI 标准自己就用 FAT 存启动文件, 规范公开、好实现、真机上到处可用3。
3.2 BPB:卷的自我说明书¶
分区第一个块叫 PBR,其中一段 BPB(BIOS 参数块) 是理解整张卷的钥匙4。关键字段:
| 字段 | 含义 | fat_disk 里的值 |
|---|---|---|
| BPB_BytsPerSec | 每块字节数 | 0x0200 = 512 |
| BPB_SecPerClus | 每簇块数(簇 = 管理单位) | 0x02 |
| BPB_RsvdSecCnt | 卷首保留块数 | 0x20 |
| BPB_NumFATs / BPB_FATSz32 | FAT 份数 / 每份 FAT 的块数 | 2 / 0x3f8 |
| BPB_RootClus | 根目录起始簇号 | 2 |
由它们可算出根目录的字节位置:
保留块数 + FAT 份数×每份 FAT 块数 + (根目录簇号-2)×每簇块数 = 块号(fat_disk 得 2064 块)5。
簇(cluster)是 FAT 的管理单位,一大块连续块;簇号从 2 起算(0、1 是保留值)6。
3.3 目录项:32 字节的档案卡¶
FAT 里目录也是文件,内容是 32 字节目录项的数组7。一条目录项: DIR_Name(11 字节,8+3 短名)、DIR_Attr(属性:只读/隐藏/系统/卷标 0x08/目录 0x10/长名 0x0f)、 DIR_FstClusHI+LO(起始簇号)、DIR_FileSize(字节数)8。 三个特殊约定:名字首字节 0xE5 = 这条空闲;0x00 = 空闲且后面再没有有效项(遍历可以停了); 属性 0x0f 的条目是长名的附属条目,结构不同,本书跳过(长文件名处理繁琐,明说从略)16。
3.4 把卷搬进内存:再借 UEFI 一次¶
正经 OS 会给每个块设备写驱动(U 盘要 USB + SCSI 栈,NVMe 盘要 NVMe 栈)—— 「开发这些驱动程序相当困难」(作者脚注:他花了一年多才写出一个 USB 驱动)9。 所以本章再借固件:UEFI 规范里有 Block I/O 协议,引导加载器趁引导服务还在, 把卷读进内存再传给内核10。 两阶段策略:引导介质上有 fat_disk 文件就读它(开发期,内容可控); 没有就 ReadBlocks 读引导介质开头 16 MiB(真机试用;FAT 的数据从卷首顺序打包, 读开头就够;块 I/O 慢,16 MiB 是速度与实用的妥协)11。 卷镜像作为 KernelMain 的第 4 个参数进内核——验证方式依然是「十六进制打印前 256 字节, 与宿主机 hexdump 对答案」12。
3.5 ls:目录项数组的点名册¶
内核侧 fat::Initialize() 存好卷指针;ls 命令的逻辑就一层 for: 根目录起点 = 卷首 + 根目录块号×块字节;指针强转成 DirectoryEntry 数组, 从下标 0 逐个读:首字节 0xE5 跳过、0x00 收工、长名条目跳过,其余把 8+3 名拆成 base.ext 打印13。 一簇装多少条目?每簇字节 ÷ 32(例:2×512÷32 = 32 条);超出一个簇的根目录要沿 FAT 链走, 那是第 18 章的活14。跑起来:终端里 ls 报出 EFI、KERNEL.ELF、MEMMAP…… 「能在自己的操作系统上执行 ls 命令,是不是很厉害?笔者是感动到了」15。
4. 作者的判断与证据¶
判断:讲文件系统从验尸开始。 不先讲规范,先 mkfs 一个空卷、写入文件、hexdump 找 「文件名在哪、内容在哪」——两张截图(空卷/写入后)胜过十页字段定义。
判断:块设备驱动这次跳过,但要说明跳过的代价。 16 MiB 上限、只读引导介质、 真机分区被挤到角落(专栏 17.1:整块大分区时 16 MiB 不够,作者重新划了个 1 GiB 分区)—— 每一步都是「借固件」的边界,明写。
证据: hexdump 对照(卷镜像前 256 字节与 QEMU 的 disk.img 完全一致); ls 输出与宿主机挂载看到的文件列表一致。
5. 边界与局限¶
- 只读、只根目录、只短名:写文件、子目录、长文件名都不在本章(FAT 链跟随后续);
- 16 MiB 上限:大分区可能装不下 FAT 区本身;专栏承认要靠重新分区绕开;
- Block I/O 是引导期特权:内核运行后想再读盘,只能自己写驱动——本书没写;
- 簇号 0/1 与各种保留值的语义只讲了用到的部分,FAT 规范的细节(脏标志、FSInfo)未涉及。
6. 可带走的¶
本章主线例子,一行写完: mkfs 一个 FAT 卷 → hexdump 找到名字与内容 → 按 BPB 算出根目录块号 → UEFI Block I/O 搬进内存 → 目录项数组点名(ls)。
- 文件系统 = 名称↔块号对应表;块设备只认块号,名字和结构全是软件的记账;
- 学任何文件系统,第一步都是做一个最小卷然后 hexdump 验尸;
- BPB 记布局(块多大/几份 FAT/根目录在哪簇),全部位置都由它算出来;
- 目录 = 目录项数组;32 字节一条;首字节 0xE5 空闲、0x00 终止;
- 8+3 短名的现实:长文件名自动缩成
HELOWO~1DAT,长名条目另存(本书跳过); - 簇号转地址:
数据区起点 + (簇号-2) × 每簇字节数; - 能借固件就别急着写驱动(Block I/O),但把借来的边界(16MiB、只读)写清楚。
7. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 文件定义与文件系统职责 | 17.1 文件和文件系统 | text/110-ch17-01-17-1.txt:4(搜「任意的字节序列」) · text/110-ch17-01-17-1.txt:17(搜「以管理名称和块编号对应表的能力为核心」) |
| 选 FAT 的理由 | 17.1 文件和文件系统 | text/110-ch17-01-17-1.txt:20(搜「UEFI标准中使用FAT」) · text/110-ch17-01-17-1.txt:21(搜「测试方便」) |
| mkfs 选项与卷名 | 17.1 文件和文件系统 | text/110-ch17-01-17-1.txt:41(搜「每个簇的扇区数为2」) · text/110-ch17-01-17-1.txt:28(搜「MIKAN OS」) |
| 写文件后 hexdump 的发现 | 17.1 文件和文件系统 | text/110-ch17-01-17-1.txt:88(搜「两个地方」) · text/110-ch17-01-17-1.txt:90(搜「deadbeef」) |
| 8+3 与长名被咬 | 17.1 文件和文件系统 | text/110-ch17-01-17-1.txt:114(搜「HELOWO~1DAT」) |
| BPB 结构与根目录计算 | 17.2 BIOS参数块 | text/111-ch17-02-17-2-bios.txt:7(搜「PBR」) · text/112-ch17-03-17-3.txt:29(搜「2064」) |
| 目录项结构 | 17.3 目录项 | text/112-ch17-03-17-3.txt:36(搜「32字节的数据结构数组」) · text/112-ch17-03-17-3.txt:43(搜「表17.3」) |
| 首字节 0xE5/0x00 | 17.5 ls命令(osbook_day17b) | text/114-ch17-05-17-5-ls-osbook-day17b.txt:67(搜「0xE5」) · text/114-ch17-05-17-5-ls-osbook-day17b.txt:70(搜「0x00」) |
| 跳过块设备驱动的理由 | 17.4 读出卷(osbook_day17a) | text/113-ch17-04-17-4-osbook-day17a.txt:21(搜「相当困难」) · text/113-ch17-04-17-4-osbook-day17a.txt:24(搜「Block I/O」) |
| 两阶段读取与 16MiB 上限 | 17.4 读出卷(osbook_day17a) | text/113-ch17-04-17-4-osbook-day17a.txt:27(搜「两阶段」) · text/113-ch17-04-17-4-osbook-day17a.txt:52(搜「16MiB」) · text/113-ch17-04-17-4-osbook-day17a.txt:129(搜「是否足够」) |
| 十六进制对照验证 | 17.4 读出卷(osbook_day17a) | text/113-ch17-04-17-4-osbook-day17a.txt:107(搜「前256字节」) · text/113-ch17-04-17-4-osbook-day17a.txt:127(搜「完全匹配」) |
| ls 实现与一簇 32 条 | 17.5 ls命令(osbook_day17b) | text/114-ch17-05-17-5-ls-osbook-day17b.txt:16(搜「ls命令」) · text/114-ch17-05-17-5-ls-osbook-day17b.txt:54(搜「32个目录条目」) |
| GetClusterAddr 与簇号起点 | 17.5 ls命令(osbook_day17b) | text/114-ch17-05-17-5-ls-osbook-day17b.txt:92(搜「GetClusterAddr」) · text/114-ch17-05-17-5-ls-osbook-day17b.txt:95(搜「起始位置为2」) |
-
出处:「17.1 文件和文件系统」(
text/110-ch17-01-17-1.txt:4(搜「任意的字节序列」);text/110-ch17-01-17-1.txt:17(搜「以管理名称和块编号对应表的能力为核心」))。 ↩ -
出处:「17.1 文件和文件系统」(
text/110-ch17-01-17-1.txt:20(搜「UEFI标准中使用FAT」);text/110-ch17-01-17-1.txt:21(搜「测试方便」))。 ↩ -
出处:「17.1 文件和文件系统」(
text/110-ch17-01-17-1.txt:41(搜「每个簇的扇区数为2」);text/110-ch17-01-17-1.txt:28(搜「MIKAN OS」))。 ↩ -
出处:「17.1 文件和文件系统」(
text/110-ch17-01-17-1.txt:88(搜「两个地方」);text/110-ch17-01-17-1.txt:90(搜「deadbeef」))。 ↩ -
出处:「17.1 文件和文件系统」(
text/110-ch17-01-17-1.txt:114(搜「HELOWO~1DAT」))。 ↩ -
出处:「17.2 BIOS参数块」(
text/111-ch17-02-17-2-bios.txt:7(搜「PBR」);text/112-ch17-03-17-3.txt:29(搜「2064」))。 ↩ -
出处:「17.3 目录项」(
text/112-ch17-03-17-3.txt:36(搜「32字节的数据结构数组」);text/112-ch17-03-17-3.txt:43(搜「表17.3」))。 ↩ -
出处:「17.5 ls命令(osbook_day17b)」(
text/114-ch17-05-17-5-ls-osbook-day17b.txt:67(搜「0xE5」);text/114-ch17-05-17-5-ls-osbook-day17b.txt:70(搜「0x00」))。 ↩ -
出处:「17.4 读出卷(osbook_day17a)」(
text/113-ch17-04-17-4-osbook-day17a.txt:27(搜「两阶段」);text/113-ch17-04-17-4-osbook-day17a.txt:52(搜「16MiB」);text/113-ch17-04-17-4-osbook-day17a.txt:129(搜「是否足够」))。 ↩ -
出处:「17.4 读出卷(osbook_day17a)」(
text/113-ch17-04-17-4-osbook-day17a.txt:107(搜「前256字节」);text/113-ch17-04-17-4-osbook-day17a.txt:127(搜「完全匹配」))。 ↩ -
出处:「17.5 ls命令(osbook_day17b)」(
text/114-ch17-05-17-5-ls-osbook-day17b.txt:16(搜「ls命令」);text/114-ch17-05-17-5-ls-osbook-day17b.txt:54(搜「32个目录条目」))。 ↩ -
出处:「17.5 ls命令(osbook_day17b)」(
text/114-ch17-05-17-5-ls-osbook-day17b.txt:92(搜「GetClusterAddr」);text/114-ch17-05-17-5-ls-osbook-day17b.txt:95(搜「起始位置为2」))。 ↩ -
出处:「17.3 目录项」(
text/112-ch17-03-17-3.txt:36(搜「32字节的数据结构数组」);text/112-ch17-03-17-3.txt:43(搜「表17.3」))。 ↩ -
出处:「17.1 文件和文件系统」(
text/110-ch17-01-17-1.txt:20(搜「UEFI标准中使用FAT」);text/110-ch17-01-17-1.txt:21(搜「测试方便」))。 ↩ -
出处:「17.5 ls命令(osbook_day17b)」(
text/114-ch17-05-17-5-ls-osbook-day17b.txt:16(搜「ls命令」);text/114-ch17-05-17-5-ls-osbook-day17b.txt:54(搜「32个目录条目」))。 ↩ -
出处:「17.4 读出卷」(
text/113-ch17-04-17-4-osbook-day17a.txt:21,搜「相当困难」);:24,搜「Block I/O」)。 ↩