Linux 存储与文件系统深度剖析(六):Btrfs 文件系统核心原理

前言

在过去几十年间,Linux 生态系统中的文件系统经历了从 ext2 到 ext4、从 XFS 到 ZFS 的漫长演化。Btrfs(B-Tree File System,发音为 “Butter FS” 或 “Better FS”)是 Oracle 在 2007 年主导开发的下一代写时复制(Copy-on-Write,CoW)文件系统,旨在填补 Linux 原生高级文件系统的空白。它于 2009 年合并进 Linux 主线内核(2.6.29),经过十余年的持续发展,已经成为 SUSE Linux Enterprise Server 的默认文件系统,也是 Fedora 33 之后的默认选择。

本文基于 Linux 6.4-rc1 内核源码(路径:fs/btrfs/)进行深度剖析,目标是从内核数据结构和核心算法层面理解 Btrfs 的工作原理。


一、设计哲学:以 CoW 和 B-Tree 为基石

1.1 写时复制(Copy-on-Write)

CoW 是 Btrfs 的灵魂。传统文件系统(如 ext4)在修改数据时采用”就地写入”(in-place update)策略:直接覆盖原有块。这种方式在掉电或崩溃时极易导致数据不一致,需要 journal(日志)来补救。

Btrfs 的策略截然不同:任何修改都不覆盖原有数据块,而是在新位置写入,再更新指针。这带来了以下天然优势:

  • 崩溃一致性:旧数据始终有效,新数据写完才更新超级块中的根指针,天然原子。
  • 快照成本为零:快照只是增加了对现有树节点的引用计数,不需要复制数据。
  • 数据完整性:每次写入都产生新块,可在写入时顺便计算校验和。

1.2 B-Tree 无处不在

Btrfs 用一棵 B-Tree 来存储所有文件系统元数据,包括目录项、inode、文件 extent、空闲空间、设备信息等。B-Tree 的键是一个三元组 (objectid, type, offset),这个统一的键空间让 Btrfs 能够将所有元数据组织在同一套搜索逻辑下,极大简化了代码复杂度。

与传统 B+ 树不同,Btrfs 的树节点分为两类:

  • 内部节点(Node):只存储键和指向子节点的物理地址指针。
  • 叶子节点(Leaf):存储实际的元数据项(item),每个 item 包含键和可变长度的数据。

二、核心数据结构

2.1 键(Key):统一的寻址空间

Btrfs 中所有数据都通过一个三元组键来定位,内核中有两种表示形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// include/uapi/linux/btrfs_tree.h

/*
* btrfs_disk_key is in disk byte order. struct btrfs_key is always
* in cpu native order. Otherwise they are identical and their sizes
* should be the same (ie both packed)
*/
struct btrfs_disk_key {
__le64 objectid;
__u8 type;
__le64 offset;
} __attribute__ ((__packed__));

struct btrfs_key {
__u64 objectid;
__u8 type;
__u64 offset;
} __attribute__ ((__packed__));

btrfs_disk_key 用于磁盘存储(小端序),btrfs_key 用于内存操作(CPU 原生序)。这一区分避免了频繁的字节序转换开销,内核在 ctree.c 中也针对小端架构做了优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// fs/btrfs/ctree.c

#ifdef __LITTLE_ENDIAN
/*
* Compare two keys, on little-endian the disk order is same as CPU order and
* we can avoid the conversion.
*/
static int comp_keys(const struct btrfs_disk_key *disk_key,
const struct btrfs_key *k2)
{
const struct btrfs_key *k1 = (const struct btrfs_key *)disk_key;
return btrfs_comp_cpu_keys(k1, k2);
}
#endif

int __pure btrfs_comp_cpu_keys(const struct btrfs_key *k1, const struct btrfs_key *k2)
{
if (k1->objectid > k2->objectid)
return 1;
if (k1->objectid < k2->objectid)
return -1;
if (k1->type > k2->type)
return 1;
if (k1->type < k2->type)
return -1;
if (k1->offset > k2->offset)
return 1;
if (k1->offset < k2->offset)
return -1;
return 0;
}

键的比较先按 objectid,再按 type,最后按 offset,三级排序确保所有同类型数据在 B-Tree 中聚集存放,提升访问局部性。

2.2 树节点:Leaf 与 Node

叶子节点(Leaf)和内部节点(Node)共享同一个头部结构 btrfs_header

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// include/uapi/linux/btrfs_tree.h

/*
* Every tree block (leaf or node) starts with this header.
*/
struct btrfs_header {
/* These first four must match the super block */
__u8 csum[BTRFS_CSUM_SIZE];
/* FS specific uuid */
__u8 fsid[BTRFS_FSID_SIZE];
/* Which block this node is supposed to live in */
__le64 bytenr;
__le64 flags;

/* Allowed to be different from the super from here on down */
__u8 chunk_tree_uuid[BTRFS_UUID_SIZE];
__le64 generation;
__le64 owner;
__le32 nritems;
__u8 level;
} __attribute__ ((__packed__));

