第6章:具体文件系统实现
章节大纲
6.1 开篇与 ext2/3/4 演进
- ext 文件系统家族历史
- ext2 基础架构:超级块、块组、inode表
- ext3 日志机制引入
- ext4 现代化改进:extent、延迟分配、多块分配
6.2 日志文件系统原理
- 日志机制设计动机
- JBD/JBD2 日志层实现
- 三种日志模式:journal、ordered、writeback
- 崩溃恢复与一致性保证
6.3 现代文件系统:Btrfs、XFS
- XFS:高性能与可扩展性
- Btrfs:写时复制与高级特性
- ZFS 影响与设计理念
- 性能特征对比
6.4 特殊文件系统
- proc:进程信息与内核接口
- sysfs:设备模型导出
- debugfs:内核调试接口
- tmpfs/ramfs:内存文件系统
6.5 本章小结
- 关键数据结构总结
- 算法复杂度分析
- 选型决策指南
6.6 练习题(8道)
- 基础题:文件系统结构理解
- 进阶题:性能分析与优化
- 挑战题:设计权衡与实现
6.7 常见陷阱与错误
- 文件系统损坏与修复
- 性能问题诊断
- 容量规划失误
6.8 最佳实践检查清单
- 文件系统选择准则
- 调优参数配置
- 监控与维护要点
6.1 ext2/3/4 文件系统演进与实现
本章深入探讨 Linux 中各种文件系统的具体实现,从经典的 ext 系列到现代的 Btrfs、XFS,再到特殊用途的 proc、sysfs 等伪文件系统。通过理解这些实现细节,我们将掌握文件系统设计的核心权衡:性能、可靠性、功能特性之间的平衡。每种文件系统都是特定需求下的产物,理解它们的设计决策对于系统架构师至关重要。
6.1.1 ext 文件系统家族历史
ext(extended filesystem)家族是 Linux 最重要的文件系统演进脉络。从 1992 年 Rémy Card 开发的 ext 开始,历经 ext2(1993)、ext3(2001)、ext4(2008),每一代都在前代基础上解决关键问题:
时间线:
1992: ext - 替代 MINIX FS,支持 2GB 文件
1993: ext2 - 成为 Linux 标准文件系统,引入块组概念
2001: ext3 - 增加日志功能,提供崩溃一致性
2008: ext4 - 支持更大容量,引入 extent、延迟分配等现代特性
历史故事:ext2 的设计深受 BSD FFS(Fast File System)影响,特别是块组(block group)概念。Theodore Ts'o 在 1994 年接手维护后,ext2 成为 Linux 事实标准。而 ext3 的开发则是由 Stephen Tweedie 主导,他提出的 JBD(Journaling Block Device)层成为 Linux 日志文件系统的基础。
6.1.2 ext2 基础架构
ext2 确立了 ext 家族的基本布局,理解它是掌握后续版本的关键:
磁盘布局
+------------------+
| 引导块 (1KB) | - 保留给引导加载器
+------------------+
| 块组 0 | ┐
| 超级块 | │
| 组描述符表 | │
| 数据块位图 | │ 块组结构
| inode 位图 | │ (重复 N 次)
| inode 表 | │
| 数据块 | │
+------------------+ ┘
| 块组 1 |
| ... |
+------------------+
| ... |
+------------------+
| 块组 N-1 |
+------------------+
核心数据结构
超级块(Super Block):
struct ext2_super_block {
__le32 s_inodes_count; /* inode 总数 */
__le32 s_blocks_count; /* 块总数 */
__le32 s_r_blocks_count; /* 保留块数 */
__le32 s_free_blocks_count; /* 空闲块数 */
__le32 s_free_inodes_count; /* 空闲 inode 数 */
__le32 s_first_data_block; /* 第一个数据块号 */
__le32 s_log_block_size; /* 块大小 = 1024 << s_log_block_size */
__le32 s_blocks_per_group; /* 每组块数 */
__le32 s_inodes_per_group; /* 每组 inode 数 */
__le32 s_mtime; /* 挂载时间 */
__le32 s_wtime; /* 写入时间 */
__le16 s_magic; /* 魔数 0xEF53 */
__le16 s_state; /* 文件系统状态 */
/* ... 更多字段 ... */
};
inode 结构:
struct ext2_inode {
__le16 i_mode; /* 文件模式 */
__le16 i_uid; /* 用户 ID */
__le32 i_size; /* 文件大小(字节)*/
__le32 i_atime; /* 访问时间 */
__le32 i_ctime; /* 创建时间 */
__le32 i_mtime; /* 修改时间 */
__le32 i_dtime; /* 删除时间 */
__le16 i_gid; /* 组 ID */
__le16 i_links_count; /* 硬链接数 */
__le32 i_blocks; /* 512 字节块数 */
__le32 i_block[15]; /* 数据块指针 */
/* i_block[0-11]: 直接块
i_block[12]: 一级间接块
i_block[13]: 二级间接块
i_block[14]: 三级间接块 */
};
块组设计哲学
块组(Block Group)是 ext2 的核心创新,其设计目标是:
- 局部性优化:相关数据放在同一块组,减少磁头移动
- 并行化:不同块组可以并行操作
- 故障隔离:单个块组损坏不影响整个文件系统
块分配算法遵循以下策略:
- 目录:在父目录所在块组或负载最轻的块组创建
- 文件:优先在父目录所在块组分配
- 大文件:跨块组分配时保持连续性
6.1.3 ext3 日志机制引入
ext3 最重要的贡献是引入日志,解决了 ext2 的崩溃一致性问题:
JBD 层架构
应用程序
↓
VFS 层接口
↓
ext3 文件系统
↓
JBD 日志层 ← 事务管理
↓
块设备层
日志事务流程
事务生命周期:
T_RUNNING → T_LOCKED → T_FLUSH → T_COMMIT → T_FINISHED
1. T_RUNNING:收集修改操作
2. T_LOCKED:停止接受新操作
3. T_FLUSH:写入日志区
4. T_COMMIT:提交事务
5. T_FINISHED:可以回收日志空间
三种日志模式对比
| 模式 | 日志内容 | 性能 | 一致性保证 |
| 模式 | 日志内容 | 性能 | 一致性保证 |
|---|---|---|---|
| journal | 元数据+数据 | 最慢 | 最强 |
| ordered | 仅元数据,数据先写 | 中等 | 较强 |
| writeback | 仅元数据 | 最快 | 仅元数据一致 |
默认使用 ordered 模式,平衡了性能和一致性。
6.1.4 ext4 现代化改进
ext4 在 2008 年成为默认文件系统,引入多项关键改进:
Extent 树替代块映射
传统块映射的问题:
- 大文件需要多级间接块
- 碎片化严重时性能下降
- 元数据开销大
Extent 解决方案:
struct ext4_extent {
__le32 ee_block; /* 逻辑块号 */
__le16 ee_len; /* extent 长度 */
__le16 ee_start_hi; /* 物理块号高 16 位 */
__le32 ee_start_lo; /* 物理块号低 32 位 */
};
/* Extent 树示例:
一个 100MB 连续文件只需要一个 extent 记录
而块映射需要 25600 个块指针 */
延迟分配(Delayed Allocation)
传统分配:
write() → 立即分配块 → 写入页缓存 → 后台刷新
延迟分配:
write() → 写入页缓存 → 后台刷新时分配块
优势:
1. 更好的块分配决策(知道实际大小)
2. 减少碎片
3. 避免短生命文件的块分配
4. 提高小文件写入性能
多块分配(Multiblock Allocation)
使用 mballoc(Multiblock Allocator):
- 伙伴系统:管理空闲块
- 预分配:为文件预留连续空间
- 局部性组:相关文件放在一起
/* 预分配策略 */
struct ext4_prealloc_space {
struct list_head pa_inode_list; /* inode 预分配链表 */
struct list_head pa_group_list; /* 块组预分配链表 */
union {
struct rb_node pa_node; /* 用于 inode 预分配 */
struct list_head pa_tmp_list;/* 临时链表 */
} u;
spinlock_t pa_lock;
atomic_t pa_count;
unsigned pa_deleted;
ext4_fsblk_t pa_pstart; /* 物理起始块 */
ext4_lblk_t pa_lstart; /* 逻辑起始块 */
ext4_grpblk_t pa_len; /* 长度 */
ext4_grpblk_t pa_free; /* 剩余空闲块 */
};
其他重要特性
-
更大的文件系统支持: - 最大卷:1 EB(ext3:32 TB) - 最大文件:16 TB(ext3:2 TB)
-
快速 fsck: - 未使用的 inode 表部分不检查 - inode 表初始化可延迟到使用时
-
纳秒级时间戳: - 支持纳秒精度 - 额外的创建时间字段
-
持久预分配: - fallocate() 系统调用支持 - 数据库等应用可预留空间
6.1.5 ext4 内核实现分析
让我们深入内核代码,理解 ext4 的关键操作:
inode 操作实现
/* fs/ext4/inode.c */
const struct inode_operations ext4_file_inode_operations = {
.setattr = ext4_setattr,
.getattr = ext4_getattr,
.listxattr = ext4_listxattr,
.get_acl = ext4_get_acl,
.set_acl = ext4_set_acl,
.fiemap = ext4_fiemap,
};
/* 文件写入路径 */
static int ext4_write_begin(struct file *file,
struct address_space *mapping,
loff_t pos, unsigned len, unsigned flags,
struct page **pagep, void **fsdata)
{
struct inode *inode = mapping->host;
int ret, needed_blocks;
/* 1. 计算需要的块数 */
needed_blocks = ext4_writepage_trans_blocks(inode);
/* 2. 开始日志事务 */
handle = ext4_journal_start(inode, EXT4_HT_WRITE_PAGE, needed_blocks);
/* 3. 准备写入页 */
if (ext4_should_dioread_nolock(inode))
ret = ext4_write_begin_inline(mapping, inode, pos, len,
flags, pagep);
else
ret = __block_write_begin(page, pos, len, ext4_get_block);
/* 4. 延迟分配处理 */
if (ret == 0 && ext4_should_journal_data(inode))
ret = ext4_walk_page_buffers(handle, page_buffers(page),
from, to, NULL,
do_journal_get_write_access);
return ret;
}
Extent 操作
/* fs/ext4/extents.c */
int ext4_ext_map_blocks(handle_t *handle, struct inode *inode,
struct ext4_map_blocks *map, int flags)
{
struct ext4_ext_path *path = NULL;
struct ext4_extent newex, *ex;
/* 1. 查找 extent */
path = ext4_find_extent(inode, map->m_lblk, NULL, 0);
/* 2. 检查是否命中缓存 */
if ((ex = path[depth].p_ext)) {
ext4_lblk_t ee_block = le32_to_cpu(ex->ee_block);
ext4_fsblk_t ee_start = ext4_ext_pblock(ex);
unsigned short ee_len = ext4_ext_get_actual_len(ex);
if (in_range(map->m_lblk, ee_block, ee_len)) {
/* 命中:直接映射 */
map->m_pblk = ee_start + map->m_lblk - ee_block;
map->m_len = ee_len - (map->m_lblk - ee_block);
goto out;
}
}
/* 3. 未命中:分配新块 */
if (flags & EXT4_GET_BLOCKS_CREATE) {
newex.ee_block = cpu_to_le32(map->m_lblk);
cluster = ext4_ext_map_clusters(inode, map, flags);
/* 4. 插入新 extent */
err = ext4_ext_insert_extent(handle, inode, &path, &newex, flags);
}
out:
ext4_ext_drop_refs(path);
return err;
}
6.2 日志文件系统原理
日志文件系统是现代文件系统的核心特性,它解决了传统文件系统最大的痛点:崩溃一致性。在 ext2 时代,系统崩溃后的 fsck 可能需要数小时,而日志机制将恢复时间缩短到秒级。本节深入剖析日志机制的设计原理和实现细节。
6.2.1 为什么需要日志
崩溃一致性问题
考虑一个简单的文件创建操作,涉及多个元数据更新:
1. 分配 inode,更新 inode 位图
2. 初始化 inode 结构
3. 在父目录中创建目录项
4. 更新父目录的修改时间
5. 更新超级块的计数器
如果在步骤 3 之后崩溃,会出现:
- inode 已分配但无法访问(孤儿 inode)
- 空间泄漏
- 文件系统不一致
传统解决方案的问题
同步元数据更新:
- 每次操作都同步写磁盘
- 性能极差(10-100 倍慢)
- 仍无法保证原子性
软更新(Soft Updates):
- BSD 的解决方案
- 复杂的依赖跟踪
- 实现困难,仍需要后台 fsck
6.2.2 日志机制核心概念
Write-Ahead Logging (WAL)
日志的核心思想来自数据库的 WAL:
原理:先写日志,后写数据
1. 将要执行的操作写入日志
2. 确保日志落盘
3. 执行实际的数据/元数据更新
4. 标记日志项完成
事务(Transaction)
将相关的修改组织成事务,保证原子性:
/* JBD2 事务结构 */
struct transaction_s {
journal_t *t_journal; /* 所属日志 */
tid_t t_tid; /* 事务 ID */
enum {
T_RUNNING, /* 接受新的修改 */
T_LOCKED, /* 不再接受修改,准备提交 */
T_FLUSH, /* 正在写入日志 */
T_COMMIT, /* 提交中 */
T_COMMIT_DFLUSH, /* 提交后刷盘 */
T_COMMIT_JFLUSH, /* 日志刷盘 */
T_FINISHED /* 完成,可回收 */
} t_state;
struct journal_head *t_buffers; /* 缓冲区链表 */
struct journal_head *t_forget; /* 需要忘记的缓冲区 */
struct journal_head *t_checkpoint_list;/* 检查点链表 */
unsigned long t_expires; /* 超时时间 */
ktime_t t_start_time; /* 开始时间 */
atomic_t t_updates; /* 活跃更新数 */
atomic_t t_outstanding_credits; /* 使用的日志空间 */
};
6.2.3 JBD2 架构详解
JBD2(Journaling Block Device 2)是 ext4 和 OCFS2 使用的日志层:
日志布局
日志区域(循环缓冲区):
+------------+------------+------------+------------+
| 超级块 | 描述符块 | 数据/元数据 | 提交块 |
+------------+------------+------------+------------+
↑ ↑
head tail
(最旧的未检查点事务) (最新提交位置)
日志超级块
/* 日志超级块 */
typedef struct journal_superblock_s {
/* 静态信息 */
journal_header_t s_header;
__be32 s_blocksize; /* 日志块大小 */
__be32 s_maxlen; /* 日志区总块数 */
__be32 s_first; /* 第一个日志块 */
/* 动态信息 */
__be32 s_sequence; /* 第一个提交 ID */
__be32 s_start; /* 第一个日志块的块号 */
__be32 s_errno; /* 错误码 */
/* 特性标志 */
__be32 s_feature_compat; /* 兼容特性 */
__be32 s_feature_incompat; /* 不兼容特性 */
__be32 s_feature_ro_compat; /* 只读兼容特性 */
/* 校验和 */
__be32 s_checksum; /* CRC32C 校验和 */
} journal_superblock_t;
日志记录类型
/* 日志块标签 */
typedef struct journal_block_tag_s {
__be32 t_blocknr; /* 磁盘块号 */
__be16 t_checksum; /* 块校验和 */
__be16 t_flags; /* 标志 */
__be32 t_blocknr_high; /* 块号高 32 位 */
} journal_block_tag_t;
#define JBD2_FLAG_ESCAPE 1 /* 块内容需要转义 */
#define JBD2_FLAG_SAME_UUID 2 /* UUID 相同 */
#define JBD2_FLAG_DELETED 4 /* 块已删除 */
#define JBD2_FLAG_LAST_TAG 8 /* 最后一个标签 */
6.2.4 三种日志模式深度分析
Journal 模式(数据日志)
操作流程:
1. 元数据 + 数据 → 日志
2. 日志落盘
3. 元数据 + 数据 → 最终位置
4. 提交事务
优点:
- 最强的一致性保证
- 数据和元数据都不会丢失
缺点:
- 所有数据写两次(日志 + 最终位置)
- 性能开销大
- 日志空间需求高
适用场景:
- 关键数据,不容许任何丢失
- 小文件为主的工作负载
Ordered 模式(默认)
操作流程:
1. 数据 → 最终位置
2. 数据落盘
3. 元数据 → 日志
4. 日志落盘
5. 元数据 → 最终位置
6. 提交事务
优点:
- 平衡性能和一致性
- 防止垃圾数据暴露
- 日志空间需求小
缺点:
- 数据写入成为瓶颈
- 大文件写入延迟事务提交
实现要点:
- 使用 JBD2_FEATURE_INCOMPAT_ASYNC_COMMIT
- 数据块写入使用 bio 批量提交
Writeback 模式
操作流程:
1. 元数据 → 日志
2. 日志落盘
3. 元数据 → 最终位置
4. 数据异步写入(不保证顺序)
优点:
- 最佳性能
- 最小日志开销
缺点:
- 可能暴露垃圾数据
- 崩溃后文件内容不确定
适用场景:
- 临时文件
- 可重新生成的数据
- 性能优先的场景
6.2.5 日志恢复机制
恢复流程
/* fs/jbd2/recovery.c */
int jbd2_journal_recover(journal_t *journal)
{
int err, err2;
journal_superblock_t *sb;
struct recovery_info info;
/* 1. 读取日志超级块 */
sb = journal->j_superblock;
/* 2. 扫描日志,找到有效事务范围 */
err = do_one_pass(journal, &info, PASS_SCAN);
/* 3. 重放日志(REDO) */
if (!err)
err = do_one_pass(journal, &info, PASS_REVOKE);
if (!err)
err = do_one_pass(journal, &info, PASS_REPLAY);
/* 4. 清理日志区域 */
journal->j_transaction_sequence = info.end_transaction;
jbd2_journal_clear_revoke(journal);
/* 5. 同步文件系统 */
sync_blockdev(journal->j_fs_dev);
return err;
}
检查点机制
检查点(Checkpoint)将内存中的脏数据写入磁盘,释放日志空间:
/* 检查点处理 */
int jbd2_log_do_checkpoint(journal_t *journal)
{
transaction_t *transaction;
struct journal_head *jh;
struct buffer_head *bh;
int result;
/* 获取最旧的未检查点事务 */
transaction = journal->j_checkpoint_transactions;
while (transaction) {
jh = transaction->t_checkpoint_list;
while (jh) {
bh = jh2bh(jh);
/* 写入脏缓冲区 */
if (buffer_dirty(bh)) {
get_bh(bh);
spin_unlock(&journal->j_list_lock);
ll_rw_block(REQ_OP_WRITE, 1, &bh);
put_bh(bh);
spin_lock(&journal->j_list_lock);
}
/* 等待 I/O 完成 */
if (buffer_locked(bh)) {
get_bh(bh);
spin_unlock(&journal->j_list_lock);
wait_on_buffer(bh);
put_bh(bh);
spin_lock(&journal->j_list_lock);
}
/* 从检查点链表移除 */
__jbd2_journal_remove_checkpoint(jh);
jh = next_jh;
}
/* 移动到下一个事务 */
transaction = transaction->t_cpnext;
}
return result;
}
6.2.6 性能优化技术
批量提交(Batching)
/* 批量提交优化 */
void jbd2_journal_commit_transaction(journal_t *journal)
{
transaction_t *commit_transaction;
struct buffer_head **wbuf;
int bufs = 0;
/* 收集所有需要写入的缓冲区 */
wbuf = kmalloc(sizeof(*wbuf) * MAX_WRITEBACK_PAGES, GFP_NOFS);
list_for_each_entry(jh, &commit_transaction->t_buffers, b_tnext) {
wbuf[bufs++] = jh2bh(jh);
if (bufs == MAX_WRITEBACK_PAGES) {
/* 批量提交 */
ll_rw_block(REQ_OP_WRITE | REQ_SYNC, bufs, wbuf);
bufs = 0;
}
}
/* 提交剩余的缓冲区 */
if (bufs)
ll_rw_block(REQ_OP_WRITE | REQ_SYNC, bufs, wbuf);
}
并行日志写入
JBD2 支持多个 CPU 并行准备日志记录:
/* 并行日志准备 */
handle_t *jbd2__journal_start(journal_t *journal, int nblocks,
unsigned int type, int line)
{
handle_t *handle;
transaction_t *transaction;
/* 每个 CPU 独立的 handle */
handle = new_handle(nblocks);
/* 获取当前运行事务 */
read_lock(&journal->j_state_lock);
transaction = journal->j_running_transaction;
/* 原子地增加引用计数 */
atomic_inc(&transaction->t_updates);
/* 预留日志空间 */
atomic_sub(nblocks, &transaction->t_outstanding_credits);
read_unlock(&journal->j_state_lock);
handle->h_transaction = transaction;
return handle;
}
快速提交(Fast Commit)
ext4 引入的快速提交机制,用于加速 fsync:
/* 快速提交实现 */
int ext4_fc_commit(journal_t *journal, tid_t tid)
{
struct ext4_fc_commit_info *fc_info;
/* 1. 检查是否可以快速提交 */
if (!ext4_fc_can_commit(journal))
return jbd2_complete_transaction(journal, tid);
/* 2. 准备快速提交信息 */
fc_info = ext4_fc_prepare(journal);
/* 3. 写入快速提交日志 */
ext4_fc_write_inode(fc_info);
ext4_fc_write_data(fc_info);
/* 4. 提交快速日志 */
ext4_fc_submit(fc_info);
return 0;
}
6.2.7 日志调优参数
关键参数配置
# 调整日志大小(ext4)
tune2fs -J size=256 /dev/sda1
# 调整提交间隔
mount -o commit=5 /dev/sda1 /mnt # 5秒提交一次
# 禁用屏障(谨慎使用)
mount -o nobarrier /dev/sda1 /mnt
# 设置日志模式
mount -o data=writeback /dev/sda1 /mnt
性能监控
/* 通过 /proc/fs/jbd2/ 监控日志性能 */
struct jbd2_stats_proc_session {
journal_t *journal;
struct transaction_stats_s *stats;
int start; /* 统计开始时间 */
int max; /* 最大事务数 */
};
/* 关键指标:
- 平均事务大小
- 提交延迟
- 检查点频率
- 日志利用率 */
6.3 现代文件系统:XFS、Btrfs
现代文件系统为了应对大规模存储、高并发访问、数据完整性等挑战,引入了许多创新设计。XFS 代表了高性能和可扩展性的极致追求,而 Btrfs 则展示了功能丰富性和灵活性的方向。理解它们的设计哲学和实现机制,对于架构大规模存储系统至关重要。
6.3.1 XFS:高性能与可扩展性
XFS 由 SGI 在 1993 年为 IRIX 开发,2001 年移植到 Linux。它的设计目标是处理超大文件和高并发访问。
架构特点
分配组(Allocation Groups):
XFS 文件系统布局:
+----------------+----------------+----------------+
| AG0 | AG1 | AG2 | ...
+----------------+----------------+----------------+
每个 AG 包含:
- 超级块副本
- 空闲空间 B+ 树
- inode B+ 树
- 自由 inode 列表
- AG 内部日志(可选)
优势:
1. 并行操作:不同 AG 可以独立操作
2. 可扩展性:AG 数量可达数千个
3. 局部性:相关数据在同一 AG
B+ 树无处不在:
/* XFS 使用 B+ 树管理几乎所有元数据 */
typedef struct xfs_btree_block {
__be32 bb_magic; /* 魔数标识 */
__be16 bb_level; /* 树的层级 */
__be16 bb_numrecs; /* 记录数 */
union {
struct {
__be32 bb_leftsib; /* 左兄弟 */
__be32 bb_rightsib; /* 右兄弟 */
} s;
struct {
__be64 bb_leftsib;
__be64 bb_rightsib;
} l;
} bb_u;
} xfs_btree_block_t;
/* B+ 树类型:
1. 空间管理:BNO(按块号)、CNT(按大小)
2. inode 管理:INO(inode 分配)、FINO(空闲 inode)
3. 目录:DIR2(大目录)
4. 扩展属性:ATTR
5. 反向映射:RMAP(块到文件映射)
6. 引用计数:REFC(共享块)*/
延迟分配(Delayed Allocation)
XFS 的延迟分配比 ext4 更激进:
/* 延迟分配实现 */
STATIC int
xfs_vm_writepage(
struct page *page,
struct writeback_control *wbc)
{
struct inode *inode = page->mapping->host;
struct xfs_inode *ip = XFS_I(inode);
struct buffer_head *bh, *head;
xfs_iomap_t iomap;
/* 1. 检查是否有延迟分配的块 */
if (buffer_delay(bh)) {
/* 2. 转换延迟分配为真实分配 */
error = xfs_iomap_write_allocate(ip, offset, &iomap);
/* 3. 优化:尝试预分配更多空间 */
if (!(iomap.iomap_flags & IOMAP_F_SHARED))
xfs_iomap_prealloc(ip, offset, count);
}
/* 4. 提交 I/O */
return xfs_submit_ioend(wbc, ioend, ret);
}
扩展属性与访问控制列表
XFS 对扩展属性的支持非常完善:
/* 扩展属性存储格式 */
typedef struct xfs_attr_leaf_entry {
__be32 hashval; /* 名称哈希值 */
__be16 nameidx; /* 名称偏移 */
__u8 flags; /* 标志 */
__u8 pad2; /* 填充 */
} xfs_attr_leaf_entry_t;
/* 属性类型 */
#define XFS_ATTR_LOCAL 0x01 /* 属性值存储在 inode */
#define XFS_ATTR_ROOT 0x02 /* 可信属性 */
#define XFS_ATTR_SECURE 0x08 /* 安全属性 */
#define XFS_ATTR_PARENT 0x10 /* 父目录指针 */
实时子卷(Realtime Subvolume)
XFS 独特的实时子卷设计,用于流媒体等场景:
/* 实时分配 */
int xfs_rtallocate_extent(
xfs_trans_t *tp,
xfs_rtblock_t bno, /* 起始块号 */
xfs_extlen_t minlen, /* 最小长度 */
xfs_extlen_t maxlen, /* 最大长度 */
xfs_extlen_t *len, /* 实际分配长度 */
xfs_rtblock_t *rtblock) /* 分配的块号 */
{
/* 实时子卷特点:
1. 固定大小 extent(通常 64KB-1MB)
2. 无元数据开销
3. 适合大文件顺序 I/O
4. 可以使用不同的块设备 */
}
6.3.2 Btrfs:写时复制与高级特性
Btrfs(B-tree FS)由 Oracle 的 Chris Mason 在 2007 年开始开发,目标是提供企业级功能同时保持性能。
核心设计理念
Copy-on-Write (CoW) Everything:
传统文件系统更新:
[Block A] → 原地修改 → [Block A']
Btrfs CoW 更新:
[Block A] → 复制 → [Block A']
↓ ↓
保留旧版本 新版本
优势:
1. 原子更新
2. 快照零成本
3. 数据完整性
4. 简化崩溃恢复
B-tree 森林架构
/* Btrfs 使用多个 B-tree 管理不同类型数据 */
enum btrfs_tree_objectid {
BTRFS_ROOT_TREE_OBJECTID = 1, /* 根树 */
BTRFS_EXTENT_TREE_OBJECTID = 2, /* extent 树 */
BTRFS_CHUNK_TREE_OBJECTID = 3, /* chunk 树 */
BTRFS_DEV_TREE_OBJECTID = 4, /* 设备树 */
BTRFS_FS_TREE_OBJECTID = 5, /* 文件系统树 */
BTRFS_CSUM_TREE_OBJECTID = 7, /* 校验和树 */
BTRFS_QUOTA_TREE_OBJECTID = 8, /* 配额树 */
BTRFS_UUID_TREE_OBJECTID = 9, /* UUID 树 */
BTRFS_FREE_SPACE_TREE_OBJECTID = 10, /* 空闲空间树 */
};
/* B-tree 节点结构 */
struct btrfs_node {
struct btrfs_header header;
struct btrfs_key_ptr ptrs[]; /* 子节点指针数组 */
} __attribute__ ((__packed__));
/* 键结构(所有 B-tree 共用) */
struct btrfs_key {
__le64 objectid; /* 对象 ID */
__u8 type; /* 类型 */
__le64 offset; /* 偏移/其他信息 */
} __attribute__ ((__packed__));
子卷与快照
Btrfs 的子卷是独立的文件系统树:
/* 创建快照 */
static int btrfs_ioctl_snap_create(
struct file *file,
void __user *arg,
int subvol)
{
struct btrfs_root *root = BTRFS_I(dir)->root;
struct btrfs_trans_handle *trans;
struct btrfs_root_item *root_item;
/* 1. 开始事务 */
trans = btrfs_start_transaction(root, 0);
/* 2. 复制根节点(CoW) */
ret = btrfs_copy_root(trans, root, root->node, &tmp,
objectid);
/* 3. 创建新的根项 */
root_item = &root->root_item;
btrfs_set_root_bytenr(root_item, tmp->start);
btrfs_set_root_level(root_item, btrfs_header_level(tmp));
btrfs_set_root_generation(root_item, trans->transid);
/* 4. 插入根树 */
ret = btrfs_insert_root(trans, tree_root, &key, root_item);
/* 快照创建完成,几乎零成本! */
}
数据校验和
每个数据块都有校验和:
/* 校验和计算 */
static int btrfs_csum_one_bio(struct btrfs_io_bio *io_bio)
{
struct bio *bio = &io_bio->bio;
struct btrfs_ordered_extent *ordered;
char *data;
u32 csum;
bio_for_each_segment(bvec, bio, iter) {
data = kmap_atomic(bvec.bv_page);
/* CRC32C 校验和 */
csum = btrfs_crc32c(~0, data + bvec.bv_offset,
bvec.bv_len);
/* 存储到校验和树 */
ret = btrfs_csum_file_blocks(trans, csum_root,
ordered->file_offset,
csum);
kunmap_atomic(data);
}
}
RAID 与数据冗余
Btrfs 内置 RAID 支持:
/* RAID 级别 */
enum btrfs_raid_types {
BTRFS_RAID_SINGLE = 0,
BTRFS_RAID_RAID0,
BTRFS_RAID_RAID1,
BTRFS_RAID_DUP, /* 单设备双副本 */
BTRFS_RAID_RAID10,
BTRFS_RAID_RAID5,
BTRFS_RAID_RAID6,
BTRFS_RAID_RAID1C3, /* 3 副本 */
BTRFS_RAID_RAID1C4, /* 4 副本 */
};
/* 条带化写入 */
static int btrfs_map_bio_raid56(
struct btrfs_fs_info *fs_info,
struct bio *bio,
struct btrfs_io_context *bioc)
{
if (bioc->raid_map) {
/* RAID5/6 需要计算校验 */
ret = raid56_parity_write(fs_info, bio, bioc);
} else {
/* RAID0/1/10 直接映射 */
for (i = 0; i < bioc->num_stripes; i++) {
if (bioc->stripes[i].dev->bdev)
submit_stripe_bio(bioc->stripes[i]);
}
}
}
透明压缩
Btrfs 支持多种压缩算法:
/* 压缩类型 */
enum btrfs_compression_type {
BTRFS_COMPRESS_NONE = 0,
BTRFS_COMPRESS_ZLIB = 1,
BTRFS_COMPRESS_LZO = 2,
BTRFS_COMPRESS_ZSTD = 3,
};
/* 压缩实现 */
int btrfs_compress_pages(
unsigned int type_level,
struct address_space *mapping,
u64 start,
struct page **pages,
unsigned long *out_pages,
unsigned long *total_in,
unsigned long *total_out)
{
struct list_head *workspace;
int ret;
/* 获取压缩工作空间 */
workspace = get_workspace(type, level);
/* 执行压缩 */
ret = compression_funcs[type]->compress_pages(
workspace, mapping, start, pages,
out_pages, total_in, total_out);
/* 压缩比检查 */
if (*total_out >= *total_in * 90 / 100) {
/* 压缩效果不好,放弃压缩 */
ret = -E2BIG;
}
put_workspace(type, workspace);
return ret;
}
6.3.3 性能特征对比
| 特性 | ext4 | XFS | Btrfs |
| 特性 | ext4 | XFS | Btrfs |
|---|---|---|---|
| 最大文件 | 16 TB | 8 EB | 16 EB |
| 最大卷 | 1 EB | 8 EB | 16 EB |
| 并发性 | 中等 | 极高 | 高 |
| 小文件性能 | 优秀 | 一般 | 良好 |
| 大文件性能 | 良好 | 极佳 | 优秀 |
| 碎片整理 | 在线 | 在线 | 在线+自动 |
| 快照 | 无 | 无 | 原生支持 |
| 压缩 | 无 | 无 | 透明压缩 |
| 校验和 | 仅元数据 | 仅元数据 | 全部数据 |
| RAID | 需要 MD/LVM | 需要 MD/LVM | 内置 |
6.3.4 ZFS 影响与设计理念
虽然 ZFS 因许可证问题不在 Linux 主线,但它的设计深刻影响了 Btrfs:
端到端数据完整性:
- 每个块都有校验和
- 校验和存储在父块
- 形成 Merkle 树
存储池概念:
- 不再有固定分区
- 动态空间分配
- 多设备管理
事务性语义:
- 所有操作都是事务
- 永不覆写(除了超级块)
- 崩溃一致性保证
ARC 缓存:
- 自适应替换缓存
- 比 LRU 更智能
- 支持压缩缓存
6.3.5 文件系统选择决策
选择 ext4 的场景
- 稳定性要求极高
- 简单可靠的通用存储
- 小到中等规模部署
- 与旧系统兼容
选择 XFS 的场景
- 超大文件(视频、科学计算)
- 高并发访问
- 大规模存储(PB 级)
- 流媒体应用
选择 Btrfs 的场景
- 需要快照功能
- 数据完整性关键
- 需要在线压缩
- 容器存储后端