存储系统-ext4 篇:为自制 OS 设计现代文件系统

"ext2 虽经典,但面对大文件、高并发时力不从心。
ext4 通过 Extents、延迟分配等特性,解决了碎片与性能问题。
本文将解析 ext4 核心机制,并对比 XFS/Btrfs/ZFS 的设计哲学,
为自制 OS 提供现代文件系统设计思路。"

引言:ext2 的局限与 ext4 的演进

ext2 作为经典文件系统,存在明显局限:

  • 碎片问题:小文件分散存储,大文件间接块遍历慢
  • 扩展性差:最大 2TB 文件,inode 静态分配
  • 无日志:断电易损坏,fsck 慢
  • 性能瓶颈:块分配无优化,写放大严重

ext4(Fourth Extended Filesystem)作为 ext2/ext3 的演进,
通过增量改进解决了这些问题,同时保持兼容性:

  • Extents:替代间接块,高效管理大文件
  • 延迟分配(Delayed Allocation):减少碎片
  • 多块分配(Multiblock Allocation):提升写性能
  • 日志校验和:提高可靠性

本文将为自制 OS 设计一个简化但现代的 ext4 驱动,
并对比其他现代文件系统的设计哲学。


第一章:ext4 核心特性解析

1.1 Extents:高效数据块管理

ext2 间接块的痛点:

  • 12 直接块 + 3 级间接 → 大文件需多次磁盘 I/O
  • 碎片严重:文件块分散在不同区域

Extents 设计:

  • 连续块范围:用 (起始块, 长度) 替代单个块指针
  • Extent 树:支持大文件高效寻址

ext4 inode 的 Extents 结构:

// ext4.h
struct ext4_extent {
    uint32_t ee_block;    // 逻辑块号(文件内偏移)
    uint16_t ee_len;      // 块数
    uint16_t ee_start_hi; // 物理块高 16 位
    uint32_t ee_start_lo; // 物理块低 32 位
} __attribute__((packed));

struct ext4_extent_header {
    uint16_t eh_magic;    // 0xF30A
    uint16_t eh_entries;  // 当前项数
    uint16_t eh_max;      // 最大项数
    uint16_t eh_depth;    // 树深度(0=叶子)
    uint32_t eh_generation; // 生成号
} __attribute__((packed));

struct ext4_extent_idx {
    uint32_t ei_block;    // 逻辑块号
    uint32_t ei_leaf_lo;  // 叶子节点低 32 位
    uint16_t ei_leaf_hi;  // 叶子节点高 16 位
    uint16_t ei_unused;
} __attribute__((packed));

// inode 中的 i_block 字段重用
struct ext4_inode {
    // ... 其他字段
    union {
        struct ext4_extent_header h;
        struct ext4_extent extents[4]; // 叶子节点
        struct ext4_extent_idx indexes[4]; // 索引节点
    } i_block[15];
};

Extents 寻址流程:

  1. 小文件(≤4 个 extent):直接存储在 inode
  2. 大文件:inode 存索引节点 → 叶子节点存 extent
  3. 查找:二分搜索 extent 数组

1.2 延迟分配(Delayed Allocation)

问题:

  • ext2 立即分配:写入时立即分配块 → 碎片
  • 小文件写放大:每次写都分配新块

解决方案:

  • 写入时仅缓存数据,不分配磁盘块
  • 文件关闭/同步时,才分配连续块

优势:

  • 减少碎片:大块连续分配
  • 减少元数据写:多个写合并为一次分配
  • 提升性能:避免小文件写放大

1.3 多块分配(Multiblock Allocation)

问题:

  • ext2 单块分配:每次分配一个块
  • 磁头移动多:块分散导致性能下降

解决方案:

  • 预分配连续块:一次分配多个块
  • 块分配器优化:寻找最大连续空闲区域

实现:

// ext4.c(简化)
static uint32_t ext4_new_blocks(struct ext4_sb_info *sb, 
                                uint32_t goal, 
                                int count) {
    // 1. 从 goal 附近查找连续 count 个空闲块
    // 2. 更新块位图和组描述符
    // 3. 返回起始块号
    return start_block;
}

第二章:ext4 磁盘布局改进

2.1 灵活的 inode 大小

ext2 限制:

  • 固定 128 字节 inode:无法扩展

