存储系统-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 寻址流程:
- 小文件(≤4 个 extent):直接存储在 inode
- 大文件:inode 存索引节点 → 叶子节点存 extent
- 查找:二分搜索 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 | ← 提交标记
+-----------------+
日志工作流程:
- 开始事务:写入 descriptor
- 写入数据:到日志或直接到文件系统
- 提交事务:写入 commit 块
- 检查点:后台将日志数据写入文件系统
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,建议:
- 从 ext4 开始:实现 Extents 和延迟分配
- 逐步引入特性:日志、校验和
- 探索 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
注:本文所有代码均为简化实现,实际使用需添加错误处理、缓存、并发控制等。