跳转至

文件系统 — 把一条字节长河切成有名有姓的文件

这一章讲什么: 到此为止,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)。

  1. 文件系统 = 名称↔块号对应表;块设备只认块号,名字和结构全是软件的记账;
  2. 学任何文件系统,第一步都是做一个最小卷然后 hexdump 验尸;
  3. BPB 记布局(块多大/几份 FAT/根目录在哪簇),全部位置都由它算出来;
  4. 目录 = 目录项数组;32 字节一条;首字节 0xE5 空闲、0x00 终止;
  5. 8+3 短名的现实:长文件名自动缩成 HELOWO~1DAT,长名条目另存(本书跳过);
  6. 簇号转地址:数据区起点 + (簇号-2) × 每簇字节数;
  7. 能借固件就别急着写驱动(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」)

  1. 出处:「17.1 文件和文件系统」(text/110-ch17-01-17-1.txt:4(搜「任意的字节序列」); text/110-ch17-01-17-1.txt:17(搜「以管理名称和块编号对应表的能力为核心」))。 ↩

  2. 出处:「17.1 文件和文件系统」(text/110-ch17-01-17-1.txt:20(搜「UEFI标准中使用FAT」); text/110-ch17-01-17-1.txt:21(搜「测试方便」))。 ↩

  3. 出处:「17.1 文件和文件系统」(text/110-ch17-01-17-1.txt:41(搜「每个簇的扇区数为2」); text/110-ch17-01-17-1.txt:28(搜「MIKAN OS」))。 ↩

  4. 出处:「17.1 文件和文件系统」(text/110-ch17-01-17-1.txt:88(搜「两个地方」); text/110-ch17-01-17-1.txt:90(搜「deadbeef」))。 ↩

  5. 出处:「17.1 文件和文件系统」(text/110-ch17-01-17-1.txt:114(搜「HELOWO~1DAT」))。 ↩

  6. 出处:「17.2 BIOS参数块」(text/111-ch17-02-17-2-bios.txt:7(搜「PBR」); text/112-ch17-03-17-3.txt:29(搜「2064」))。 ↩

  7. 出处:「17.3 目录项」(text/112-ch17-03-17-3.txt:36(搜「32字节的数据结构数组」); text/112-ch17-03-17-3.txt:43(搜「表17.3」))。 ↩

  8. 出处:「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」))。 ↩

  9. 出处:「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(搜「是否足够」))。 ↩

  10. 出处:「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(搜「完全匹配」))。 ↩

  11. 出处:「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个目录条目」))。 ↩

  12. 出处:「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」))。 ↩

  13. 出处:「17.3 目录项」(text/112-ch17-03-17-3.txt:36(搜「32字节的数据结构数组」); text/112-ch17-03-17-3.txt:43(搜「表17.3」))。 ↩

  14. 出处:「17.1 文件和文件系统」(text/110-ch17-01-17-1.txt:20(搜「UEFI标准中使用FAT」); text/110-ch17-01-17-1.txt:21(搜「测试方便」))。 ↩

  15. 出处:「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个目录条目」))。 ↩

  16. 出处:「17.4 读出卷」(text/113-ch17-04-17-4-osbook-day17a.txt:21,搜「相当困难」);:24,搜「Block I/O」)。 ↩