ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

《从零手写操作系统 (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 PIOProgrammed I/OCPU逐字读写端口传输数据。它慢~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个扇区(sector512B)到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大小(v1256, v0128) 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 1byte 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_size1024时代后来支持4KB块时为了向后兼容保留了512B单位。这意味着i_blocks * 512 ≠ i_size因为稀疏文件和尾部碎片。永远不要用i_blocks计算文件大小只用i_size。i_blocks仅用于磁盘配额统计。3. 为什么目录项的name不是null-terminatedExt2目录项用name_len显式标记长度name字段后紧跟下一个dir_entry由rec_len对齐。这节省了每个条目1字节的\0空间在大量小文件名场景下显著减少磁盘占用。你的解析代码必须用memcpy手动截断绝不能直接用strcmp或strlen——后者会越过边界读到垃圾数据甚至触发#PF。调试ChecklistExt2文件系统排查症状可能原因排查方法超级块magic错误LBA计算错误/字节序问题/读取偏移不对hex dump sb_buf前4字节确认验证ata_read_sectors(2,...)对应byte 1024确认struct packedinode读到全零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.htmlLinux Kernel:fs/ext2/inode.c,fs/ext2/dir.cOSDev Wiki - ATA PIO Mode / Ext2Understanding 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读懂现代编译器输出的二进制
返回列表