注意头部开头的 csum 字段:每一个树块都有自己的校验和,这是 Btrfs 数据完整性的基础。level 字段为 0 表示叶子节点,大于 0 表示内部节点。

叶子节点存储实际的 item,其布局是”两端向中间生长”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// include/uapi/linux/btrfs_tree.h

/*
* A leaf is full of items. offset and size tell us where to find the item in
* the leaf (relative to the start of the data area)
*/
struct btrfs_item {
struct btrfs_disk_key key;
__le32 offset;
__le32 size;
} __attribute__ ((__packed__));

/*
* Leaves have an item area and a data area:
* [item0, item1....itemN] [free space] [dataN...data1, data0]
*
* The data is separate from the items to get the keys closer together during
* searches.
*/
struct btrfs_leaf {
struct btrfs_header header;
struct btrfs_item items[];
} __attribute__ ((__packed__));

item 数组从叶子头部向后增长,而 item 对应的变长数据从尾部向前增长,两者在中间汇聚。这种布局使得 item 的键(8+1+8=17 字节)紧密排列在叶子开头,二分查找时的缓存命中率极高。

内部节点只存储键指针对(Key-Pointer Pair):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// include/uapi/linux/btrfs_tree.h

/*
* All non-leaf blocks are nodes, they hold only keys and pointers to other
* blocks.
*/
struct btrfs_key_ptr {
struct btrfs_disk_key key;
__le64 blockptr;
__le64 generation;
} __attribute__ ((__packed__));

struct btrfs_node {
struct btrfs_header header;
struct btrfs_key_ptr ptrs[];
} __attribute__ ((__packed__));

blockptr 是子节点的逻辑字节地址,generation 记录子节点最后一次被修改时所在的事务 ID,用于 CoW 判断和校验。

2.3 根(Root):每棵树的入口

每棵 B-Tree 对应一个 btrfs_root 结构,它是内存中对树的抽象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// fs/btrfs/ctree.h

/*
* in ram representation of the tree. extent_root is used for all allocations
* and for the extent tree extent_root root.
*/
struct btrfs_root {
struct rb_node rb_node;

struct extent_buffer *node; /* 当前根节点(写操作用) */
struct extent_buffer *commit_root; /* 已提交的根节点(读操作用) */
struct btrfs_root *log_root;
struct btrfs_root *reloc_root;

unsigned long state;
struct btrfs_root_item root_item;
struct btrfs_key root_key;
struct btrfs_fs_info *fs_info;

spinlock_t inode_lock;
struct rb_root inode_tree; /* 内存中 inode 的红黑树 */

/* red-black tree that keeps track of in-memory inodes */
struct radix_tree_root delayed_nodes_tree;

refcount_t refs;
/* ... 以及大量的锁、列表、计数器 ... */
};

nodecommit_root 的区别是 Btrfs 事务机制的关键:在事务提交完成之前,读操作使用 commit_root(稳定视图),写操作使用 node(当前最新版本)。

2.4 超级结构:btrfs_fs_info

btrfs_fs_info 是整个文件系统实例的核心控制块,包含了所有重要子系统的句柄:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// fs/btrfs/fs.h

struct btrfs_fs_info {
u8 chunk_tree_uuid[BTRFS_UUID_SIZE];
unsigned long flags;
struct btrfs_root *tree_root; /* 存放其他树根的"根之树" */
struct btrfs_root *chunk_root; /* 物理块组管理树 */
struct btrfs_root *dev_root; /* 设备信息树 */
struct btrfs_root *fs_root; /* 默认子卷树 */
struct btrfs_root *quota_root;
struct btrfs_root *uuid_root;
struct btrfs_root *data_reloc_root;
struct btrfs_root *block_group_root;

/* The log root tree is a directory of all the other log roots */
struct btrfs_root *log_root_tree;

/* The tree that holds the global roots (csum, extent, etc) */
rwlock_t global_root_lock;
struct rb_root global_root_tree;

spinlock_t trans_lock;
struct list_head trans_list;
struct btrfs_transaction *running_transaction;

u64 generation;
u64 last_trans_committed;

unsigned long compress_type:4;
unsigned int compress_level;
u32 commit_interval;
/* ... */
};

tree_root 是”树中之树”(Tree of Trees),它的每一个 item 记录了一棵子卷树或系统树的根节点位置。Btrfs 通过这种递归结构实现了几乎无限数量的子卷/快照管理。

2.5 路径:btrfs_path

在 B-Tree 中搜索时,需要记录从根到叶子的完整路径,以便后续插入、删除时能直接在路径上操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// fs/btrfs/ctree.h

/*
* btrfs_paths remember the path taken from the root down to the leaf.
* level 0 is always the leaf, and nodes[1...BTRFS_MAX_LEVEL] will point
* to any other levels that are present.
*
* The slots array records the index of the item or block pointer
* used while walking the tree.
*/
struct btrfs_path {
struct extent_buffer *nodes[BTRFS_MAX_LEVEL];
int slots[BTRFS_MAX_LEVEL];
u8 locks[BTRFS_MAX_LEVEL];
u8 reada;
u8 lowest_level;

unsigned int search_for_split:1;
unsigned int keep_locks:1;
unsigned int skip_locking:1;
unsigned int search_commit_root:1;
unsigned int need_commit_sem:1;
unsigned int skip_release_on_error:1;
unsigned int search_for_extension:1;
unsigned int nowait:1;
};

