前言:让数据“活过重启”

        在上一章中,我们实现了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读懂现代编译器输出的二进制!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