存储系统-ext2 篇:为自制 OS 实现经典文件系统

"ext2 是 Linux 的经典文件系统,结构精巧、文档齐全,
是自制操作系统的最佳学习起点。
本文将深度解析 ext2 磁盘布局、inode 寻址、目录结构,
并实现一个完整可用的 ext2 驱动。"

引言:为什么选择 ext2?

在自制操作系统中,实现文件系统是必经之路
ext2(Second Extended Filesystem)是最佳选择:

  • 结构清晰:超级块、块组、位图层次分明
  • 文档齐全:官方规范详细,社区资料丰富
  • 无日志:比 ext3/ext4 简单,适合教学
  • 广泛支持:Linux、QEMU、工具链完善

本文将为自制 OS 实现一个完整、高效、可读写的 ext2 驱动,
支持百万级文件组织、目录遍历、块分配等核心功能。


第一章:ext2 磁盘布局设计

1.1 整体布局

ext2 将磁盘划分为块组(Block Group),每个块组包含:

+-----------------+
|  超级块 (Superblock)  | ← 通常只在块组 0 有完整副本
+-----------------+
|  块组描述符 (GDT)    | ← 描述所有块组
+-----------------+
|  块位图 (Block Bitmap) |
+-----------------+
|  inode 位图 (Inode Bitmap) |
+-----------------+
|  inode 表 (Inode Table) |
+-----------------+
|  数据块 (Data Blocks)  |
+-----------------+

关键设计思想:

  • 局部性:每个块组尽量自包含,减少跨组访问
  • 冗余:超级块和 GDT 在多个块组备份
  • 可扩展:支持大磁盘(2TB)和大量文件(数百万)

1.2 核心数据结构

超级块(ext2_super_block)

// ext2.h
struct ext2_super_block {
    uint32_t s_inodes_count;    // inode 总数
    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; // 第一个数据块编号(通常为 1)
    uint32_t s_log_block_size;   // 块大小 = 1024 << s_log_block_size
    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;            // 状态(EXT2_VALID_FS, EXT2_ERROR_FS)
    uint32_t s_errors;           // 错误处理行为
    uint32_t s_first_ino;        // 第一个非保留 inode(11)
    uint16_t s_inode_size;       // inode 大小(128 字节)
    // ... 其他字段
} __attribute__((packed));

块组描述符(ext2_group_desc)

struct ext2_group_desc {
    uint32_t bg_block_bitmap;       // 块位图块号
    uint32_t bg_inode_bitmap;       // inode 位图块号
    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;
    uint32_t bg_reserved[3];
} __attribute__((packed));

inode(ext2_inode)

struct ext2_inode {
    uint16_t i_mode;        // 文件类型与权限
    uint16_t i_uid;         // 所有者 ID
    uint32_t i_size;        // 文件大小(字节)
    uint32_t i_atime;       // 访问时间
    uint32_t i_ctime;       // 创建时间
    uint32_t i_mtime;       // 修改时间
    uint32_t i_dtime;       // 删除时间
    uint16_t i_gid;         // 组 ID
    uint16_t i_links_count; // 硬链接数
    uint32_t i_blocks;      // 块数(512 字节块)
    uint32_t i_flags;       // 文件标志
    uint32_t i_osd1;        // OS 相关
    uint32_t i_block[15];   // 数据块指针
    uint32_t i_generation;  // 文件版本
    uint32_t i_file_acl;    // 访问控制列表
    uint32_t i_dir_acl;     // 目录 ACL(大文件大小)
    uint32_t i_faddr;       // 片地址
    uint32_t i_osd2[3];     // OS 相关
} __attribute__((packed));

第二章:inode 与数据块寻址

2.1 数据块寻址机制

ext2 使用 15 个指针 实现高效数据块寻址:

| i_block[0-11] | i_block[12] | i_block[13] | i_block[14] |
| 直接块        | 一级间接    | 二级间接    | 三级间接    |

寻址能力(4KB 块大小):

| 类型 | 指针数 | 总容量 | |——|——–|——–| | 直接块 | 12 | 48 KB | | 一级间接 | 1024 | 4 MB | | 二级间接 | 1024×1024 | 4 GB | | 三级间接 | 1024³ | 4 TB |

最大文件 4TB,足够教学使用

2.2 块地址解析函数