nodes[0] 是叶子节点,nodes[1]nodes[2] 等是对应层级的内部节点。slots[i] 记录在第 i 层节点中选中的槽位序号。locks[i] 记录各层持有的锁类型(读锁或写锁)。


三、B-Tree 核心操作

3.1 查找:btrfs_search_slot

btrfs_search_slot 是 Btrfs 中最核心的函数,几乎所有对文件系统的读写操作最终都通过它来定位数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// fs/btrfs/ctree.c

/*
* btrfs_search_slot - look for a key in a tree and perform necessary
* modifications to preserve tree invariants.
*
* @trans: Handle of transaction, used when modifying the tree
* @p: Holds all btree nodes along the search path
* @root: The root node of the tree
* @key: The key we are looking for
* @ins_len: Indicates purpose of search:
* >0 for inserts it's size of item inserted
* <0 for deletions
* 0 for plain searches, not modifying the tree
* @cow: boolean should CoW operations be performed.
*
* If @key is found, 0 is returned and you can find the item in the leaf level
* of the path (level 0)
*
* If @key isn't found, 1 is returned and the leaf level of the path (level 0)
* points to the slot where it should be inserted
*/
int btrfs_search_slot(struct btrfs_trans_handle *trans, struct btrfs_root *root,
const struct btrfs_key *key, struct btrfs_path *p,
int ins_len, int cow)

函数的主循环从根节点开始,逐层向下,每层通过 btrfs_bin_search 做二分查找定位子节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// fs/btrfs/ctree.c

int btrfs_bin_search(struct extent_buffer *eb, int first_slot,
const struct btrfs_key *key, int *slot)
{
unsigned long p;
int item_size;
u32 low = first_slot;
u32 high = btrfs_header_nritems(eb);
int ret;
const int key_size = sizeof(struct btrfs_disk_key);

if (btrfs_header_level(eb) == 0) {
p = offsetof(struct btrfs_leaf, items);
item_size = sizeof(struct btrfs_item);
} else {
p = offsetof(struct btrfs_node, ptrs);
item_size = sizeof(struct btrfs_key_ptr);
}

while (low < high) {
unsigned long oip;
unsigned long offset;
struct btrfs_disk_key *tmp;
struct btrfs_disk_key unaligned;
int mid;

mid = (low + high) / 2;
offset = p + mid * item_size;
oip = offset_in_page(offset);

if (oip + key_size <= PAGE_SIZE) {
const unsigned long idx = get_eb_page_index(offset);
char *kaddr = page_address(eb->pages[idx]);
oip = get_eb_offset_in_page(eb, offset);
tmp = (struct btrfs_disk_key *)(kaddr + oip);
} else {
read_extent_buffer(eb, &unaligned, offset, key_size);
tmp = &unaligned;
}

ret = comp_keys(tmp, key);
if (ret < 0)
low = mid + 1;
else if (ret > 0)
high = mid;
else {
*slot = mid;
return 0;
}
}
*slot = low;
return 1;
}

值得注意的是,这里有一个微优化:优先尝试直接访问 extent buffer 的 page(避免跨页拷贝),只有当键跨越了页边界时才用 read_extent_buffer 拷贝到临时变量。

在写操作时(cow=1),btrfs_search_slot 在下行过程中遇到需要修改的节点,会调用 btrfs_cow_block 进行 CoW 处理,并对不再需要的层级释放锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// fs/btrfs/ctree.c (btrfs_search_slot 主循环简化版)

while (b) {
level = btrfs_header_level(b);

if (cow) {
if (!should_cow_block(trans, root, b))
goto cow_done;
/* ... 确保持有足够级别的写锁 ... */
err = btrfs_cow_block(trans, root, b,
p->nodes[level + 1],
p->slots[level + 1], &b,
BTRFS_NESTING_COW);
if (err) {
ret = err;
goto done;
}
}
cow_done:
p->nodes[level] = b;
/* ... 向下一层走 ... */
}

3.2 节点的分裂与合并

当向叶子节点插入 item 时,若叶子空间不足,split_leaf 会创建一个新叶子并将一半 item 迁移过去(同时触发 CoW)。删除时,若节点中的 item 数量低于阈值,balance_level 会尝试从相邻节点借 item 或合并节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// fs/btrfs/ctree.c

/*
* node level balancing, used to make sure nodes are in proper order for
* item deletion. We balance from the top down, so we have to make sure
* that a deletion won't leave a node completely empty later on.
*/
static noinline int balance_level(struct btrfs_trans_handle *trans,
struct btrfs_root *root,
struct btrfs_path *path, int level)
{
/* ... 尝试从左/右兄弟节点移入 item,若节点为空则释放 ... */
if (btrfs_header_nritems(mid) == 1) {
/*
* we're not allowed to leave a node with one item in the
* tree during a delete. A deletion from lower in the tree
* could try to delete the only pointer in this node.
* So, pull some keys from the left.
*/
wret = balance_node_right(trans, mid, left);
}
if (btrfs_header_nritems(mid) == 0) {
btrfs_clear_buffer_dirty(trans, mid);
btrfs_tree_unlock(mid);
del_ptr(root, path, level + 1, pslot);
root_sub_used(root, mid->len);
btrfs_free_tree_block(trans, btrfs_root_id(root), mid, 0, 1);
}
/* ... */
}

