Linux dm-switch:device-mapper 动态路径切换目标完全指南
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,
};
与 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_table,L77-L125)按以下规则确定位宽:
- 表项位宽
region_table_entry_bits从 1 bit 起,直到1 << bits >= nr_paths。换言之,为表示nr_paths条路径,取能容纳它的最小 bit 数——例如 3 条路径需要 2 bit,16 条路径正好 4 bit。 - 每个 slot 是
unsigned long(64 位平台上为 64 bit),region_entries_per_slot = 64 / entry_bits,例如 4 bit 表项时一个 slot 装 16 个区域。 - 整个 region table 按 slot 数用
vmalloc_array分配;若设备过大,报错"Region table too large"。
因此文档中所说的“16 成员组每个地址区间只用 4 bit、密度远超 dm table 的 B 树”正是由此实现直接支撑的。同时,当 region_size 为 2 的幂时 region_size_bits 记录其对数,查找时用移位代替除法加速(见 switch_get_position、switch_get_path_nr,L127-L173)。
初始状态:round robin
initialise_region_table(L190-L203)在创建设备时把所有区域按 0,1,2,…,nr_paths-1,0,1,… 的轮转模式填满,也就是说即使不做任何配置,设备刚创建时也是可用且均衡的,用户再按需用消息覆盖个别区域。
I/O 下发调用链
每条 bio 到达时执行 switch_map(L320-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 = 1(L310),注释表明对于 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_mappings(L374-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_paths、region_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/switch0、vg1/switch1、vg1/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-switch(drivers/md/Makefile:
obj-$(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 表达不了”时,用紧凑位图做动态路径选择。 使用与理解时请记住以下要点:
- 区域单位是 512 字节扇区,构造时算好
<region_size>;位宽自动取能容纳num_paths的最小 bit 数,16 成员下每区域仅 4 bit。 - 通常与 dm-multipath 搭配成上下两层:上层 switch 决定成员,下层 multipath 在成员内多路径间做负载均衡与故障切换。
- 创建后初始映射为 round robin,天然均衡;之后随时用
set_region_mappings热更新单个或整段区域的路由,消息内 index/path_nr/R 参数一律使用无前缀十六进制。 R<n>,<m>是省流量的重复加载语法,适合周期性映射;dmsetup status不返回内容属于预期行为。- target 依赖内核
CONFIG_DM_SWITCH(EXPERIMENTAL),模块名dm-switch,版本为 1.1.0。
结合本仓库内 switch.rst、dm-switch.c 以及 DM 目标家族的 索引页,可以继续横向对比 striped、multipath、linear 等目标,理解 device-mapper 生态在不同映射诉求下的设计取舍。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00