首页
/ Linux dm-switch:device-mapper 动态路径切换目标完全指南

Linux dm-switch:device-mapper 动态路径切换目标完全指南

2026-09-07 22:12:00作者:袁立春Spencer

dm-switch 是 Linux device-mapper 家族中的一员,它创建一个“支持在固定路径集合上按固定尺寸 I/O 区域做任意映射”的设备,并允许在运行期通过消息动态切换任意区域所走的后端路径。本文以内核文档 switch.rst 为主体,结合 dm-switch.c 源码实现与内核配置,系统讲解它的诞生背景、位图索引工作原理、构造参数、运行时消息机制以及端到端实操示例,帮助你理解它适用于什么场景、如何构建与使用、底层又是如何工作的。

一、dm-switch 是什么

dm-switch 的目标是:在大规模固定尺寸地址区域没有简单规律、无法用 dm-stripe 之类的紧凑表项表达时,依然能高效、低内存地把 I/O 映射到底层块设备

一个区域(region)是设备上固定大小的扇区区间(默认按 512 字节扇区计),每个区域可以被独立地重定向到任意一条后端路径(path);这个映射关系在设备运行期间可以通过向 target 发送 message 来动态修改。

从源码角度看,它注册为一个名为 switch、版本 {1, 1, 0}、带 DM_TARGET_NOWAIT 特性的 device-mapper 目标:

static struct target_type switch_target = {
	.name = "switch",
	.version = {1, 1, 0},
	.features = DM_TARGET_NOWAIT,
	.module = THIS_MODULE,
	.ctr = switch_ctr,
	.dtr = switch_dtr,
	.map = switch_map,
	.message = switch_message,
	.status = switch_status,
	.prepare_ioctl = switch_prepare_ioctl,
	.iterate_devices = switch_iterate_devices,
};

(见 drivers/md/dm-switch.c

与 dm-stripe / 通用 DM table 的差异

  • dm-stripe 能表达规则条纹分布,但映射必须“有简单规律可循”,才能用紧凑的参数描述;
  • 通用 device-mapper table 也能把设备不同区间映射到不同目标,但当区间数量达到百万级时(地址区域尺寸为数十 MB、LUN 横跨大量成员时很容易出现),一张表会产生超过一百万条表项,B 树化表结构将占用太多内存;
  • dm-switch位图代替表项集合:每个区域在 region table 中只占一个很小的位段,16 成员存储组中每个区域仅 4 bit,地址密度远高于 dm table,从而在区域数量极大时仍能保持紧凑的内存占用。

二、背景:分布式 frameless 存储架构催生的“两层设备层级”

dm-switch 最初源自 Dell EqualLogic 等 iSCSI 存储阵列的使用场景。这类存储系统采用分布式无框(frameless)架构:一个存储组(storage group)由多个彼此独立的存储阵列“成员”(member)组成,每个成员拥有独立控制器、磁盘与网卡。创建 LUN 时,数据被铺到多个成员上,但对外只暴露一个目标发现门户(discovery portal),对发起端隐藏了具体分布细节。

当 initiator 建立 iSCSI 会话时,每个会话只连到某一成员的某个网口;访问某个 LUN 的数据可以通过任意 iSCSI 会话发出,若目标块位于其他成员,则由阵列内部转发(对发起端透明)。同时存储布局是动态的,块会随负载均衡需求在成员间搬移。

在这种架构下做多路径(multipathing)配置时,host 与 target 双方可以用多个网口建立多条 iSCSI 会话以聚合带宽。最简单粗暴的做法是用 round robin 把 I/O 均匀撒到所有路径上、靠存储阵列内部转发,但如果能把数据直接发到真正拥有该块数据的那个成员,网络与转发开销会更小,性能更好。

Upper / Lower 两层设备

dm-switch 文档给出的方案是构建一个两层设备层级:

  • Upper Tier(上层)——决定把 I/O 发给哪个成员:由单个 dm-switch 设备构成,内部用位图查表,为每个地址区域选定正确的下层设备。
  • Lower Tier(下层)——负载均衡到某一成员的多个路径:为每个成员建立一个 dm multipath 设备;每个 multipath 设备把直达该成员的路径放进一个主优先组(priority group),借助既有 path selector 在这些路径间做负载均衡;同时额外构造一个非首选优先组,放入通往其他成员的路径,作为故障切换(failover)备用路径。
              +------------------------------+
              |   dm-switch(Upper Tier)      |
              |   位图查表 → 选择正确成员      |
              +------+-------+-------+-------+
                     |       |       |
        +------------+       |       +------------+
   dm-mpath member0   dm-mpath member1    dm-mpath member2
   主组: 直达路径          直达路径            直达路径
   备组: 到其他成员路径(故障切换)

三、工作机理:位图 region table 与 I/O 重映射

区域表的位宽设计

核心数据结构在 drivers/md/dm-switch.c

struct switch_ctx {
	struct dm_target *ti;
	unsigned int nr_paths;			/* 路径总数 */
	unsigned int region_size;		/* 以 512 字节扇区为单位的区域大小 */
	unsigned long nr_regions;		/* 组成设备的区域总数 */
	signed char region_size_bits;		/* log2(region_size) 或 -1 */
	unsigned char region_table_entry_bits;	/* 单个区域表项占用的 bit 数 */
	unsigned char region_entries_per_slot;	/* 每个 slot(unsigned long)放几个表项 */
	region_table_slot_t *region_table;	/* 区域表本体 */
	struct switch_path path_list[];
};

构造时(alloc_region_tableL77-L125)按以下规则确定位宽:

  1. 表项位宽 region_table_entry_bits 从 1 bit 起,直到 1 << bits >= nr_paths。换言之,为表示 nr_paths 条路径,取能容纳它的最小 bit 数——例如 3 条路径需要 2 bit,16 条路径正好 4 bit。
  2. 每个 slot 是 unsigned long(64 位平台上为 64 bit),region_entries_per_slot = 64 / entry_bits,例如 4 bit 表项时一个 slot 装 16 个区域。
  3. 整个 region table 按 slot 数用 vmalloc_array 分配;若设备过大,报错 "Region table too large"

因此文档中所说的“16 成员组每个地址区间只用 4 bit、密度远超 dm table 的 B 树”正是由此实现直接支撑的。同时,当 region_size 为 2 的幂时 region_size_bits 记录其对数,查找时用移位代替除法加速(见 switch_get_positionswitch_get_path_nrL127-L173)。

初始状态:round robin

initialise_region_tableL190-L203)在创建设备时把所有区域按 0,1,2,…,nr_paths-1,0,1,… 的轮转模式填满,也就是说即使不做任何配置,设备刚创建时也是可用且均衡的,用户再按需用消息覆盖个别区域。