四、写时复制(CoW)机制详解

4.1 何时需要 CoW

should_cow_block 函数决定一个树块是否需要被 CoW:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// fs/btrfs/ctree.c

static inline int should_cow_block(struct btrfs_trans_handle *trans,
struct btrfs_root *root,
struct extent_buffer *buf)
{
if (btrfs_is_testing(root->fs_info))
return 0;

/* Ensure we can see the FORCE_COW bit */
smp_mb__before_atomic();

/*
* We do not need to cow a block if
* 1) this block is not created or changed in this transaction;
* 2) this block does not belong to TREE_RELOC tree;
* 3) the root is not forced COW.
*
* What is forced COW:
* when we create snapshot during committing the transaction,
* after we've finished copying src root, we must COW the shared
* block to ensure the metadata consistency.
*/
if (btrfs_header_generation(buf) == trans->transid &&
!btrfs_header_flag(buf, BTRFS_HEADER_FLAG_WRITTEN) &&
!(root->root_key.objectid != BTRFS_TREE_RELOC_OBJECTID &&
btrfs_header_flag(buf, BTRFS_HEADER_FLAG_RELOC)) &&
!test_bit(BTRFS_ROOT_FORCE_COW, &root->state))
return 0;
return 1;
}

核心逻辑:如果一个块已经在当前事务中被分配或修改(generation == trans->transid),且还没有被写回磁盘(!WRITTEN),则不需要 CoW,可以直接修改。否则必须 CoW。

这个优化极为重要:同一事务内对同一块的多次修改不会产生多余的 CoW 开销。

4.2 CoW 的实现:__btrfs_cow_block

实际的 CoW 工作由 __btrfs_cow_block 完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// fs/btrfs/ctree.c

/*
* does the dirty work in cow of a single block. The parent block (if
* supplied) is updated to point to the new cow copy. The new buffer is marked
* dirty and returned locked.
*/
static noinline int __btrfs_cow_block(struct btrfs_trans_handle *trans,
struct btrfs_root *root,
struct extent_buffer *buf,
struct extent_buffer *parent, int parent_slot,
struct extent_buffer **cow_ret,
u64 search_start, u64 empty_size,
enum btrfs_lock_nesting nest)
{
/* ... */
level = btrfs_header_level(buf);

/* 分配一个新树块 */
cow = btrfs_alloc_tree_block(trans, root, parent_start,
root->root_key.objectid, &disk_key, level,
search_start, empty_size, nest);

/* 将旧块内容完整复制到新块 */
copy_extent_buffer_full(cow, buf);

/* 更新新块的头部元数据 */
btrfs_set_header_bytenr(cow, cow->start);
btrfs_set_header_generation(cow, trans->transid); /* 标记为当前事务 */
btrfs_set_header_backref_rev(cow, BTRFS_MIXED_BACKREF_REV);
btrfs_clear_header_flag(cow, BTRFS_HEADER_FLAG_WRITTEN |
BTRFS_HEADER_FLAG_RELOC);
btrfs_set_header_owner(cow, root->root_key.objectid);

/* 更新反向引用,处理共享块的引用计数 */
ret = update_ref_for_cow(trans, root, buf, cow, &last_ref);

if (buf == root->node) {
/* 如果 CoW 的是根节点,更新 root->node 指针 */
rcu_assign_pointer(root->node, cow);
btrfs_free_tree_block(trans, btrfs_root_id(root), buf,
parent_start, last_ref);
} else {
/* 否则更新父节点中的指针 */
btrfs_set_node_blockptr(parent, parent_slot, cow->start);
btrfs_set_node_ptr_generation(parent, parent_slot, trans->transid);
btrfs_mark_buffer_dirty(parent);
btrfs_free_tree_block(trans, btrfs_root_id(root), buf,
parent_start, last_ref);
}

btrfs_mark_buffer_dirty(cow);
*cow_ret = cow;
return 0;
}

