收起左侧

【抛砖引玉】群晖磁盘直接挂载飞牛的问题分析和解决方法

0
回复
28
查看
[ 复制链接 ]

1

主题

0

回帖

0

牛值

江湖小虾

本文不是严格意义上的教程,为折腾的经验分享【涉及编译内核模块,折腾有风险,谨慎操作】

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相关的元信息,而报错的部分则是在校验 inodeflag信息是否合法
而群晖上的 inodeflag无法通过新版本的校验

// 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 itemflag不合法
貌似等价于检查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_itemflag的校验,所以在修改代码时 rootinode的校验都需要注意,关键看自身的报错信息

PS:以上内容如果有人发过类似的文章,大家请无视本帖

收藏
送赞
分享
唔系一只🦆
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则