// ext2.c
static uint32_t ext2_get_block_address(struct ext2_inode *inode, 
                                      uint32_t block_index) {
    struct ext2_sb_info *sb = get_sb_info();
    uint32_t block_size = EXT2_BLOCK_SIZE(sb);
    uint32_t *indirect = NULL;
    
    // 1. 直接块
    if (block_index < 12) {
        return inode->i_block[block_index];
    }
    
    // 2. 一级间接
    if (block_index < 12 + 1024) {
        if (inode->i_block[12] == 0) return 0;
        indirect = (uint32_t*)kmalloc(block_size);
        block_read(sb->s_bdev, inode->i_block[12], indirect, 1);
        uint32_t addr = indirect[block_index - 12];
        kfree(indirect);
        return addr;
    }
    
    // 3. 二级间接(简化:仅演示)
    // ... 类似实现
    
    return 0;
}

2.3 块分配与释放

分配新块

static uint32_t ext2_new_block(struct ext2_sb_info *sb) {
    // 1. 查找空闲块(遍历块位图)
    for (int group = 0; group < sb->s_groups_count; group++) {
        struct ext2_group_desc *gd = &sb->s_group_desc[group];
        if (gd->bg_free_blocks_count == 0) continue;
        
        // 读取块位图
        uint8_t *bitmap = kmalloc(sb->s_block_size);
        block_read(sb->s_bdev, gd->bg_block_bitmap, bitmap, 1);
        
        // 查找空闲位
        for (int i = 0; i < sb->s_blocks_per_group; i++) {
            int byte = i / 8;
            int bit = i % 8;
            if (!(bitmap[byte] & (1 << bit))) {
                // 标记为已用
                bitmap[byte] |= (1 << bit);
                block_write(sb->s_bdev, gd->bg_block_bitmap, bitmap, 1);
                
                // 更新组描述符
                gd->bg_free_blocks_count--;
                write_group_desc(sb, group);
                
                kfree(bitmap);
                return gd->bg_block_bitmap + i; // 返回绝对块号
            }
        }
        kfree(bitmap);
    }
    return 0; // 无空闲块
}

释放块

static void ext2_free_block(struct ext2_sb_info *sb, uint32_t block) {
    // 1. 计算块组和组内偏移
    uint32_t blocks_per_group = sb->s_blocks_per_group;
    int group = block / blocks_per_group;
    int offset = block % blocks_per_group;
    
    // 2. 更新块位图
    struct ext2_group_desc *gd = &sb->s_group_desc[group];
    uint8_t *bitmap = kmalloc(sb->s_block_size);
    block_read(sb->s_bdev, gd->bg_block_bitmap, bitmap, 1);
    
    int byte = offset / 8;
    int bit = offset % 8;
    bitmap[byte] &= ~(1 << bit);
    block_write(sb->s_bdev, gd->bg_block_bitmap, bitmap, 1);
    
    kfree(bitmap);
    
    // 3. 更新组描述符
    gd->bg_free_blocks_count++;
    write_group_desc(sb, group);
}

第三章:目录结构与文件操作

3.1 目录项(ext2_dir_entry)

ext2 目录是特殊文件,内容为目录项数组:

struct ext2_dir_entry {
    uint32_t inode;         // inode 编号
    uint16_t rec_len;       // 记录长度(字节)
    uint8_t name_len;       // 名称长度
    uint8_t file_type;      // 文件类型(可选)
    char name[0];           // 可变长名称
} __attribute__((packed));

关键设计:

  • rec_len ≥ name_len + 8:支持记录删除(标记 inode=0)
  • rec_len 对齐:通常 4 字节对齐
  • 变长名称:name[0] 表示动态数组

3.2 目录查找

// ext2.c
static struct ext2_dir_entry *ext2_find_entry(struct ext2_inode *dir, 
                                             const char *name) {
    uint32_t size = dir->i_size;
    uint32_t blocks = (size + EXT2_BLOCK_SIZE(sb) - 1) / EXT2_BLOCK_SIZE(sb);
    
    for (uint32_t i = 0; i < blocks; i++) {
        uint32_t block_addr = ext2_get_block_address(dir, i);
        if (block_addr == 0) continue;
        
        char *block = kmalloc(EXT2_BLOCK_SIZE(sb));
        block_read(sb->s_bdev, block_addr, block, 1);
        
        // 遍历目录项
        char *ptr = block;
        while (ptr < block + EXT2_BLOCK_SIZE(sb)) {
            struct ext2_dir_entry *de = (void*)ptr;
            if (de->rec_len == 0) break;
            
            if (de->inode != 0 && 
                de->name_len == strlen(name) && 
                strncmp(de->name, name, de->name_len) == 0) {
                struct ext2_dir_entry *result = kmalloc(de->rec_len);
                memcpy(result, de, de->rec_len);
                kfree(block);
                return result;
            }
            
            ptr += de->rec_len;
        }
        kfree(block);
    }
    return NULL;
}