I/O 下发调用链

每条 bio 到达时执行 switch_mapL320-L330):

static int switch_map(struct dm_target *ti, struct bio *bio)
{
	struct switch_ctx *sctx = ti->private;
	sector_t offset = dm_target_offset(ti, bio->bi_iter.bi_sector);
	unsigned int path_nr = switch_get_path_nr(sctx, offset);

	bio_set_dev(bio, sctx->path_list[path_nr].dmdev->bdev);
	bio->bi_iter.bi_sector = sctx->path_list[path_nr].start + offset;

	return DM_MAPIO_REMAPPED;
}

流程为:取 bio 在 dm-switch 设备内的相对扇区 offset → 由 offset / region_size 定位区域号 → 读 region table 得到 path_nr → 把 bio 改写到该路径的块设备上,并将扇区加上该路径的 start 偏移 → 以 DM_MAPIO_REMAPPED 同步完成重映射(这是纯 I/O 路径,不涉及线程调度)。

细节上,表项读取/写入使用 READ_ONCE/WRITE_ONCE;若出现 path_nr >= nr_paths 的异常(例如处理器非原子写场景),会安全回退到 path 0(见 switch_get_path_nr 中的注释)。而所有修改映射的消息都在 message_mutex 串行化下执行,避免并发改写撕裂表项(switch_message)。

十六进制解析的微优化

因为 set_region_mappings 消息可能一次性下发海量条目,解析速度至关重要。dm-switch.c 用一个 256 项的 hex_table 查表法解析十六进制。源码注释给出的实测对比(加载 1,000,000 条映射)表明查表法比条件分支解析更快:PA-RISC 上 0.29s vs 0.31s,Opteron 上 0.0495s vs 0.0498s。这说明消息处理路径是经过性能考量的热点代码。

四、构造参数:构建一个 dm-switch 设备

dmsetup 表的构造参数语法如下:

