首页
/ dm-clone:Linux 内核 device-mapper 按需克隆 target 的原理剖析与 dmsetup 实战

dm-clone:Linux 内核 device-mapper 按需克隆 target 的原理剖析与 dmsetup 实战

2026-09-07 14:28:11作者:咎竹峻Karen

dm-clone 是 Linux 内核 device-mapper 子系统中的一个 target,它能把一个只读的源设备在后台逐步“水合”(hydrate)复制到可写的目标设备上,同时立刻呈现一个完整可用的虚拟块设备。本指南以 Documentation/admin-guide/device-mapper/dm-clone.rst 为主线,结合 dm-clone 主实现元数据实现内核 Kconfig 配置,完整讲解其子设备与区域模型、读写重定向语义、后台水合机制、构造/状态/消息接口,并给出可直接照做的文件系统克隆实战流程。读完你将能够配置内核、用 dmsetup 建表并解释其输出,理解这个 target 相较 dm-cache、dm-snapshot、dm-mirror 等方案的取舍。

dm-clone 是什么:立即可用的“边还原边访问”

dm-clone 产生的是一个现有只读源设备到可写目标设备的一对一拷贝。它对外呈现一个虚拟块设备,所有数据看起来“立即出现”,而底层真正执行的是:

  • 未拷贝区域 → 读直接走源设备、写则先触发该区域水合再落到目标设备;
  • 已拷贝区域 → 读写全部落在目标设备;
  • 拷贝在后台与用户 I/O 并行进行。

其最典型的使用场景,是把一个可能远程、高延迟、只读、归档型的块设备克隆到本地、快速、主用型设备上做低延迟 I/O:例如通过 NBD、Fibre Channel、iSCSI、AoE 等网络存储协议访问只读备份,将其恢复到本地 SSD/NVMe,且无需等待恢复完成即可挂载使用。一旦水合全部完成,就可以卸下 dm-clone 表,换成直接映射到目标设备的 linear 表。

内核 Kconfig 中该功能定位为 config DM_CLONEtristate "Clone target (EXPERIMENTAL)"depends on BLK_DEV_DMselect DM_PERSISTENT_DATA),见 drivers/md/Kconfig。target 的实现位于 drivers/md/dm-clone-target.c(其中以 target_type clone_target = { .name = "clone", .version = {1, 0, 0}, ... } 注册,见 第 2158 行附近),元数据复用 thin-provisioning 所用的 persistent-data 库,由 drivers/md/dm-clone-metadata.c头文件 承载。

核心术语:Hydration(水合)

文档给出了一个精确定义的关键词:

Hydration:把目标设备中某个区域填充上源设备对应区域数据的过程,也就是把该区域从源设备拷贝到目标设备。

一旦某个区域被水合,后续针对它的所有 I/O 都会被重定向到目标设备。这个"区域是否已水合"的位图状态,正是元数据设备要持久化记录的核心内容。

设计:三个子设备 + 固定大小区域

子设备三件套

target 构造时接收三个块设备:

  1. 源设备(source dev):只读、被克隆并作为水合数据来源的设备。内核侧打开时以 BLK_OPEN_READ 方式打开,见 parse_source_dev()drivers/md/dm-clone-target.c#L1711-L1723),从机制上保证了源设备始终只读;
  2. 目标设备(destination dev):水合的目的地,最终成为源设备克隆。以读写方式打开(parse_dest_dev());
  3. 元数据设备(metadata dev):一块相对较小的快速设备,记录哪些区域在目标设备上已经有效(即已被水合,或经由用户 I/O 直接写入覆盖)。同样以读写方式打开(parse_metadata_dev())。

约束:目标设备大小必须至少等于源设备大小。从代码看,region 总数由 ti->len(表行长度,也就是虚拟设备/目标的大小)向上取整除 region size 得到(clone->region_shift = __ffs(...)nr_regions = dm_sector_div_up(ti->len, clone->region_size),见 构造函数中段),因此表行长度必须不超过目标设备容量,同时不小于源设备大小,才能保证全部源数据都有落脚之处。