3.3 创建文件

static int ext2_create(struct vfs_inode *dir, const char *name, uint16_t mode) {
    // 1. 分配新 inode
    uint32_t inode_num = ext2_new_inode(sb);
    if (inode_num == 0) return -1;
    
    // 2. 初始化 inode
    struct ext2_inode *inode = kmalloc(sizeof(struct ext2_inode));
    memset(inode, 0, sizeof(struct ext2_inode));
    inode->i_mode = mode;
    inode->i_links_count = 1;
    inode->i_uid = 0;
    inode->i_gid = 0;
    inode->i_size = 0;
    inode->i_blocks = 0;
    // ... 时间戳
    
    // 3. 写入 inode 表
    ext2_write_inode(sb, inode_num, inode);
    
    // 4. 添加目录项
    ext2_add_entry(dir, name, inode_num, mode);
    
    kfree(inode);
    return 0;
}

3.4 添加目录项

static int ext2_add_entry(struct vfs_inode *dir, 
                         const char *name, 
                         uint32_t inode_num, 
                         uint16_t mode) {
    uint32_t block_size = EXT2_BLOCK_SIZE(sb);
    uint32_t dir_size = dir->i_size;
    uint32_t needed = 8 + strlen(name);
    needed = (needed + 3) & ~3; // 4 字节对齐
    
    // 1. 查找可插入的空隙
    uint32_t blocks = (dir_size + block_size - 1) / block_size;
    for (uint32_t i = 0; i < blocks; i++) {
        uint32_t block_addr = ext2_get_block_address(dir_inode, i);
        if (block_addr == 0) continue;
        
        char *block = kmalloc(block_size);
        block_read(sb->s_bdev, block_addr, block, 1);
        
        char *ptr = block;
        while (ptr < block + block_size) {
            struct ext2_dir_entry *de = (void*)ptr;
            if (de->rec_len == 0) break;
            
            // 检查空隙
            uint32_t used = 8 + de->name_len;
            uint32_t gap = de->rec_len - used;
            if (gap >= needed) {
                // 分裂记录
                struct ext2_dir_entry *new_de = (void*)(ptr + used);
                new_de->inode = inode_num;
                new_de->rec_len = gap;
                new_de->name_len = strlen(name);
                new_de->file_type = IFTODT(mode);
                strcpy(new_de->name, name);
                
                de->rec_len = used;
                
                block_write(sb->s_bdev, block_addr, block, 1);
                kfree(block);
                return 0;
            }
            
            ptr += de->rec_len;
        }
        kfree(block);
    }
    
    // 2. 无空隙,追加新块
    uint32_t new_block = ext2_new_block(sb);
    if (new_block == 0) return -1;
    
    char *new_block_data = kmalloc(block_size);
    memset(new_block_data, 0, block_size);
    
    struct ext2_dir_entry *de = (void*)new_block_data;
    de->inode = inode_num;
    de->rec_len = block_size;
    de->name_len = strlen(name);
    de->file_type = IFTODT(mode);
    strcpy(de->name, name);
    
    block_write(sb->s_bdev, new_block, new_block_data, 1);
    kfree(new_block_data);
    
    // 3. 更新目录 inode
    dir->i_size += block_size;
    dir->i_blocks += block_size / 512;
    ext2_write_inode(sb, dir->i_ino, dir);
    
    return 0;
}

第四章:ext2 与 VFS 集成

4.1 VFS 操作函数实现

inode_operations

// ext2.c
static struct vfs_inode_operations ext2_dir_inode_ops = {
    .lookup = ext2_lookup,
    .create = ext2_create,
    .mkdir = ext2_mkdir,
    .rmdir = ext2_rmdir,
    .unlink = ext2_unlink,
};

static struct vfs_inode_operations ext2_file_inode_ops = {
    .setattr = ext2_setattr,
};

