news 2026/10/3 12:06:56

《从零手写操作系统 (19):Ext2文件系统实战——从内存到磁盘的跨越》

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
《从零手写操作系统 (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读懂现代编译器输出的二进制!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 12:04:51

VS Code 常用插件推荐:把 settings.json 改到 TaoToken 统一管理 AI 补全

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 12:03:12

Cursor 的 online RL 实践:TaoToken 统一 Key 通道下的 RL Infra 调试与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 12:02:45

多源生活服务数据难聚合?Scrapy采集架构设计与落地全实战

做过本地生活运营或者数据分析的朋友大概率碰过这个难题&#xff1a;商家信息、团购套餐、用户评分散在各大平台&#xff0c;格式不统一、更新还快&#xff0c;靠人工整理效率低到离谱&#xff0c;写几个单页脚本凑合用&#xff0c;平台一改版就全崩。 去年我给一家本地商业运营…

作者头像 李华