元数据设备的大小也被限定:DM_CLONE_METADATA_MAX_SECTORS_WARNING 定义为 16GiB,超过该尺寸的设备会触发内核警告 "Metadata device ... is larger than 16GB: excess space will not be used"drivers/md/dm-clone-metadata.hparse_metadata_dev())。

区域(Region):水合的基本单位

dm-clone 把源、目标设备划分为固定大小的区域。区域是水合的粒度单位——即单次从源到目标拷贝的最小数据量。对应地,内核侧设置了 dm_set_target_max_io_len(ti, clone->region_size)构造函数),把发给该 target 的 I/O 按 region 边界进行切分管理。

区域大小在首次创建 dm-clone 设备时配置,此后不可变更:

  • 推荐取文件系统块大小,通常即 4KB;
  • 合法范围:最小 8 扇区(4KB,MIN_REGION_SIZE (1 << 3))到 2097152 扇区(1GB,MAX_REGION_SIZE (1 << 21)),且必须是 2 的幂,这些常量定义于 dm-clone-target.c 头部
  • 由于 region 位图用 bitset 维护且受 test_bit 限制,region 总数不能超过 2^31,否则返回 "Too many regions. Consider increasing the region size",见 validate_nr_regions()dm-clone-target.c#L1663-L1675)。

I/O 重定向语义:已水合/未水合两条路径

文档描述并经 clone_map()dm-clone-target.c#L1314-L1371)确认的读写规则为:

bio 情形 处理方式
读写命中已水合区域 全部由目标设备提供服务(remap 到 dest)
读命中未水合区域 直接重定向到源设备读取(remap_to_source
写命中未水合区域 延迟该写,立即启动该区域水合,水合完成后再把延迟的 bio 下发到目标设备
一次整区域大小的写 跳过从源的拷贝(overwrite),直接把该区域写入目标设备并标记为水合

对“整区域覆盖写”,实现上有专门路径:is_overwrite_bio() 检测 bio 覆盖整个 region 后走 hydration_overwrite(),直接改写目标区域并更新元数据,避免无谓地从源设备拷贝将被覆盖的数据(见 hydration_overwrite()complete_overwrite_bio())。

延迟写由目标侧工作队列驱动:未水合区域的写 bio 会进入该 region 水合任务关联的 deferred 队列,区域水合成功后由 issue_deferred_bios() 统一提交(对应 issue_deferred_bios())。

Discard:把“不要的数据”变成免费水合

dm-clone 对 discard(trim) 有专门优化语义:如果一个 discard 请求落在尚未水合的范围,它会被当作"跳过这些区域水合"的提示——即不把数据从源拷贝到目标,只把元数据里的区域标记为已水合。代码注释同样点明:"dm-clone interprets discards and performs a fast hydration of the discarded regions, i.e., we skip the copy from the source device and just mark the regions as hydrated"(见 clone_map())。

这正是实践中"先 mount + fstrim 再开启后台水合"这一节流手段能生效的根本原因:文件系统发来的 discard 让 dm-clone 自动跳过对空余空间的拷贝。默认情况下,如果目标设备支持 discard,dm-clone 会把 discard 继续向下传给目标设备(feature 参数 no_discard_passdown 可关闭该行为,代码中对应 DM_CLONE_DISCARD_PASSDOWN 标志位)。

后台水合:持续拷贝与 I/O 感知的节流

  • dm-clone 持续从源设备向目标设备拷贝,直到整块设备拷完;
  • 拷贝会占用带宽,用户可设置节流阈值,防止任意时刻超过限定数量的区域同时在拷;
  • dm-clone 会感知用户 I/O:有 I/O in-flight 时暂停后台水合。对应内核行为是完成一批水合后,仅当 DM_CLONE_HYDRATION_ENABLEDios_in_flight == 0 时才唤醒 worker 继续(见 hydration_kcopyd_callback());
  • 消息 hydration_threshold <#regions> 设置同时拷贝的区域数上限,默认 1 个 region(DEFAULT_HYDRATION_THRESHOLD 1);
  • 实际拷贝由 dm-kcopyd 执行,默认单次拷贝请求大小等于 region size。消息 hydration_batch_size <#regions> 可调节拷贝请求大小:增大 batch size 会让 dm-clone 把相邻的连续区域批量合起来一次拷贝(默认 1,DEFAULT_HYDRATION_BATCH_SIZE 1)。对应实现 __batch_hydration()/hydration_copy() 会把 from/to 两个 dm_io_region 拼接成更大拷贝交给 dm_kcopyd_copy()(见 hydration_copy())。

此外目标侧还导出了模块参数 clone_hydration_throttle("A percentage of time allocated for hydrating regions",见 模块头部),可从模块层限制水合所占的时间比例,作为带宽控制的另一道闸门。

当目标设备水合完成时,dm-clone 会向用户空间发送一个 dm event,用于通知管理程序"拷贝完毕,可以切表"。

元数据落盘与崩溃一致性语义

关于 on-disk 元数据的提交策略,文档与代码给出的机制高度一致:

  • 每次收到 FLUSH 或 FUA bio 都提交一次元数据bio_triggers_commit() 判断,flush/FUA bio 会先等元数据提交完成再放行,见 clone 结构中 deferred_flush_bios 的设计注释);
  • 若期间没有任何这类请求,则约每 1 秒自动提交一次COMMIT_PERIOD HZ /* 1 sec */,由 do_waker 周期性唤醒,见 do_waker());
  • 由此,dm-clone 设备的行为类似带易失写缓存的物理磁盘:掉电可能丢失少量最近的写入,但元数据在任何崩溃场景下始终一致

