本文不是严格意义上的教程,为折腾的经验分享【涉及编译内核模块,折腾有风险,谨慎操作】
OS版本介绍(以下均为以 unraid
为底座的 VM
)
群晖:OS 7.X,单盘 Raid0
(具体忘了)
飞牛:OS 0.9.18
前言
为了标题搜索了很多帖子,什么SMB挂载、双盘拷贝、老群晖的挂载方案,这些方法对我都不适用或者没那条件,我就想直接挂载上
但直接挂载报错
# mount xxxx xxxx
can't read superblock on xxxxxxx
dmesg(1) may have more information after failed mount system call.
# dmesg 内容如下,kernel 5.4.0
BTRFS critical (device md3) corrupt leaf: root = 1 block=xxxxxx slot= xxx, invalid root flag, have 0x400000000 expect mask 0x1000000000001
BTRFS error (device md3) block=xxxx read time tree block corruption detected
于是在外网搜了很多帖子,有人说老板本内核能挂载的,一试还真可以,没问题
又有说 在老内核删除磁盘中 btrfs
关于群晖的 subvolume
就可以在新版本挂载,试了在新内核果然能挂载。
但 ls
报错,文件夹也进不去,文件夹除了名字以外全是问号。一看 dmesg
还是报类似的错误
但这似乎解决了一定的问题,因为报错信息改变了,这点后文会说
# ls
read error: Input/output error
d??????????? ????? <文件夹>
# dmesg内容如下,kernel 5.4.0
corrupt leaf: root=xxxxx block=xxxx slot=xxxx ino=xxx, unknown flags detected: 0x40000000
- 至此三个疑问
挂载的错误是什么意思?
为什么老内核可以挂载,新内核不可以?
怎么让新内核和老内核一样挂载成功?
再加一个彩蛋:为什么删除 btrfs
的群晖相关 subvolume
后,错误发生变化?
为了解决以上疑问,我不得不开始研究内核代码
正文
通过对内核版本的不断尝试,可以锁定这个不能挂载的问题最早出现在两个系统版本的过渡上
5.1.21-050121
-> 5.2.0-050200
(<= 前者能挂载,>=后者则不能)
而且通过报错信息确定,报错相关代码在内核的 fs/btrfs/tree-check.c
中
5.2.0-050200
之后的版本,报错信息都可能有变化,但应都在这个文件中
# tree-check.c,kernel 5.2.0-050200
static int check_leaf_item(struct extent_buffer *leaf,
struct btrfs_key *key, int slot,
struct btrfs_key *prev_key)
{
int ret = 0;
struct btrfs_chunk *chunk;
switch (key->type) {
case BTRFS_EXTENT_DATA_KEY:
ret = check_extent_data_item(leaf, key, slot, prev_key);
break;
case BTRFS_EXTENT_CSUM_KEY:
ret = check_csum_item(leaf, key, slot);
break;
case BTRFS_DIR_ITEM_KEY:
case BTRFS_DIR_INDEX_KEY:
case BTRFS_XATTR_ITEM_KEY:
ret = check_dir_item(leaf, key, slot);
break;
case BTRFS_BLOCK_GROUP_ITEM_KEY:
ret = check_block_group_item(leaf, key, slot);
break;
case BTRFS_CHUNK_ITEM_KEY:
chunk = btrfs_item_ptr(leaf, slot, struct btrfs_chunk);
ret = btrfs_check_chunk_valid(leaf, chunk, key->offset);
break;
case BTRFS_DEV_ITEM_KEY:
ret = check_dev_item(leaf, key, slot);
break;
case BTRFS_INODE_ITEM_KEY:
// 调用入口
ret = check_inode_item(leaf, key, slot);
break;
}
return ret;
}
static int check_inode_item(struct extent_buffer *leaf,
struct btrfs_key *key, int slot)
{
.....
if (btrfs_inode_flags(leaf, iitem) & ~BTRFS_INODE_FLA**ASK) {
// 报错点
inode_item_err(fs_info, leaf, slot,
"unknown flags detected: 0x%llx",
btrfs_inode_flags(leaf, iitem) &
~BTRFS_INODE_FLA**ASK);
return -EUCLEAN;
}
return 0;
}
通过对比两个版本的代码就知道为什么 5.1.21-050121
挂载没问题,且不会报错了
因为上面的代码是新版本刚加的!!!
# tree-check.c,kernel 5.1.21-050121
static int check_leaf_item(struct btrfs_fs_info *fs_info,
struct extent_buffer *leaf,
struct btrfs_key *key, int slot)
{
int ret = 0;
switch (key->type) {
case BTRFS_EXTENT_DATA_KEY:
ret = check_extent_data_item(fs_info, leaf, key, slot);
break;
case BTRFS_EXTENT_CSUM_KEY:
ret = check_csum_item(fs_info, leaf, key, slot);
break;
case BTRFS_DIR_ITEM_KEY:
case BTRFS_DIR_INDEX_KEY:
case BTRFS_XATTR_ITEM_KEY:
ret = check_dir_item(fs_info, leaf, key, slot);
break;
case BTRFS_BLOCK_GROUP_ITEM_KEY:
ret = check_block_group_item(fs_info, leaf, key, slot);
break;
// 并没有check_inode_item
}
return ret;
}
所以这些新加的代码是干什么?
现在通过读代码和报错信息可以大概分析出来报错的原因
整个 check_leaf_item
,都是在校验BTFS相关的元信息,而报错的部分则是在校验 inode
的 flag
信息是否合法
而群晖上的 inode
的 flag
无法通过新版本的校验
// corrupt leaf: root=xxxxx block=xxxx slot=xxxx ino=xxx, unknown flags detected: 0x400000000
if (btrfs_inode_flags(leaf, iitem) & ~BTRFS_INODE_FLA**ASK) {
// 报错点
inode_item_err(fs_info, leaf, slot,
"unknown flags detected: 0x%llx",
btrfs_inode_flags(leaf, iitem) &
~BTRFS_INODE_FLA**ASK);
return -EUCLEAN;
}
/*
* ctree.h,kernel 5.1.21-050121
* flag为32位,1 << N,代表32位中的第N位为1
* 以下是所有合法的 Inode flags
* 报错的 0x40000000 转换成以下格式为(1 << 30),并不合法
*/
#define BTRFS_INODE_NODATASUM (1 << 0)
#define BTRFS_INODE_NODATACOW (1 << 1)
#define BTRFS_INODE_READONLY (1 << 2)
#define BTRFS_INODE_NOCOMPRESS (1 << 3)
#define BTRFS_INODE_PREALLOC (1 << 4)
#define BTRFS_INODE_SYNC (1 << 5)
#define BTRFS_INODE_IMMUTABLE (1 << 6)
#define BTRFS_INODE_APPEND (1 << 7)
#define BTRFS_INODE_NODUMP (1 << 8)
#define BTRFS_INODE_NOATIME (1 << 9)
#define BTRFS_INODE_DIRSYNC (1 << 10)
#define BTRFS_INODE_COMPRESS (1 << 11)
到目前为止最开始的两个疑问都可以被解答了
- 挂载的错误是什么意思?
群晖的 btrfs
元信息在内核校验下不合法
- 为什么老内核可以挂载,而新内核不可以?
因为老内核不校验这些元信息
- 接下来只剩最后一个问题,怎么让新内核和老内核一样挂载成功?
我想到三种思路(衡量自身能力和风险,我选择1)
- 1、改内核
btrfs
的代码(跳过报错的校验),替换编译内核模块
- 2、修改磁盘的
brtfs
元信息的 flag
,替换为内核可以识别不报错的 flag
- 3、等飞牛官方改内核代码兼容
编译内核的方法粗略步骤如下(单独编译模块,甚至不用重启):
以我的飞牛OS为例,0.9.18
版本内核为 6.12.18-trim
1、ssh
连接飞牛
2、下载 6.12.18
内核源码并解压
3、apt
安装所需工具
4、修改代码,跳过报错的校验(注释、兼容随你。**过海,各显神通)
5、编译内核(也可以先编译后改代码,再单独编译一次修改的文件即可)
6、编译出模块文件,拿到编译后的 brtfs.ko
7、卸载当前所有的挂载,用 modprobe
卸载 btrfs
模块
8、备份旧模块(/lib/modules/$(uanme -r)/kernel/fs/btrfs/brtfs.ko
),并将你编译后的 brtfs.ko
替换
9、用 modprobe
重启 btrfs
模块
10、随便把群晖挂载到哪个目录后,修改群晖文件夹的用户和用户组,即可正常使用
纯linux菜鸟,以上均为摸索经验,大佬请斧正
关于彩蛋:为什么删除 btrfs
的群晖相关 subvolume
后,错误发生变化了?
需要先解答另一个问题,最开头的错误还有没解释的
# kernel 5.4.0
BTRFS critical (device md3) corrupt leaf: root = 1 block=xxxxxx slot= xxx, invalid root flag, have 0x400000000 expect mask 0x1000000000001
这个错误是因为 check_leaf_item
的校验逻辑在后续内核版本又得到加强,新增 check_root_item
校验报的错,校验出 root item
的 flag
不合法
貌似等价于检查brtfs subvolume的flag
# kernel 5.4.0
static int check_root_item(struct extent_buffer *leaf, struct btrfs_key *key,
int slot)
{
.....
// 另一个报错信息的源头
if (btrfs_root_flags(&ri) & ~valid_root_flags) {
generic_err(leaf, slot,
"invalid root flags, have 0x%llx expect mask 0x%llx",
btrfs_root_flags(&ri), valid_root_flags);
return -EUCLEAN;
}
return 0;
}
static int check_leaf_item(struct extent_buffer *leaf,
struct btrfs_key *key, int slot,
struct btrfs_key *prev_key)
{
int ret = 0;
struct btrfs_chunk *chunk;
switch (key->type) {
.....
// 以下均为新增校验
case BTRFS_ROOT_ITEM_KEY:
// 报错的校验入口
ret = check_root_item(leaf, key, slot);
break;
case BTRFS_EXTENT_ITEM_KEY:
case BTRFS_METADATA_ITEM_KEY:
ret = check_extent_item(leaf, key, slot);
break;
case BTRFS_TREE_BLOCK_REF_KEY:
case BTRFS_SHARED_DATA_REF_KEY:
case BTRFS_SHARED_BLOCK_REF_KEY:
ret = check_simple_keyed_refs(leaf, key, slot);
break;
case BTRFS_EXTENT_DATA_REF_KEY:
ret = check_extent_data_ref(leaf, key, slot);
break;
}
return ret;
}
事后分析,有可能我在 brtfs
里的一通删除操作,把不合法的 subvolume/root item
全给删了,所以这部分校验反而能通过了。后续在更高版本内核也没有再报过这个错
遇到了这个报错的人,不要学习我的乱删一桶。而应该同样考虑是否跳过 root_item
对 flag
的校验,所以在修改代码时 root
和 inode
的校验都需要注意,关键看自身的报错信息
PS:以上内容如果有人发过类似的文章,大家请无视本帖