《从零手写操作系统 (19):Ext2文件系统实战——从内存到磁盘的跨越》
前言:让数据“活过重启”
在上一章中,我们实现了RamFS,拥有了完整的VFS抽象和inode/dentry模型。但RamFS有一个致命缺陷:断电即失忆。一个无法持久化存储的操作系统,永远只是一个高级玩具。今天,我们将跨越从“内存数据结构”到“磁盘物理布局”的鸿沟,实现真正的Ext2文件系统读取。
Ext2是Linux早期事实标准,也是教学文件系统的黄金选择。它的on-disk结构清晰、文档完备、无日志复杂性,完美展示了Unix FS的核心设计:超级块、块组、inode表、间接块寻址。本章实现的Ext2驱动将直接挂载到你的VFS层上,上一章写的path_resolve、vfs_open等代码一行不改即可复用。
本章里程碑:
- ✅ 实现ATA/IDE磁盘驱动(PIO模式):扇区级读写
- ✅ 解析Ext2超级块与块组描述符表
- ✅ 实现inode定位与读取(含间接块寻址)
- ✅ 解析目录项列表,对接VFS dentry缓存
- ✅ 将Ext2注册为VFS后端,支持只读挂载
- ✅ 验证:从QEMU虚拟磁盘读取文件并cat输出
核心概念:Ext2的物理布局与寻址数学
磁盘不是数组,而是“块组拼接的二维表格”
Ext2将磁盘划分为多个块组(Block Group),每个块组是自包含的迷你文件系统:
[Boot] [Superblock] [GDT] [BG0: Bmap|Imap|ITable|Data] [BG1: ...] [BG2: ...]
- Superblock:全局元数据(总inode数、块大小、每块组inode数等)
- GDT (Group Descriptor Table):每个块组的位图和inode表起始块号
- Inode Table:该块组所有inode的连续数组
- Data Blocks:实际文件内容
⚠️ 关键公式:给定inode号
ino,定位其物理位置的数学推导:group_index = (ino - 1) / inodes_per_group local_ino_index = (ino - 1) % inodes_per_group inode_block = gdt[group_index].inode_table + (local_ino_index * inode_size) / block_size offset_in_block = (local_ino_index * inode_size) % block_size这个公式是Ext2读取的基石。任何一步整数除法或取模出错,都会读到垃圾数据。
间接块:小文件友好与大文件支持的平衡
Ext2 inode中有15个块指针:
i_block[0..11]:直接块i_block[12]:一级间接(指向一个块,该块存储N个数据块号)i_block[13]:二级间接(指向块→存储N个一级间接块号)i_block[14]:三级间接
对于4KB块大小,单个文件最大可达 (12 + 1024 + 1024² + 1024³) × 4KB ≈ 16TB。但99%的文件只用前几个直接块。这种设计避免了小文件的额外I/O开销,同时保留了大文件扩展能力。
PIO模式的局限性与教学价值
本章使用ATA PIO(Programmed I/O):CPU逐字读写端口传输数据。它慢(~1MB/s)、阻塞、不支持DMA。但PIO的优势是零依赖:不需要配置PCI、不需要理解总线主控、不需要中断合并调试。对于首次接触磁盘驱动的开发者,PIO是唯一能在一天内跑通的选择。性能优化留给后续章节的AHCI/NVMe驱动。
实战代码
ATA PIO磁盘驱动
// drivers/ata.c
#include "io.h"
#include "kprintf.h"
#define ATA_PRIMARY_IO 0x1F0
#define ATA_REG_DATA 0x00
#define ATA_REG_SECCOUNT 0x02
#define ATA_REG_LBA_LO 0x03
#define ATA_REG_LBA_MID 0x04
#define ATA_REG_LBA_HI 0x05
#define ATA_REG_DEVICE 0x06
#define ATA_REG_COMMAND 0x07
#define ATA_REG_STATUS 0x07
#define ATA_CMD_READ_PIO 0x20
#define ATA_STATUS_BSY 0x80
#define ATA_STATUS_DRQ 0x08
#define ATA_STATUS_ERR 0x01
static void ata_wait_ready(void) {
while (inb(ATA_PRIMARY_IO + ATA_REG_STATUS) & ATA_STATUS_BSY);
}
static int ata_wait_drq(void) {
for (int i = 0; i < 100000; i++) {
uint8_t status = inb(ATA_PRIMARY_IO + ATA_REG_STATUS);
if (status & ATA_STATUS_ERR) return -1;
if (status & ATA_STATUS_DRQ) return 0;
}
return -1; // Timeout
}
// ★ 读取count个扇区(sector=512B)到buf
int ata_read_sectors(uint32_t lba, uint8_t count, void *buf) {
ata_wait_ready();
outb(ATA_PRIMARY_IO + ATA_REG_SECCOUNT, count);
outb(ATA_PRIMARY_IO + ATA_REG_LBA_LO, lba & 0xFF);
outb(ATA_PRIMARY_IO + ATA_REG_LBA_MID, (lba >> 8) & 0xFF);
outb(ATA_PRIMARY_IO + ATA_REG_LBA_HI, (lba >> 16) & 0xFF);
outb(ATA_PRIMARY_IO + ATA_REG_DEVICE, 0xE0 | ((lba >> 24) & 0x0F));
outb(ATA_PRIMARY_IO + ATA_REG_COMMAND, ATA_CMD_READ_PIO);
uint16_t *p = (uint16_t *)buf;
for (int s = 0; s < count; s++) {
if (ata_wait_drq() < 0) return -1;
for (int i = 0; i < 256; i++) { // 512B = 256 words
*p++ = inw(ATA_PRIMARY_IO + ATA_REG_DATA);
}
}
return 0;
}
// ★ 按Ext2块大小读取(自动处理多扇区)
int ext2_read_block(uint32_t block_no, void *buf, uint32_t block_size) {
uint32_t sectors_per_block = block_size / 512;
uint32_t start_sector = block_no * sectors_per_block;
return ata_read_sectors(start_sector, sectors_per_block, buf);
}
Ext2超级块与GDT解析
// fs/ext2.h
#define EXT2_MAGIC 0xEF53
#define EXT2_ROOT_INO 2
typedef struct __attribute__((packed)) {
uint32_t s_inodes_count;
uint32_t s_blocks_count;
uint32_t s_r_blocks_count;
uint32_t s_free_blocks_count;
uint32_t s_free_inodes_count;
uint32_t s_first_data_block;
uint32_t s_log_block_size; // block_size = 1024 << log
uint32_t s_log_frag_size;
uint32_t s_blocks_per_group;
uint32_t s_frags_per_group;
uint32_t s_inodes_per_group;
uint32_t s_mtime;
uint32_t s_wtime;
uint16_t s_mnt_count;
uint16_t s_max_mnt_count;
uint16_t s_magic; // ★ 必须==0xEF53
uint16_t s_state;
uint16_t s_errors;
uint16_t s_minor_rev_level;
uint32_t s_lastcheck;
uint32_t s_checkinterval;
uint32_t s_creator_os;
uint32_t s_rev_level;
uint16_t s_def_resuid;
uint16_t s_def_resgid;
// v1+ fields...
uint32_t s_first_ino;
uint16_t s_inode_size; // ★ inode大小(v1+=256, v0=128)
uint16_t s_block_group_nr;
uint32_t s_feature_compat;
uint32_t s_feature_incompat;
uint32_t s_feature_ro_compat;
uint8_t s_uuid[16];
char s_volume_name[16];
// ... padding to 1024 bytes
} ext2_superblock_t;
typedef struct __attribute__((packed)) {
uint32_t bg_block_bitmap;
uint32_t bg_inode_bitmap;
uint32_t bg_inode_table; // ★ inode表起始块号
uint16_t bg_free_blocks_count;
uint16_t bg_free_inodes_count;
uint16_t bg_used_dirs_count;
uint16_t bg_pad;
uint8_t bg_reserved[12];
} ext2_group_desc_t;
typedef struct __attribute__((packed)) {
uint16_t i_mode;
uint16_t i_uid;
uint32_t i_size;
uint32_t i_atime;
uint32_t i_ctime;
uint32_t i_mtime;
uint32_t i_dtime;
uint16_t i_gid;
uint16_t i_links_count;
uint32_t i_blocks; // 512B扇区数,非字节!
uint32_t i_flags;
uint32_t i_osd1;
uint32_t i_block[15]; // ★ 块指针
uint32_t i_generation;
uint32_t i_file_acl;
uint32_t i_dir_acl;
uint32_t i_faddr;
uint8_t i_osd2[12];
} ext2_inode_t;
// 运行时Ext2状态
typedef struct {
ext2_superblock_t sb;
ext2_group_desc_t *gdt; // kmalloc分配
uint32_t block_size;
uint32_t inodes_per_group;
uint32_t inode_size;
uint32_t num_groups;
} ext2_fs_t;
extern ext2_fs_t ext2;
// fs/ext2.c - 初始化
#include "ext2.h"
#include "memory.h"
ext2_fs_t ext2;
int ext2_init(void) {
// 超级块固定在偏移1024处(第2个扇区开始)
uint8_t sb_buf[1024];
if (ata_read_sectors(2, 2, sb_buf) < 0) {
kprintf("[EXT2] Failed to read superblock\n");
return -1;
}
memcpy(&ext2.sb, sb_buf, sizeof(ext2_superblock_t));
if (ext2.sb.s_magic != EXT2_MAGIC) {
kprintf("[EXT2] Bad magic: 0x%x\n", ext2.sb.s_magic);
return -1;
}
ext2.block_size = 1024 << ext2.sb.s_log_block_size;
ext2.inode_size = (ext2.sb.s_rev_level >= 1) ? ext2.sb.s_inode_size : 128;
ext2.inodes_per_group = ext2.sb.s_inodes_per_group;
ext2.num_groups = (ext2.sb.s_inodes_count + ext2.inodes_per_group - 1)
/ ext2.inodes_per_group;
// 读取GDT:紧跟超级块之后
uint32_t gdt_start_block = (ext2.block_size == 1024) ? 2 : 1;
uint32_t gdt_bytes = ext2.num_groups * sizeof(ext2_group_desc_t);
ext2.gdt = kmalloc(gdt_bytes);
if (!ext2.gdt) return -1;
// GDT可能跨多个块,简化假设单块可容纳
if (ext2_read_block(gdt_start_block, ext2.gdt, ext2.block_size) < 0) {
kprintf("[EXT2] Failed to read GDT\n");
return -1;
}
kprintf("[EXT2] Mounted. Block=%d, InodeSize=%d, Groups=%d\n",
ext2.block_size, ext2.inode_size, ext2.num_groups);
return 0;
}
inode读取与间接块寻址
// fs/ext2.c - inode操作
int ext2_read_inode(uint32_t ino, ext2_inode_t *out) {
if (ino == 0 || ino > ext2.sb.s_inodes_count) return -1;
uint32_t group = (ino - 1) / ext2.inodes_per_group;
uint32_t index = (ino - 1) % ext2.inodes_per_group;
uint32_t inode_table_block = ext2.gdt[group].bg_inode_table;
uint32_t byte_offset = index * ext2.inode_size;
uint32_t block_containing_inode = inode_table_block +
byte_offset / ext2.block_size;
uint32_t offset_in_block = byte_offset % ext2.block_size;
uint8_t *block_buf = kmalloc(ext2.block_size);
if (!block_buf) return -1;
if (ext2_read_block(block_containing_inode, block_buf, ext2.block_size) < 0) {
kfree(block_buf);
return -1;
}
memcpy(out, block_buf + offset_in_block, sizeof(ext2_inode_t));
kfree(block_buf);
return 0;
}
// ★ 逻辑块号 → 物理块号(含间接块解析)
uint32_t ext2_get_physical_block(ext2_inode_t *inode, uint32_t logical_block) {
uint32_t ptrs_per_block = ext2.block_size / sizeof(uint32_t);
// 直接块
if (logical_block < 12) {
return inode->i_block[logical_block];
}
logical_block -= 12;
// 一级间接
if (logical_block < ptrs_per_block) {
uint32_t *indirect = kmalloc(ext2.block_size);
ext2_read_block(inode->i_block[12], indirect, ext2.block_size);
uint32_t phys = indirect[logical_block];
kfree(indirect);
return phys;
}
logical_block -= ptrs_per_block;
// 二级间接
if (logical_block < ptrs_per_block * ptrs_per_block) {
uint32_t *l1 = kmalloc(ext2.block_size);
ext2_read_block(inode->i_block[13], l1, ext2.block_size);
uint32_t l1_idx = logical_block / ptrs_per_block;
uint32_t l2_idx = logical_block % ptrs_per_block;
uint32_t *l2 = kmalloc(ext2.block_size);
ext2_read_block(l1[l1_idx], l2, ext2.block_size);
uint32_t phys = l2[l2_idx];
kfree(l2); kfree(l1);
return phys;
}
// 三级间接省略(教学OS极少用到)
kprintf("[EXT2] Triple indirect not implemented\n");
return 0;
}
// ★ 读取文件内容到用户缓冲
ssize_t ext2_read_file(uint32_t ino, uint32_t offset, void *buf, size_t count) {
ext2_inode_t inode;
if (ext2_read_inode(ino, &inode) < 0) return -1;
if (offset >= inode.i_size) return 0;
if (offset + count > inode.i_size) count = inode.i_size - offset;
uint32_t bytes_read = 0;
uint8_t *block_buf = kmalloc(ext2.block_size);
while (bytes_read < count) {
uint32_t log_blk = (offset + bytes_read) / ext2.block_size;
uint32_t blk_off = (offset + bytes_read) % ext2.block_size;
uint32_t chunk = ext2.block_size - blk_off;
if (chunk > count - bytes_read) chunk = count - bytes_read;
uint32_t phys_blk = ext2_get_physical_block(&inode, log_blk);
if (phys_blk == 0) break; // Sparse file hole
ext2_read_block(phys_blk, block_buf, ext2.block_size);
memcpy((uint8_t *)buf + bytes_read, block_buf + blk_off, chunk);
bytes_read += chunk;
}
kfree(block_buf);
return bytes_read;
}
目录解析与VFS对接
// fs/ext2.c - 目录遍历
typedef struct __attribute__((packed)) {
uint32_t inode;
uint16_t rec_len;
uint8_t name_len;
uint8_t file_type;
// name follows (name_len bytes, NOT null-terminated!)
} ext2_dir_entry_t;
// ★ 回调式目录遍历,避免一次性加载整个目录
int ext2_readdir(uint32_t dir_ino,
void (*callback)(const char *name, uint32_t ino, uint8_t type, void *ctx),
void *ctx) {
ext2_inode_t inode;
if (ext2_read_inode(dir_ino, &inode) < 0) return -1;
uint8_t *block_buf = kmalloc(ext2.block_size);
uint32_t pos = 0;
while (pos < inode.i_size) {
uint32_t log_blk = pos / ext2.block_size;
uint32_t phys_blk = ext2_get_physical_block(&inode, log_blk);
if (phys_blk == 0) break;
ext2_read_block(phys_blk, block_buf, ext2.block_size);
uint32_t off_in_blk = 0;
while (off_in_blk < ext2.block_size && pos < inode.i_size) {
ext2_dir_entry_t *de = (ext2_dir_entry_t *)(block_buf + off_in_blk);
if (de->rec_len == 0) break; // Corrupt or end
if (de->inode != 0 && de->name_len > 0) {
char name[256];
memcpy(name, (char *)de + 8, de->name_len);
name[de->name_len] = '\0';
callback(name, de->inode, de->file_type, ctx);
}
off_in_blk += de->rec_len;
pos += de->rec_len;
}
}
kfree(block_buf);
return 0;
}
// VFS glue: 将Ext2 inode转为VFS inode缓存
// (此处省略完整glue代码,核心思路:ext2_readdir填充dentry树,
// vfs_open时调用ext2_read_inode构建内存inode节点)
关键细节解析
1. 为什么超级块在偏移1024而非0?
块大小为1024时,超级块占据整个block 1(byte 1024-2047)。块大小为4096时,超级块仍在byte 1024处,但只占block 0的一部分。这个固定偏移是Ext2的硬性约定,使得bootloader可以在不解析FS的情况下跳过超级块区域。如果你的ata_read_sectors(2, ...)读不到有效magic,首先检查LBA计算是否正确(sector 2 = byte 1024)。
2. 为什么i_blocks单位是512B扇区而非block_size?
这是历史遗留:Ext2诞生于block_size=1024时代,后来支持4KB块时为了向后兼容保留了512B单位。这意味着i_blocks * 512 ≠ i_size(因为稀疏文件和尾部碎片)。永远不要用i_blocks计算文件大小,只用i_size。i_blocks仅用于磁盘配额统计。
3. 为什么目录项的name不是null-terminated?
Ext2目录项用name_len显式标记长度,name字段后紧跟下一个dir_entry(由rec_len对齐)。这节省了每个条目1字节的'\0'空间,在大量小文件名场景下显著减少磁盘占用。你的解析代码必须用memcpy+手动截断,绝不能直接用strcmp或strlen——后者会越过边界读到垃圾数据甚至触发#PF。
调试Checklist:Ext2文件系统排查
| 症状 | 可能原因 | 排查方法 |
|---|---|---|
| 超级块magic错误 | LBA计算错误/字节序问题/读取偏移不对 | hex dump sb_buf前4字节确认;验证ata_read_sectors(2,...)对应byte 1024;确认struct packed |
| inode读到全零 | group/index公式整数溢出/inode_table块号错误 | kprintf打印group、index、block_containing_inode;用debugfs在宿主机验证同一ino的物理位置 |
| 文件内容乱码 | 间接块索引越界/block_buf未初始化/offset计算错 | 对已知小文件(纯直接块)先验证直读正确;再测需间接块的文件;dump logical→physical映射 |
| 目录遍历遗漏条目 | rec_len对齐错误/name_len边界检查缺失 | 用hex dump原始目录块对照spec手动验证;确认while循环条件同时检查off_in_blk和pos |
| 大文件读取截断 | get_physical_block返回0/三级间接未实现 | 确认测试文件<4MB(仅需一级间接);dump i_block[12]值及间接块内容 |
| QEMU磁盘镜像无法识别 | mkfs.ext2参数不匹配/镜像格式错误 | 宿主机执行file disk.img确认ext2;用debugfs disk.img -R ls验证镜像本身完好 |
🔧 黄金法则:Ext2调试的终极武器是宿主机的debugfs工具。在你的开发机上执行:
debugfs disk.img debugfs: stat <2> # 查看根目录inode原始字段 debugfs: dump <ino> /tmp/f # 导出文件内容对比 debugfs: ncheck <ino> # 反查路径永远不要猜测磁盘上的数据长什么样。当你的OS读出异常值时,先用debugfs确认那个位置到底存了什么。如果debugfs显示正确而你的代码读错,问题一定在你的LBA计算或struct布局中。
本章小结与下一步
今天我们完成了操作系统最关键的跨越之一:
- ✅ 实现了ATA PIO磁盘驱动,获得扇区级读写能力
- ✅ 完整解析Ext2超级块、GDT、inode表与间接块
- ✅ 目录遍历对接VFS,实现只读文件访问
- ✅ 验证了从QEMU虚拟磁盘读取真实文件的端到端链路
从此,你的操作系统拥有了跨重启的持久记忆。当你在自制Shell中执行cat /etc/motd并从磁盘中读出内容时,你见证的是存储栈从抽象到物理的完整贯通。
下一章预告:《ELF加载器与动态链接基础》
当前的exec只能加载平坦二进制。下一章将实现ELF解析器:读取程序头、加载PT_LOAD段、处理符号重定位,让你的OS能够运行gcc编译的标准ELF可执行文件,并为未来的共享库打下基础。
参考资料
- Ext2 Filesystem Specification: https://www.nongnu.org/ext2-doc/ext2.html
- Linux Kernel:
fs/ext2/inode.c,fs/ext2/dir.c - OSDev Wiki - ATA PIO Mode / Ext2
- Understanding the Linux Virtual File System (Tigran Aivazian)
- 本系列完整代码:[你的GitHub仓库链接](Commit:
e1x2t3f)
📝 作者注:这是《从零手写操作系统》系列的第19篇。Ext2是实现过程中挫败感最强的章节之一。当你第三次发现inode公式差一个off-by-one、或者struct packing导致字段错位时,请记住:Linux内核的ext2代码经历了二十多年的bug修复才达到今天的稳定性。建议先用mke2fs创建一个最小镜像(
mkfs.ext2 -b 1024 -I 128 disk.img 1M),减少变量干扰。文件系统驱动的正确性是用十六进制验证出来的,不是用printf猜出来的。 下一章,我们让OS读懂现代编译器输出的二进制!
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)