ext4 改进:

  • 可变 inode 大小(128-4096 字节)
  • 扩展属性(xattr):存储额外元数据
// 超级块字段
uint16_t s_inode_size;    // inode 大小
uint16_t s_first_ino;     // 第一个非保留 inode

2.2 区(Block Group)改进

flex_bg 特性:

  • 多个块组组成 flex_bg
  • 元数据集中存储:超级块、GDT 在 flex_bg 开头
  • 提升大文件性能:数据块连续分配

无序 inode 分配:

  • inode 分配不严格按组:减少热点

2.3 校验和与元数据保护

元数据校验和:

  • 超级块、GDT、inode 表 添加 CRC32 校验
  • 提前检测损坏:避免静默数据损坏
// 超级块扩展字段
uint32_t s_checksum;      // 超级块校验和
uint32_t s_checksum_type; // 校验类型

第三章:日志(Journaling)机制

3.1 日志基础

日志结构:

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

日志工作流程:

  1. 开始事务:写入 descriptor
  2. 写入数据:到日志或直接到文件系统
  3. 提交事务:写入 commit 块
  4. 检查点:后台将日志数据写入文件系统

3.2 三种日志模式

模式 元数据 数据 安全性 性能
journal 日志 日志 最高 最慢
ordered 日志 先文件系统后日志 高(默认)
writeback 日志 无顺序 最快

ordered 模式(推荐):

  • 数据先写入文件系统
  • 元数据写入日志并提交
  • 保证数据一致性:避免元数据指向未写入数据

3.3 日志实现要点

事务管理:

// journal.h
struct journal_handle {
    int h_transaction_id;
    int h_buffer_credits; // 缓冲区配额
};

struct journal_handle *journal_start(struct journal *journal, int nblocks);
int journal_stop(struct journal_handle *handle);

日志写入:

// journal.c
static int journal_write_metadata(struct journal_handle *handle, 
                                 struct buffer_head *bh) {
    // 1. 将 buffer_head 加入事务
    // 2. 写入日志描述符
    // 3. 写入数据块(journal 模式)
    return 0;
}

第四章:ext4 与 VFS 集成

4.1 Extents 寻址实现

// ext4.c
static uint32_t ext4_find_block(struct ext4_inode *inode, uint32_t block) {
    struct ext4_extent_header *eh = (void*)inode->i_block;
    
    if (eh->eh_depth == 0) {
        // 叶子节点:二分搜索
        struct ext4_extent *extents = (void*)(eh + 1);
        for (int i = 0; i < eh->eh_entries; i++) {
            if (block >= extents[i].ee_block && 
                block < extents[i].ee_block + extents[i].ee_len) {
                return extents[i].ee_start_lo + (block - extents[i].ee_block);
            }
        }
    } else {
        // 索引节点:递归查找
        struct ext4_extent_idx *indexes = (void*)(eh + 1);
        // ... 类似实现
    }
    return 0;
}

4.2 延迟分配写入

// ext4_file.c
static ssize_t ext4_file_write(struct vfs_file *file, 
                              const char *buf, 
                              size_t count) {
    // 1. 将数据写入 page cache(不分配磁盘块)
    // 2. 标记页为 dirty
    // 3. 返回(实际分配在 fsync/close 时)
    return count;
}

static int ext4_file_fsync(struct vfs_file *file) {
    // 1. 为 dirty 页分配磁盘块(延迟分配)
    // 2. 更新 inode
    // 3. 提交日志事务
    return 0;
}

第五章:现代文件系统设计哲学对比

5.1 XFS:高性能日志文件系统

核心思想:

  • 分配组(Allocation Group):并行分配
  • B+ 树:高效管理 inode 和 extent
  • 日志优化:异步提交,高吞吐

适用场景:

  • 大文件:媒体、数据库
  • 高并发:多线程写入

5.2 Btrfs:写时复制(CoW)文件系统

核心思想:

  • CoW:修改时复制,保证一致性
  • 子卷(Subvolume):快照、克隆
  • RAID 集成:内置 RAID 0/1/10

优势:

  • 快照:秒级创建
  • 数据校验:checksum 防静默损坏
  • 在线碎片整理

挑战:

  • 复杂度高:CoW 导致写放大
  • 碎片问题:长期运行后性能下降

5.3 ZFS:终极企业级文件系统