这段代码展示了 CoW 的完整流程:

  1. 分配新块(btrfs_alloc_tree_block
  2. 复制旧块内容(copy_extent_buffer_full
  3. 更新新块头部,将 generation 设为当前事务 ID
  4. 更新引用计数(旧块引用减少,新块引用增加)
  5. 将父节点中指向旧块的指针改为指向新块
  6. 释放旧块(可能是真正释放,也可能只是解除当前根的引用)

4.3 共享块的引用计数

当多棵树(如快照和原始子卷)共享同一个树块时,btrfs_block_can_be_shared 会检测到这种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// fs/btrfs/ctree.c

/*
* check if the tree block can be shared by multiple trees
*/
int btrfs_block_can_be_shared(struct btrfs_root *root,
struct extent_buffer *buf)
{
/*
* Tree blocks not in shareable trees and tree roots are never shared.
* If a block was allocated after the last snapshot and the block was
* not allocated by tree relocation, we know the block is not shared.
*/
if (test_bit(BTRFS_ROOT_SHAREABLE, &root->state) &&
buf != root->node && buf != root->commit_root &&
(btrfs_header_generation(buf) <=
btrfs_root_last_snapshot(&root->root_item) ||
btrfs_header_flag(buf, BTRFS_HEADER_FLAG_RELOC)))
return 1;
return 0;
}

如果一个树块的 generation 小于等于 last_snapshot(最后一次快照时的事务 ID),说明它可能被某个快照引用,必须走完整的引用计数路径。


五、快照与子卷(Snapshot / Subvolume)

5.1 子卷:独立的文件系统树

Btrfs 的子卷(Subvolume)是一棵独立的 B-Tree,有自己的根节点,可以像独立文件系统一样被挂载。每个子卷在 tree_root 中有对应的 btrfs_root_item,通过唯一的 objectid 标识。

快照(Snapshot)本质上是对某个子卷树根节点的浅拷贝:两者共享所有树块,通过引用计数追踪共享关系。写时复制保证修改任何一方都不会影响另一方。

5.2 快照创建的内核实现

快照创建发生在事务提交阶段,由 create_pending_snapshot 完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// fs/btrfs/transaction.c

static noinline int create_pending_snapshot(struct btrfs_trans_handle *trans,
struct btrfs_pending_snapshot *pending)
{
/* ... */
/* 为新快照分配一个新的 objectid */
pending->error = btrfs_get_free_objectid(tree_root, &objectid);

/* 记录源子卷的最后快照事务 ID */
btrfs_set_root_last_snapshot(&root->root_item, trans->transid);
memcpy(new_root_item, &root->root_item, sizeof(*new_root_item));

/* 对源子卷的根节点做一次 CoW,确保后续修改都走 CoW 路径 */
old = btrfs_lock_root_node(root);
ret = btrfs_cow_block(trans, root, old, NULL, 0, &old,
BTRFS_NESTING_COW);

/* 将 CoW 后的根节点复制一份,作为快照的根 */
ret = btrfs_copy_root(trans, root, old, &tmp, objectid);

/* 设置 FORCE_COW 标志,后续对源子卷的任何修改都必须 CoW */
set_bit(BTRFS_ROOT_FORCE_COW, &root->state);
smp_wmb();

/* 将快照的根节点写入 tree_root */
btrfs_set_root_node(new_root_item, tmp);
key.offset = trans->transid;
ret = btrfs_insert_root(trans, tree_root, &key, new_root_item);

/* 添加目录项,使快照可以通过文件系统路径访问 */
ret = btrfs_add_root_ref(trans, objectid,
parent_root->root_key.objectid,
btrfs_ino(BTRFS_I(parent_inode)), index,
&fname.disk_name);
/* ... */
}

这段代码揭示了快照创建的精髓:整个操作代价几乎为零,只是:

  1. 给源子卷根节点做一次 CoW(保证后续修改独立)
  2. 复制根节点作为快照根(btrfs_copy_root
  3. 在 tree_root 中插入一条记录

不涉及任何数据块的复制,无论子卷有多大,快照的创建时间都是 O(1)。

5.3 btrfs_pending_snapshot 结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// fs/btrfs/transaction.h

struct btrfs_pending_snapshot {
struct dentry *dentry;
struct inode *dir;
struct btrfs_root *root;
struct btrfs_root_item *root_item;
struct btrfs_root *snap;
struct btrfs_qgroup_inherit *inherit;
struct btrfs_path *path;
struct btrfs_block_rsv block_rsv;
int error;
dev_t anon_dev;
bool readonly;
struct list_head list;
};

快照请求被挂到当前事务的 pending_snapshots 链表上,在事务提交时统一处理。这保证了快照创建的原子性。


六、数据校验和(Checksum)机制

6.1 支持的校验和算法

Btrfs 支持四种校验和算法,在 ctree.c 中以静态数组定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
// fs/btrfs/ctree.c

static const struct btrfs_csums {
u16 size;
const char name[10];
const char driver[12];
} btrfs_csums[] = {
[BTRFS_CSUM_TYPE_CRC32] = { .size = 4, .name = "crc32c" },
[BTRFS_CSUM_TYPE_XXHASH] = { .size = 8, .name = "xxhash64" },
[BTRFS_CSUM_TYPE_SHA256] = { .size = 32, .name = "sha256" },
[BTRFS_CSUM_TYPE_BLAKE2] = { .size = 32, .name = "blake2b",
.driver = "blake2b-256" },
};
算法 长度 特点
CRC32c 4 字节 默认算法,硬件加速,速度最快
xxHash64 8 字节 非加密哈希,速度极快
SHA256 32 字节 加密哈希,安全性高
BLAKE2b-256 32 字节 加密哈希,兼顾速度与安全

校验和覆盖:

  • 元数据:每个树块(leaf 或 node)的 btrfs_header 中的 csum 字段覆盖整个块。
  • 数据块:文件数据的校验和存储在 checksum tree(csum tree)中,以 (inode, offset) 为键索引。

6.2 元数据树块的校验

每个树块在读入内存时(btree_read_folio_end_io_hook)和写出磁盘前都会验证校验和。如果校验失败,内核会报告 I/O 错误并拒绝使用该块,结合 RAID 功能可以自动从副本恢复。


七、事务机制

7.1 事务状态机

Btrfs 的事务系统是保障崩溃一致性的核心。在 transaction.c 的注释中,完整描述了事务的状态转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
No running transaction
|
V
Transaction N [TRANS_STATE_RUNNING]
| 新事务,所有写操作都可以加入
V
Transaction N [TRANS_STATE_COMMIT_START]
| 某个调用者触发了 commit,等待所有写者退出
V
Transaction N [TRANS_STATE_COMMIT_DOING]
| 执行重量级工作:运行 delayed refs,创建 pending snapshots,更新 qgroups
V
Transaction N [TRANS_STATE_UNBLOCKED]
| 所有树已修改完毕,开始将脏页写回磁盘
| 此时 Transaction N+1 可以开始运行
V
Transaction N [TRANS_STATE_SUPER_COMMITTED]
| 超级块已写入(原子操作,此后文件系统一致)
V
Transaction N [TRANS_STATE_COMPLETED]
所有工作完成,事务结构体被释放

7.2 btrfs_transaction 数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// fs/btrfs/transaction.h

struct btrfs_transaction {
u64 transid;
atomic_t num_extwriters; /* 外部写者数量(必须在 commit 前归零) */
atomic_t num_writers; /* 总写者数量(必须在 commit 前归零) */
refcount_t use_count;

unsigned long flags;
enum btrfs_trans_state state;
int aborted;

struct list_head list;
struct extent_io_tree dirty_pages;
time64_t start_time;
wait_queue_head_t writer_wait;
wait_queue_head_t commit_wait;
struct list_head pending_snapshots; /* 待创建的快照 */
struct list_head dirty_bgs; /* 脏块组 */
struct extent_io_tree pinned_extents;/* 被 pin 的 extent(不可释放) */

struct btrfs_delayed_ref_root delayed_refs; /* 延迟引用更新 */
struct btrfs_fs_info *fs_info;
};

7.3 加入事务:join_transaction

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// fs/btrfs/transaction.c

static noinline int join_transaction(struct btrfs_fs_info *fs_info,
unsigned int type)
{
struct btrfs_transaction *cur_trans;

spin_lock(&fs_info->trans_lock);
loop:
/* The file system has been taken offline. No new transactions. */
if (BTRFS_FS_ERROR(fs_info)) {
spin_unlock(&fs_info->trans_lock);
return -EROFS;
}

cur_trans = fs_info->running_transaction;
if (cur_trans) {
if (TRANS_ABORTED(cur_trans)) {
spin_unlock(&fs_info->trans_lock);
return cur_trans->aborted;
}
if (btrfs_blocked_trans_types[cur_trans->state] & type) {
spin_unlock(&fs_info->trans_lock);
return -EBUSY;
}
refcount_inc(&cur_trans->use_count);
atomic_inc(&cur_trans->num_writers);
extwriter_counter_inc(cur_trans, type);
spin_unlock(&fs_info->trans_lock);
return 0;
}
/* ... 若无运行中事务,创建新事务 ... */
}

btrfs_blocked_trans_types 数组定义了不同状态下哪些类型的事务连接被阻止,这是实现流控和有序提交的关键。

7.4 事务提交的关键步骤

完整的 btrfs_commit_transaction 流程包括:

  1. 等待外部写者退出wait_event on num_extwriters
  2. 运行 delayed refs(将延迟的 extent 引用修改写入 extent tree)
  3. 创建 pending snapshots(调用 create_pending_snapshot
  4. commit_cowonly_roots(更新 extent tree、chunk tree 等非可共享树)
  5. switch_commit_roots(将各子卷的 commit_root 切换为当前 node
  6. 写出脏页btrfs_write_and_wait_transaction
  7. 写超级块write_all_supers,先写备份,再写主超级块)

超级块写入使用原子写策略:先将新超级块写到所有设备,再将最新事务 ID 写入超级块头部的固定偏移(64KB 处)。由于超级块大小(4KB)不超过最小 I/O 单位,这个写操作是原子的,确保崩溃后要么使用新版本要么使用旧版本,不会出现中间状态。


八、RAID 支持

8.1 RAID 配置矩阵

Btrfs 在 volumes.c 中通过 btrfs_raid_array 定义了所有支持的 RAID 级别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// fs/btrfs/volumes.c

const struct btrfs_raid_attr btrfs_raid_array[BTRFS_NR_RAID_TYPES] = {
[BTRFS_RAID_RAID10] = {
.sub_stripes = 2,
.devs_min = 2,
.tolerated_failures = 1,
.ncopies = 2,
.nparity = 0,
.raid_name = "raid10",
},
[BTRFS_RAID_RAID1] = {
.devs_max = 2,
.devs_min = 2,
.tolerated_failures = 1,
.ncopies = 2,
.nparity = 0,
.raid_name = "raid1",
},
[BTRFS_RAID_RAID1C3] = {
.devs_max = 3,
.devs_min = 3,
.tolerated_failures = 2,
.ncopies = 3,
.nparity = 0,
.raid_name = "raid1c3",
},
[BTRFS_RAID_RAID1C4] = {
.devs_max = 4,
.devs_min = 4,
.tolerated_failures = 3,
.ncopies = 4,
.nparity = 0,
.raid_name = "raid1c4",
},
[BTRFS_RAID_DUP] = {
.devs_max = 1,
.devs_min = 1,
.tolerated_failures = 0,
.ncopies = 2, /* 同一设备上写两份 */
.nparity = 0,
.raid_name = "dup",
},
[BTRFS_RAID_RAID0] = {
.devs_min = 1,
.tolerated_failures = 0,
.ncopies = 1,
.nparity = 0,
.raid_name = "raid0",
},
[BTRFS_RAID_RAID5] = {
.devs_min = 2,
.tolerated_failures = 1,
.ncopies = 1,
.nparity = 1,
.raid_name = "raid5",
},
[BTRFS_RAID_RAID6] = {
.devs_min = 3,
.tolerated_failures = 2,
.ncopies = 1,
.nparity = 2,
.raid_name = "raid6",
},
};

8.2 Btrfs RAID 的特殊性

与传统 Linux MD RAID 不同,Btrfs 的 RAID 是在文件系统层实现的,有以下重要特性:

元数据和数据可以独立配置 RAID 级别。例如可以让元数据走 RAID1(两份冗余),数据走 RAID0(条带化,追求速度):

1
mkfs.btrfs -m raid1 -d raid0 /dev/sda /dev/sdb

Btrfs RAID 能做端到端校验。scrub 操作会读出 RAID 各副本并比较校验和,发现不一致时能从好的副本修复,这是 MD RAID 无法做到的。

RAID5/6 目前有已知问题。Btrfs RAID5/6 存在写洞(write hole)问题,即在条带写入过程中掉电可能导致数据不一致。生产环境中不推荐将 RAID5/6 用于重要数据(截至 Linux 6.4)。

8.3 块组与条带分配

Btrfs 将存储空间划分为块组(Block Group),每个块组有固定的类型(DATA、METADATA、SYSTEM)和 RAID 配置。btrfs_bg_flags_to_raid_index 将块组标志转换为 RAID 索引:

1
2
3
4
5
6
7
8
9
10
// fs/btrfs/volumes.c

enum btrfs_raid_types __attribute_const__
btrfs_bg_flags_to_raid_index(u64 flags)
{
const u64 profile = (flags & BTRFS_BLOCK_GROUP_PROFILE_MASK);
if (!profile)
return BTRFS_RAID_SINGLE;
return BTRFS_BG_FLAG_TO_INDEX(profile);
}

九、压缩支持

9.1 透明压缩

Btrfs 支持对文件数据进行透明压缩,在写入时自动压缩,读取时自动解压。支持三种算法:

1
2
3
// fs/btrfs/compression.c

static const char* const btrfs_compress_types[] = { "", "zlib", "lzo", "zstd" };

压缩分发逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// fs/btrfs/compression.c

static int compression_compress_pages(int type, struct list_head *ws,
struct address_space *mapping, u64 start, struct page **pages,
unsigned long *out_pages, unsigned long *total_in,
unsigned long *total_out)
{
switch (type) {
case BTRFS_COMPRESS_ZLIB:
return zlib_compress_pages(ws, mapping, start, pages,
out_pages, total_in, total_out);
case BTRFS_COMPRESS_LZO:
return lzo_compress_pages(ws, mapping, start, pages,
out_pages, total_in, total_out);
case BTRFS_COMPRESS_ZSTD:
return zstd_compress_pages(ws, mapping, start, pages,
out_pages, total_in, total_out);
case BTRFS_COMPRESS_NONE:
default:
*out_pages = 0;
return -E2BIG;
}
}

9.2 算法比较

算法 压缩率 速度 适用场景
zlib 较慢 冷数据,存档
lzo 极快 实时压缩,热数据
zstd 快(可调 level 1-15) 通用推荐

挂载时通过 compress=zstd:3 等选项指定算法和级别。若某个文件压缩后体积比原始大(不可压缩数据如图片、视频),Btrfs 会自动给该文件标记 NOCOMPRESS 属性,后续写入跳过压缩流程,避免浪费 CPU。

9.3 压缩与 CoW 的结合

压缩数据写入流程:

  1. 用户写入数据,进入 ordered extent 队列
  2. 后台工作线程尝试压缩
  3. 若压缩成功,以压缩后的尺寸分配 extent,写入磁盘
  4. 在 extent tree 中记录压缩类型和原始/压缩大小

由于 Btrfs 是 CoW 的,每次写入都分配新 extent,压缩和非压缩的数据可以共存,也无需担心就地更新压缩数据的复杂性。


十、常见运维操作

10.1 balance:数据重新分布

btrfs balance 是 Btrfs 最重要也最复杂的运维操作。它遍历所有块组,对每个块组中的所有 extent 进行重新分配(relocate),用途包括:

  • 添加/移除设备后重新平衡数据分布
  • 转换 RAID 级别(例如将 single 转为 raid1)
  • 碎片整理(减少元数据碎片)
  • 缩减文件系统(先 balance 将数据从尾部移走,再 resize)
1
2
3
4
5
# 将元数据从 single 转换为 raid1
btrfs balance start -mconvert=raid1 /mountpoint

# 只 balance 使用率低于 10% 的块组(快速减少块组数量)
btrfs balance start -dusage=10 /mountpoint

balance 操作可以暂停和恢复,适合在生产系统上分阶段执行。

10.2 scrub:数据完整性检查

btrfs scrub 读取文件系统中所有数据和元数据,验证校验和,并在 RAID 配置下从副本修复损坏数据:

1
2
btrfs scrub start /mountpoint
btrfs scrub status /mountpoint

scrub 是 Btrfs 数据完整性保障的重要工具,建议定期运行(例如每月一次)。它能发现磁盘静默数据损坏(silent data corruption),这是传统文件系统无法检测的问题。

10.3 send/receive:增量备份

btrfs sendbtrfs receive 实现了高效的增量数据传输,基于快照差异:

1
2
3
4
5
6
7
# 创建初始快照并发送到备份设备
btrfs subvolume snapshot -r /data /data_snap1
btrfs send /data_snap1 | btrfs receive /backup

# 后续增量备份
btrfs subvolume snapshot -r /data /data_snap2
btrfs send -p /data_snap1 /data_snap2 | btrfs receive /backup

增量 send 只传输两个快照之间的差异(新增、修改、删除的文件),效率极高。内核通过比较两棵 B-Tree(通过遍历 commit_root)来生成差异流。

10.4 子卷管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 创建子卷
btrfs subvolume create /mountpoint/mysubvol

# 创建可写快照
btrfs subvolume snapshot /mountpoint/mysubvol /mountpoint/mysnap

# 创建只读快照(用于备份或 send)
btrfs subvolume snapshot -r /mountpoint/mysubvol /mountpoint/mysnap_ro

# 列出所有子卷
btrfs subvolume list /mountpoint

# 删除子卷(异步,后台回收空间)
btrfs subvolume delete /mountpoint/mysnap

十一、性能特点与适用场景

11.1 性能优势

顺序写入性能优秀:由于 CoW 天然地将随机写转化为顺序追加(总是在新位置写入),对 HDD 友好,可以减少磁头寻道。

元数据操作高效:统一的 B-Tree 结构,所有元数据操作都是 O(log n)。单个大目录的遍历比 ext4 更快(B-Tree vs. linear hash)。

快照/克隆近乎零成本:快照、克隆文件(reflink)的创建时间不随数据量增长,始终是 O(1)。

内置压缩提升有效存储:对于文本、日志、代码等可压缩数据,zstd 压缩可将实际存储量降低 40%-70%,同时因为读取更少字节,在某些场景下实际读速度反而更快。

11.2 性能局限

随机小写开销较高:CoW 意味着每次写都要分配新块并更新元数据,比 ext4 的就地写入有额外开销。对于数据库等频繁随机写的工作负载,通常建议使用 nodatacow 挂载选项或将数据库文件放在关闭了 CoW 的子目录。

元数据碎片:长期运行后,由于 CoW 不断分配新块,元数据可能产生碎片,导致 B-Tree 节点分散。定期 balance 可以缓解此问题。

RAID56 不适合生产:如前所述,RAID5/6 的写洞问题尚未完全解决。

11.3 适用场景

场景 推荐配置 理由
桌面/工作站 默认单设备 + zstd 快照便于系统回滚,压缩节省空间
容器宿主机 多设备 raid1 子卷隔离容器,快照快速回滚
NAS / 媒体服务器 多设备 raid1 + scrub 数据完整性保障,增量备份高效
开发服务器 单设备 + 快照 代码仓库快照,便于实验性操作
高并发数据库 nodatacow + 关闭压缩 避免 CoW 开销,接近裸盘性能

总结

Btrfs 是一个设计理念超前的文件系统,其以 B-Tree + CoW 为核心的架构带来了快照、压缩、校验、RAID 的完整集成。通过本文的源码分析,可以看到:

  1. 统一的 B-Tree 键空间btrfs_key)是架构简洁性的来源
  2. should_cow_block 的优化使同一事务内的多次修改不产生额外 CoW 开销
  3. 快照创建的 O(1) 复杂度来自于 CoW + 引用计数的天然配合
  4. 事务状态机RUNNING -> COMMIT_START -> COMMIT_DOING -> ...)保证了崩溃一致性
  5. 端到端校验和(元数据每块、数据每 extent)提供了比传统文件系统强得多的数据完整性保障

随着 Linux 内核持续演进(6.4 及以后),Btrfs 的稳定性和性能还在不断提升,是现代 Linux 系统中极具价值的文件系统选择。