<num_paths> <region_size> <num_optional_args> [<optional_args>...] [<dev_path> <offset>]+
参数 含义 源码约束/说明
<num_paths> 用于分发 I/O 的路径总数 至少为 1;上限受路径结构体内存分配约束(见 switch_ctr_args 校验),解析错误报 "Invalid number of paths"
<region_size> 一个区域包含的 512 字节扇区数;每个区域可被重定向到任意可用路径 合法范围为 1..UINT_MAX("Invalid region size"),并通过 dm_set_target_max_io_len(ti, region_size) 把单次下发 I/O 的最大长度限制为 region_size,保证单个 bio 不跨区域
<num_optional_args> 可选参数个数 目前内核不支持任何可选参数,必须为 0(校验区间 {0,0}),预留扩展空间
<dev_path> 代表某条路径的底层块设备 dm_get_device 打开(parse_path);参数总数必须严格等于 num_paths * 2,否则报 "Incorrect number of path arguments"
<offset> 该路径上数据起始位置相对块设备起点的偏移,单位 512 字节扇区 以十进制解析(kstrtoull(…, 10, …)),转发请求时累加到扇区号上(path_list[path_nr].start + offset),通常为 0

需要注意:<region_size><offset> 都以 512 字节扇区计,不是字节。例如一个 64 KiB 区域 = 128 扇区。

另外,构造器内部将 ti->num_discard_bios = 1L310),注释表明对于 UNMAP(discard),把请求送到任意一条路径即可,无需逐区域查表。设备总长度则由 dmsetup 表中 "<起始扇区> <长度扇区>" 决定,不受路径数影响。

五、消息机制:set_region_mappings

dm-switch 目前只支持一条消息(其余消息会打印 "Unrecognised message received." 并返回 -EINVAL),格式为:

set_region_mappings <index>:<path_nr> [<index>]:<path_nr> [<index>]:<path_nr>...

用于修改区域表——指定哪些区域重定向到哪些路径。各字段语义:

  • <index>:区域编号(区域尺寸即构造参数中的 <region_size>)。若省略 index(写成 :path_nr),则使用“上一个区域 + 1”。以十六进制书写,且不带任何 0x 前缀。
  • <path_nr>:路径编号,范围 0 … (<num_paths> - 1)同样为无前缀十六进制。
  • R<n>,<m>:批量重复参数,<n><m> 均为十六进制数。语义是把最近写入的 <n> 条映射重复到接下来的 <m> 个区域槽位,用于快速加载具有周期性的映射表,避免手工逐条书写。

解析与合法性校验(源码视角)

process_set_region_mappingsL374-L463)对每条参数的处理如下:

  • 若参数以 R/r 开头(源码用 *string & 0xdf 做大小写归一),解析 R<n>,<m>,要求:
    • <n>(cycle_length)非零,且 cycle_length - 1 <= 当前 region_index,即必须有足够的历史映射可供复制
    • 复制不越界:region_index + m < nr_regions
    • 复制时逐个推进 region_index,把 region_index - n 处的旧映射值写到当前位置——因每次写入会被后续循环读到,因此能无缝衔接成连续重复的模式。
  • 若参数以 : 开头:region_index 自增 1(隐式 index)。
  • 否则:用查表法把前缀解析成 region_index,再要求紧跟 :
  • : 之后解析 path_nr,并校验 path_nr < nr_pathsregion_index < nr_regions,任一不满足都会打印 DMWARN 并整体返回 -EINVAL(整条消息被拒绝,不做部分生效)。
  • 整条消息在 message_mutex 保护下一次执行完,保证与并发 bio 查表 / 并发消息互斥。

因此书写时请务必留意:十六进制不要0x#h 后缀;path_nr 一旦超过 num_paths-1 整条消息会被拒绝。

六、Status 行为

文档明确指出:dm-switch 不汇报 status 行(No status line is reported)。

源码对三种 status 类型的行为分别为(switch_status):

  • STATUSTYPE_INFO(对应 dmsetup status):结果为空字符串;
  • STATUSTYPE_IMA:同样为空;
  • STATUSTYPE_TABLE(对应 dmsetup table):输出构造参数 "<num_paths> <region_size> 0" 后跟随每组 <dev_name> <offset>,可供重新载入表使用。

所以在实际排障中,想看当前映射明细不能用 dmsetup status,而应结合 dmsetup table 查看构造配置,映射本身的变化则通过消息语义自己跟踪(或在下发 set_region_mappings 前用 dmsetup message 的设备号确认对象正确)。

七、端到端实操示例

下面沿用 switch.rst 中的示例,假设你有三个大小相同的卷 vg1/switch0vg1/switch1vg1/switch2

1. 创建 64 KiB 区域大小的 switch 设备

blockdev --getsz 返回设备以 512 字节扇区计的大小,直接用作表长度;64 KiB = 128 扇区,所以第三个参数是 128:

dmsetup create switch --table "0 `blockdev --getsz /dev/vg1/switch0`
	switch 3 128 0 /dev/vg1/switch0 0 /dev/vg1/switch1 0 /dev/vg1/switch2 0"