核心思想:

  • 池化存储(ZPool):整合多设备
  • 端到端校验:从应用到磁盘
  • ARC 缓存:自适应替换缓存

优势:

  • 数据完整性:checksum + 自愈
  • 快照/克隆:高效 CoW
  • 压缩/去重:节省空间

挑战:

  • 内存需求高:ARC 需大量 RAM
  • 许可证问题:CDDL 与 GPL 不兼容

5.4 对比总结

特性 ext4 XFS Btrfs ZFS
日志 CoW CoW
快照 有(LVM) 原生 原生
校验和 元数据
RAID 外部 外部 内置 内置
适用场景 通用 大文件 桌面/服务器 企业存储

💡 自制 OS 建议:从 ext4 开始,逐步引入 CoW、校验和等特性


第六章:FUSE:用户态文件系统框架

6.1 FUSE 核心思想

问题:

  • 内核文件系统开发复杂:需处理 VFS、缓存、锁
  • 调试困难:内核崩溃难调试

解决方案:

  • 用户态文件系统:通过 FUSE 内核模块与 VFS 交互
  • 标准 API:实现 open/read/write 等回调

架构:

+------------------+
|   用户态文件系统  |  // 实现 FUSE API
+------------------+
|   libfuse        |  // 用户库
+------------------+
|   fuse.ko        |  // 内核模块(VFS 适配)
+------------------+
|       VFS        |
+------------------+

6.2 FUSE for 自制 OS

内核 FUSE 模块:

// fuse_vfs.c
static ssize_t fuse_read(struct vfs_file *file, char *buf, size_t count) {
    // 1. 构造 FUSE 读请求
    struct fuse_read_in in = {
        .fh = file->f_private,
        .offset = file->f_pos,
        .size = count,
    };
    
    // 2. 发送请求到用户态
    struct fuse_out_header out;
    fuse_send_request(FUSE_READ, &in, sizeof(in), buf, count, &out);
    
    return out.len;
}

用户态文件系统示例:

// user/myfs.c
static int myfs_read(const char *path, char *buf, size_t size, 
                     off_t offset, struct fuse_file_info *fi) {
    // 1. 查找文件
    // 2. 从内存/网络读取数据
    // 3. 复制到 buf
    return actual_size;
}

static struct fuse_operations myfs_ops = {
    .read = myfs_read,
    .write = myfs_write,
    // ...
};

int main() {
    return fuse_main(0, NULL, &myfs_ops, NULL);
}

优势:

  • 快速开发:用 C/Python 实现文件系统
  • 安全隔离:用户态崩溃不影响内核
  • 灵活扩展:支持网络文件系统、加密等

结论:现代文件系统的演进之路

ext4 代表了渐进式改进的成功:
在保持 ext2 兼容性的同时,
通过 Extents、延迟分配等特性,
解决了碎片与性能问题。

而 XFS、Btrfs、ZFS 展现了不同设计哲学

  • XFS:极致性能
  • Btrfs:CoW 与快照
  • ZFS:数据完整性

对于自制 OS,建议:

  1. 从 ext4 开始:实现 Extents 和延迟分配
  2. 逐步引入特性:日志、校验和
  3. 探索 FUSE:快速实现用户态文件系统

真正的文件系统,始于对数据布局的深刻理解,
成于对性能与可靠性的平衡。


附录:ext4 关键常量

ext4 特性标志

#define EXT4_FEATURE_COMPAT_EXTENTS 0x0040
#define EXT4_FEATURE_COMPAT_FLEX_BG 0x0200
#define EXT4_FEATURE_RO_COMPAT_METADATA_CSUM 0x4000
#define EXT4_FEATURE_INCOMPAT_EXTENTS 0x0040
#define EXT4_FEATURE_INCOMPAT_FLEX_BG 0x0200

日志常量

#define JFS_MAGIC 0xC03B3998
#define JFS_DESCRIPTOR_BLOCK 1
#define JFS_COMMIT_BLOCK 2

实用工具

  • mkfs.ext4:创建 ext4 镜像
    mkfs.ext4 -F disk.img
    
  • tune2fs:调整 ext4 参数
    tune2fs -O extents disk.img  # 启用 extents
    

:本文所有代码均为简化实现,实际使用需添加错误处理、缓存、并发控制等。