file_operations

static struct vfs_file_operations ext2_file_ops = {
    .read = ext2_file_read,
    .write = ext2_file_write,
    .open = ext2_file_open,
    .release = ext2_file_release,
};

static struct vfs_file_operations ext2_dir_ops = {
    .read = ext2_dir_read,
    .open = ext2_dir_open,
    .release = ext2_dir_release,
};

4.2 文件读取实现

static ssize_t ext2_file_read(struct vfs_file *file, char *buf, size_t count) {
    struct ext2_inode *inode = file->f_inode->i_private;
    uint32_t pos = file->f_pos;
    uint32_t size = inode->i_size;
    
    if (pos >= size) return 0;
    if (pos + count > size) count = size - pos;
    
    uint32_t block_size = EXT2_BLOCK_SIZE(sb);
    uint32_t start_block = pos / block_size;
    uint32_t end_block = (pos + count + block_size - 1) / block_size;
    
    char *buffer = buf;
    for (uint32_t b = start_block; b < end_block; b++) {
        uint32_t block_addr = ext2_get_block_address(inode, b);
        if (block_addr == 0) {
            memset(buffer, 0, block_size);
        } else {
            char *block = kmalloc(block_size);
            block_read(sb->s_bdev, block_addr, block, 1);
            memcpy(buffer, block, block_size);
            kfree(block);
        }
        
        buffer += block_size;
    }
    
    // 复制所需部分
    uint32_t offset_in_block = pos % block_size;
    memcpy(buf, buf + offset_in_block, count);
    
    return count;
}

4.3 目录读取实现

static ssize_t ext2_dir_read(struct vfs_file *file, char *buf, size_t count) {
    // 简化:返回 dirent 结构
    struct ext2_inode *dir = file->f_inode->i_private;
    // ... 遍历目录项,填充 struct dirent
    return 0;
}

第五章:ext2 优化与 ext3 对比

5.1 块分配策略优化

当前限制:

  • 顺序扫描位图:O(n) 时间
  • 无预分配:频繁分配小文件导致碎片

优化方向:

  1. 空闲块缓存:缓存每个块组的空闲块范围
  2. 目录局部性:目录 inode 和数据块尽量同组
  3. 预分配:大文件分配连续块

5.2 ext3 日志机制对比

ext2 的致命缺陷:

  • 元数据不一致:写入过程中断电 → 文件系统损坏
  • fsck 慢:启动时需全盘检查

ext3 解决方案:

  • Journaling(日志):先写日志,再写数据
  • 三种模式
    • journal:元数据+数据都写日志(最安全)
    • ordered:元数据写日志,数据先写(默认)
    • writeback:仅元数据写日志(最快)

日志结构:

+-----------------+
|  Journal Superblock |
+-----------------+
|  Descriptor Block   | ← 描述要写入的块
+-----------------+
|  Data Blocks        | ← 实际数据
+-----------------+
|  Commit Block       | ← 提交标记
+-----------------+

💡 自制 OS 初期无需实现日志,但需注意 ext2 的脆弱性


结论:构建可靠的存储基石

ext2 是自制操作系统的理想文件系统起点
通过实现其核心组件,
我们掌握了:

  • 磁盘布局:超级块、块组、位图
  • 数据寻址:直接/间接块
  • 目录管理:变长目录项
  • 块分配:位图管理

此 ext2 驱动为后续实现 ext3 日志、ext4 extents 奠定了坚实基础。
真正的文件系统,始于对磁盘布局的深刻理解。


附录:关键常量与工具

ext2 常量

#define EXT2_SUPER_MAGIC 0xEF53
#define EXT2_ROOT_INO 2
#define EXT2_GOOD_OLD_FIRST_INO 11

// 文件类型
#define S_IFDIR  0x4000
#define S_IFREG  0x8000
#define S_IFLNK  0xA000

// 目录项类型
#define EXT2_FT_DIR 2
#define EXT2_FT_REG_FILE 1

实用工具

  • mkfs.ext2:创建 ext2 镜像
    dd if=/dev/zero of=disk.img bs=1M count=32
    mkfs.ext2 -F disk.img
    
  • debugfs:调试 ext2 文件系统
    debugfs disk.img
    debugfs> ls
    debugfs> stat <2>  # 查看根 inode
    

:本文所有代码均为简化实现,实际使用需添加错误处理、边界检查、缓存等。