解释构造参数:3(三条路径)128(区域 128 扇区 = 64 KiB)0(无可选参数),然后三组 <dev_path> <offset> 都从偏移 0 开始。创建成功后 device-mapper 会先按 round robin 填充初始区域表,因此此时无需任何消息即可直接读写。

2. 设置前 7 个区域的定向映射

把前 7 个区域(编号 0..6)依次指到 switch0、switch1、switch2、switch0、switch1、switch2、switch1:

dmsetup message switch 0 set_region_mappings 0:0 :1 :2 :0 :1 :2 :1

第一条 0:0 显式给出区域号 0 → 路径 0;后续条目省略 index,内核会自动依次使用 1、2、3……因此上面写出的路径序列正好对应区域 0~6。

3. 用 R, 加载重复周期

命令:

dmsetup message switch 0 set_region_mappings 1000:1 :2 R2,10

先把区域 1000 指向路径 1、区域 1001 指向路径 2,然后 R2,10 表示“把最后 2 条映射(路径 1、2 这一对)重复 10 次”。它完全等价于下面逐条展开(注意所有数字依旧是十六进制):

dmsetup message switch 0 set_region_mappings 1000:1 :2 :1 :2 :1 :2 :1 :2 \
	:1 :2 :1 :2 :1 :2 :1 :2 :1 :2

也就是说,区域 1002~1011 会得到 1、2、1、2… 的交替映射。用 R 模式批量配置规律性较强的映射可以显著减少消息体积与解析开销。

4. 常用配套操作

  • 查看当前表配置:dmsetup table switch
  • 查看状态(INFO 为空符合预期):dmsetup status switch
  • 删除设备:dmsetup remove switch

八、内核配置、编译与模块加载

dm-switch 的内核配置项位于 drivers/md/Kconfig

config DM_SWITCH
	tristate "Switch target support (EXPERIMENTAL)"
	depends on BLK_DEV_DM
  • 依赖 BLK_DEV_DM(device-mapper 框架本体);
  • 标记为 EXPERIMENTAL,可编译为 y(内建)或 m(模块);
  • 模块名为 dm-switchdrivers/md/Makefileobj-$(CONFIG_DM_SWITCH) += dm-switch.o);
  • 源码顶部 MODULE_LICENSE("GPL")MODULE_DESCRIPTION(DM_NAME " dynamic path switching target"),模块作者来自 Dell 与 Red Hat。

若以模块方式使用,加载/卸载:

modprobe dm-switch     # 或 insmod dm-switch.ko

随后即可用 dmsetup create 按上文语法构造设备。要确认 target 是否注册成功,可查看 /proc/misc 之外的 device-mapper target 列表(如 dmsetup targets 应能看到 switch v1.1.0)。

九、ioctl 与设备尺寸的配合

dm-switch 会把 ioctl 透传到区域 0 所映射路径对应的块设备,用于处理 getgeo、BLKSSZGET 等命令(switch_prepare_ioctl)。但透传有严格前提:仅当 dm-switch 设备长度与该路径数据区大小精确一致ti->len + start == bdev_nr_sectors(bdev))时才放行,否则拒绝透传,避免向一个大小不匹配的底层设备下发越界语义的 ioctl。这也是为什么示例中要求三个底层卷“大小相同”、且表长度取卷的真实扇区数——构造时各路径的尺寸关系会直接影响上层设备的管理行为。

十、适用场景与要点回顾

dm-switch 的核心价值可以用一句话概括:当“区域多到 DM table 装不下”且“映射无规则到 dm-stripe 表达不了”时,用紧凑位图做动态路径选择。 使用与理解时请记住以下要点:

  1. 区域单位是 512 字节扇区,构造时算好 <region_size>;位宽自动取能容纳 num_paths 的最小 bit 数,16 成员下每区域仅 4 bit。
  2. 通常与 dm-multipath 搭配成上下两层:上层 switch 决定成员,下层 multipath 在成员内多路径间做负载均衡与故障切换。
  3. 创建后初始映射为 round robin,天然均衡;之后随时用 set_region_mappings 热更新单个或整段区域的路由,消息内 index/path_nr/R 参数一律使用无前缀十六进制
  4. R<n>,<m> 是省流量的重复加载语法,适合周期性映射;dmsetup status 不返回内容属于预期行为。
  5. target 依赖内核 CONFIG_DM_SWITCH(EXPERIMENTAL),模块名 dm-switch,版本为 1.1.0。

结合本仓库内 switch.rstdm-switch.c 以及 DM 目标家族的 索引页,可以继续横向对比 striped、multipath、linear 等目标,理解 device-mapper 生态在不同映射诉求下的设计取舍。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391