这也意味着它不是严格的"每笔写入都同步落元数据"的快照语义——这正是它能做到比 dm-snapshot 更高性能的关键(见下文设计取舍)。

Target 接口:Constructor 参数

构造语法(文档原文,扇区为单位):

clone <metadata dev> <destination dev> <source dev> <region size>
      [<#feature args> [<feature arg>]* [<#core args> [<core arg>]*]]
位置参数 含义
metadata dev 保存持久化元数据的快速设备
destination dev 水合目标设备,源将被克隆到这里
source dev 只读的源数据设备
region size 区域大小,单位:扇区
#feature args 后续 feature 参数个数
feature args 取值 no_hydrationno_discard_passdown
#core args 后续 core 参数个数,必须是偶数(key/value 对)
core args 传给 dm-clone 的 key/value 对,例如 hydration_threshold 256

可选 Feature 参数

feature 参数 作用
no_hydration 创建后台水合被禁用的实例(可用 message enable_hydration 稍后开启)
no_discard_passdown 禁止把 discard 下传到目标设备

可选 Core 参数

core 参数 作用
hydration_threshold <#regions> 后台水合期间,任意时刻最多同时从源拷向目标的 region 数
hydration_batch_size <#regions> 后台水合期间,把相邻连续 region 拼成每批这么多 region 一次拷贝

参数解析在 parse_feature_args()parse_core_args() 中逐项完成;core 参数要求偶数个以保证 key/value 配对(文档中也强调 "An even number of arguments corresponding to key/value pairs")。若既想建实例时立即干活、又想调整并发与批量,可在建表时传入如 2 hydration_threshold 256 hydration_batch_size 64 这样的 core 参数段。

Target 接口:Status 输出

dmsetup status(或 dmsetup table)会返回如下格式的字符串:

<metadata block size> <#used metadata blocks>/<#total metadata blocks>
<region size> <#hydrated regions>/<#total regions> <#hydrating regions>
<#feature args> <feature args>* <#core args> <core args>*
<clone metadata mode>

各字段说明(对照文档):

字段 含义
metadata block size 每个元数据块的固定大小(扇区)
#used metadata blocks 已使用的元数据块数
#total metadata blocks 元数据块总数
region size 设备配置的区域大小(扇区)
#hydrated regions 已完成水合的区域数
#total regions 需要水合的区域总数
#hydrating regions 当前正在水合的区域数
#feature args 其后 feature 参数个数
feature args feature 参数,例如 no_hydration
#core args 其后 core 参数个数(偶数)
core args 调优用 key/value 对,例如 hydration_threshold 256
clone metadata mode 元数据模式:ro(只读)或 rw(读写)

关于元数据模式还有一个重要的故障语义:在严重情况下,即便只读模式也被判定不安全,则不再允许任何 I/O,status 将只包含字符串 Fail;一旦元数据模式发生变化,会向用户空间发送 dm event。这些模式对应代码中的 enum clone_metadata_mode { CM_WRITE, CM_READ_ONLY, CM_FAIL } 以及 __set_clone_mode()/__metadata_operation_failed() 相关逻辑(dm-clone-target.c#L60-L65)。

#hydrated regions/#total regions 两个计数非常适合用来监控水合进度(例如脚本轮询 dmsetup status clone,二者相等即代表拷贝完成,随后会收到 dm event)。

Target 接口:Messages(运行时控制)

dmsetup message <device> <sector> <message> 支持以下运行时控制命令(实现见 clone_message()):

message 作用
disable_hydration 禁用目标设备的后台水合
enable_hydration 启用(或重新启用)后台水合
hydration_threshold <#regions> 设置后台水合并发阈值
hydration_batch_size <#regions> 设置后台水合批量大小

注意两个细节:设置 hydration_threshold 0 会暂停水合,之后一旦调大阈值,worker 会被唤醒以继续水合(见 set_hydration_threshold() 的注释与 实现);enable_hydration/disable_hydration 通过置位/清位 DM_CLONE_HYDRATION_ENABLED 标志实现(enable/disable_hydration())。

实战:克隆一个含文件系统的设备

沿用文档的完整示例,假设源设备是一块含文件系统、经过网络协议暴露的只读块设备。核心套路是:先建表但不水合 → 挂载并 fstrim 让 discard 跳过空闲空间 → 再开启后台水合 → 水合完成后切为 linear 表

1. 创建 dm-clone 设备(先禁用后台水合)

dmsetup create clone --table "0 1048576000 clone $metadata_dev $dest_dev \
  $source_dev 8 1 no_hydration"

表行字段拆解:

  • 0:起始扇区,从设备头开始映射;
  • 1048576000:映射长度(扇区),应等于目标设备可容纳、且不小于源设备的扇区数,请按自己设备的实际大小替换(示例值 1048576000 扇区约对应 512GB 量级,仅作示意);
  • clone:target 类型名;
  • $metadata_dev $dest_dev $source_dev:按构造顺序为元数据设备、目标设备、源设备;
  • 8:region size = 8 扇区 = 4KB(文档推荐值,与文件系统块大小一致);
  • 1 no_hydration:1 个 feature 参数 no_hydration,即建好后先不做后台拷贝。

2. 挂载并 trim 文件系统(跳过空闲空间)

mount /dev/mapper/clone /mnt/cloned-fs
fstrim /mnt/cloned-fs

fstrim 产生的 discard 被 dm-clone 解释为"这些区域无需水合"提示——只更新元数据、不拷贝数据,于是文件系统的空闲空间不会被白白复制。对已经水合的区域,discard 会(默认)继续下传到目标设备做真实回收。

3. 开启后台水合

dmsetup message clone 0 enable_hydration

此后拷贝在后台持续进行,与用户的正常读写并行。可通过 dmsetup status clone 观察 #hydrated regions/#total regions 进度;若想限制对用户 I/O 的影响,可发 dmsetup message clone 0 hydration_threshold 128 之类调整并发。

4. 水合完成后替换为 linear 表

dmsetup suspend clone
dmsetup load clone --table "0 1048576000 linear $dest_dev 0"
dmsetup resume clone

完成这四步后:

  • 目标设备已经是源设备的完整一对一拷贝;
  • dm-clone 自身不再需要,元数据设备可安全丢弃或另作他用
  • 表被换成直接映射目标设备的 linear target,零额外开销地继续服务后续 I/O。

需要特别提醒:整个流程的目标设备始终可被读写,数据"看起来立即全部存在",这正是该方案相对"先完整拷贝再交付"的最大价值。若源设备很大而目标拷贝未完成前发生故障重启,dm-clone 元数据的持久化位图会准确记录已完成区域,设备仍可挂载并继续工作。

已知问题与限制(文档原文)

  1. 未水合区域的读延迟:对未水合区域的读会被导向源设备。若源设备延迟高而用户反复读同一区域,可能拉低性能。目前依赖页缓存缓存这些区域,尽量避免重复从源读;文档建议未来应把这类读当作"尽早水合相关区域"的提示。
  2. 内存回收:水合完成后应释放 in-core 资源(即跟踪区域水合状态的位图)。
  3. 后台水合错误处理:后台水合期间若读源或写目标失败,当前仅打印错误信息,水合会无限期重试直到成功;文档认为应在一系列失败后停止后台水合并发送 dm event 通知用户空间。

设计取舍:为什么不用 dm-cache / dm-snapshot / dm-mirror / dm-thin?

实现 dm-clone 前,作者系统考察过 4 类替代方案,文档的"Why not...?"一节给出了清晰的排除理由,这也是理解 dm-clone 定位的最佳注脚:

用 dm-cache 且 cache 大小等于源设备、写一个新克隆策略:排除。因为结果 cache 设备并不是源的一对一镜像,克隆完成也无法拿掉 cache 层;且 dm-cache 会写源设备,违背源只读的硬性要求;缓存与克隆语义本质不同。

用 dm-snapshot 且 COW 设备等于源设备:排除。dm-snapshot 把元数据存放在 COW 设备内,结果设备同样不是源的一对一镜像;它没有后台拷贝机制;而且为保证快照一致性,每个 pending exception 完成都要提交元数据——克隆场景不需要如此严格,可以像 dm-thin/dm-cache 那样仅在 FLUSH/FUA bio 或周期性提交,这显著提升了性能。

用 dm-mirror:排除。mirror 有后台镜像拷贝机制,但它会写所有镜像,同样违背源设备必须只读的要求。

用 dm-thin 的外部快照(external snapshot):这是最接近的方案——thin 卷本身是源的一对一镜像,对未置备/未克隆区域读写的行为也与 dm-clone 一致。但仍不采用,原因有二:一是它没有后台拷贝机制(虽可实现);二是更关键——作者希望支持任意块设备作为克隆目标,而不局限于 thin 卷。Thin-provisioning 为维护 thin 卷映射存在固有的元数据开销,显著拖累性能。反过来,如果用户确实想用 thin 方案,完全可以把一个 thin LV 作为 dm-clone 的目标设备来叠加使用,二者并不互斥。

小结

dm-clone 通过"固定大小区域位图 + 读走源/写触发水合 + 后台 dm-kcopyd 批量拷贝 + FLUSH/FUA 或周期提交元数据"这套组合,在不侵犯源设备只读性的前提下,实现了"边使用边克隆"的高效快慢分层迁移。对需要在远程归档备份与本地高性能盘之间做无缝切换的场景(NBD/iSCSI/FC/AoE 恢复、数据上盘、灾难演练等),它是一个由内核原生提供、可通过标准 device-mapper 工具链直接驱动的务实答案。想从代码入手深入研究,建议从 drivers/md/dm-clone-target.cclone_map() 主路径与 do_worker() 工作队列开始,配合 drivers/md/dm-clone-metadata.c 理解 bitset 位图与空间映射的持久化细节。

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

项目优选

收起
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
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
594
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
916
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
516
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388