Linux Device Mapper 的 kcopyd:块设备扇区异步复制框架的原理与用法
导读
kcopyd 是 Linux Device Mapper(DM)子系统内部提供的一项"块设备扇区批量复制"服务,它允许内核模块把一个块设备上的一段扇区区域复制到一个或多个目标块设备上,并在复制完成时通过回调函数异步通知发起者。它是 dm-snapshot、dm-mirror(dm-raid1)等经典 DM 目标的后端动力,如今也被 dm-thin、dm-cache、dm-clone、dm-zoned、dm-writecache、dm-vdo 等多个 DM 目标复用。本文以 Documentation/admin-guide/device-mapper/kcopyd.rst 为骨架,结合其底层实现 drivers/md/dm-kcopyd.c 与对外头文件 include/linux/dm-kcopyd.h,完整讲解 kcopyd 的使用模型(client 生命周期、io_region 描述、复制提交与完成回调)以及内部的分片调度、页池管理与顺序写约束等实现细节,帮助读者从"怎么用"深入到"怎么实现"。
一、kcopyd 是什么:一个可复用的扇区异步复制服务
DM(Device Mapper)中的各类 target 经常需要执行"把一段数据从一个块设备搬到另一个或多个块设备"的操作,例如快照例外表(exception store)的迁移、镜像的恢复与重同步、精简池的按需分配复制等。如果每个 target 各自实现一套读写调度逻辑,既容易出错也难以保证内存等资源被有效复用。kcopyd(kernel copy daemon)正是为了解决这一共性需求而提供的公共设施。
依据官方文档的定位:
Kcopyd provides the ability to copy a range of sectors from one block-device to one or more other block-devices, with an asynchronous completion notification. It is used by dm-snapshot and dm-mirror.
即:kcopyd 能把一个块设备上的一段扇区区间复制到一个或多个其他块设备上,并以异步完成通知的方式回调发起者。原文档以 dm-snapshot 与 dm-mirror 为典型使用方;而从当前仓库源码看,kcopyd 的使用面已经扩展到了 dm-thin.c、dm-cache-target.c、dm-clone-target.c、dm-raid1.c、dm-snap.c、dm-zoned-reclaim、dm-writecache 以及 dm-vdo 等多个 DM 驱动中,是名副其实的"复制基础设施"。
需要说明的是,原文档示例沿用了较早期未加 dm_ 前缀的接口命名(如 kcopyd_client_create()),当前内核树中的正式接口统一带有 dm_ 前缀,并增加了节流(throttle)等新参数。下文将以当前仓库 include/linux/dm-kcopyd.h 中的实际签名为准展开,二者在概念上一一对应。
二、核心数据结构:用 io_region 描述复制区域
文档指出,发起一次复制任务前,使用者需要准备 io_region 结构体来描述源与目标区域。每个 io_region 包含一个块设备指针、起始扇区与区域大小(以扇区为单位)。原始结构体在当前内核中已更名为 struct dm_io_region,定义于 include/linux/dm-io.h:
struct dm_io_region {
struct block_device *bdev;
sector_t sector;
sector_t count; /* If this is zero the region is ignored. */
};
三个字段的含义:
| 字段 | 类型 | 含义 |
|---|---|---|
bdev |
struct block_device * |
目标块设备,复制读写操作的作用对象 |
sector |
sector_t |
区域起始扇区号(512 字节扇区为基本单位) |
count |
sector_t |
区域包含的扇区数量;为 0 时该区域会被忽略 |
源区域只需一个 dm_io_region;而目标区域则以数组形式提供,每个元素对应一个要写入的目的设备(一次最多支持的目标数由 DM_KCOPYD_MAX_REGIONS 决定,当前值为 8,见 include/linux/dm-kcopyd.h)。这也对应了文档中"source 用单个结构、destinations 用结构数组"的约定。
三、标准使用流程:创建 client → 提交复制 → 完成回调 → 销毁 client
3.1 创建 client 并预留页池
文档描述:使用者必须先创建一个 kcopyd client,并告知系统为复制任务预留多少内存页,对应调用 kcopyd_client_create()。这一设计在当前实现中体现为"每个 client 拥有自己独立的内存页池(page pool)":所有读写 DM 请求所需的页面在 client 创建时被预分配、反复回收复用,从而避免在每次复制任务中临时申请页导致延迟与碎片。
当前内核的创建接口为(include/linux/dm-kcopyd.h):
struct dm_kcopyd_client;
struct dm_kcopyd_client *dm_kcopyd_client_create(struct dm_kcopyd_throttle *throttle);
void dm_kcopyd_client_destroy(struct dm_kcopyd_client *kc);
void dm_kcopyd_client_flush(struct dm_kcopyd_client *kc);
与原文档的关键差异在于:创建时不再直接以"页数"为参数,而是传一个可选的 struct dm_kcopyd_throttle * 节流控制器(不需要节流时传 NULL)。页面预留数量由内核根据"子任务大小"自动换算得到,而不是由调用方拍板。
从实现看,dm_kcopyd_client_create()(drivers/md/dm-kcopyd.c)依次完成以下初始化工作:
- 分配并清零
struct dm_kcopyd_client; - 初始化自旋锁
job_lock与四条 job 链表(见后文调度小节); - 通过
mempool_init_slab_pool()建立 job 内存池(下限MIN_JOBS= 8 个); - 创建专用工作队列
kcopyd,标志为WQ_MEM_RECLAIM | WQ_PERCPU,保证在内存回收路径下仍可排队执行(dm 代码常运行于 IO 完成等易饥饿的上下文); - 读取模块参数
kcopyd_subjob_size_kb得到子任务大小sub_job_size,并按DIV_ROUND_UP(sub_job_size << SECTOR_SHIFT, PAGE_SIZE)预留对应数量的页(client_reserve_pages()); - 创建底层
dm_io_client(实际下发块设备 IO 的引擎); - 初始化等待队列
destroyq与 job 计数器nr_jobs。
client 的核心结构(drivers/md/dm-kcopyd.c)把"页池、io client、job 内存池、工作队列、节流器、四条 job 链表与计数"全部封装在一起,一个 target(如 snapshot、mirror)在其构造阶段只需创建一次 client,即可在其整个生命周期内反复提交复制任务。
3.2 提交一次复制任务
文档给出的提交入口语义是:传入 client 指针、源与目标的 io_region 指针、完成回调函数名以及回调上下文。当前实现对应的函数是(include/linux/dm-kcopyd.h):
void dm_kcopyd_copy(struct dm_kcopyd_client *kc, struct dm_io_region *from,
unsigned int num_dests, struct dm_io_region *dests,
unsigned int flags, dm_kcopyd_notify_fn fn, void *context);
参数对照说明:
| 参数 | 含义 |
|---|---|
kc |
由 dm_kcopyd_client_create() 创建的 client |
from |
指向源区域的 dm_io_region;传 NULL 表示"不读任何数据",改为向目标写零(对应专门的 dm_kcopyd_zero()) |
num_dests / dests |
目标区域数量与目标区域数组 |
flags |
行为标志位(见第 4 节) |
fn / context |
复制完成后的回调函数与透传上下文指针 |
3.3 完成回调与错误上报语义
文档强调:复制完成后 kcopyd 会调用使用者的回调例程,并把使用者的 context 原样传回,同时指出本次复制过程中是否发生了读错误或写错误。回调类型当前定义为(include/linux/dm-kcopyd.h):
typedef void (*dm_kcopyd_notify_fn)(int read_err, unsigned long write_err,
void *context);
错误语义非常关键,必须区分两种"错误"的编码方式:
read_err:布尔量。非 0 表示源端读失败;write_err:位图(bitset)。每一位对应一个目标区域——也就是说,第 n 个目标写失败时,write_err的第 n 位会被置位,便于调用方精确判断是哪一块目标盘出错。
源码中错误的记录逻辑可验证这一点:IO 完成后在 complete_io()(drivers/md/dm-kcopyd.c)中,若发生错误则写操作累积进 job->write_err(按位或),读操作则直接把 job->read_err 置 1;除非设置 DM_KCOPYD_IGNORE_ERROR(见下节),有错误的 job 会直接进入完成队列并结束。
另外值得留意的是,回调并非在块设备 IO 完成的中断/软中断上下文直接触发,而是由实现保证所有完成回调都从 kcopyd 工作队列线程串行派发。源码注释明确指出有些调用方假设"所有完成回调来自同一线程且互不竞争",因此 dm_kcopyd_copy() 内部会把完成回调排队到工作队列后再执行(见 drivers/md/dm-kcopyd.c 附近注释)。这也解释了为什么 drivers/md/dm-kcopyd.c 还额外提供了 dm_kcopyd_prepare_callback() / dm_kcopyd_do_callback() 这一对接口,用于"准备好一个回调、再从任意(含中断)上下文投递它",由 kcopyd 线程统一执行。
3.4 销毁 client
当使用者不再需要复制服务时,调用销毁接口(include/linux/dm-kcopyd.h):
void dm_kcopyd_client_destroy(struct dm_kcopyd_client *kc);
文档说明销毁将释放为复制任务预留的内存页。实现细节上,dm_kcopyd_client_destroy()(drivers/md/dm-kcopyd.c)会先通过 wait_event(kc->destroyq, !atomic_read(&kc->nr_jobs)) 阻塞等待该 client 提交的全部 job 完成,并用 BUG_ON 断言四条 job 链表必须为空,然后依次销毁工作队列、dm_io_client、释放预留页并归还 job 内存池。因此调用方应确保在安全时刻(无未完成任务)执行销毁,通常对应 DM target 的析构(dtr)阶段。
四、flags 标志位:错误忽略与顺序写约束
头文件中定义了两种复制行为标志(include/linux/dm-kcopyd.h):
#define DM_KCOPYD_IGNORE_ERROR 1
#define DM_KCOPYD_WRITE_SEQ 2
DM_KCOPYD_IGNORE_ERROR(值 1,忽略错误继续)
设置后,单个分段发生读/写错误时不会立刻终止整个复制任务,而是记录错误状态后继续尝试后续分段,最终在回调中以 read_err / write_err 汇总上报,由调用方决定如何处理。典型使用场景是 dm-mirror 的恢复流程:镜像恢复时某些副本设备可能处于临时故障状态,需要"尽量把能写的都写了",在 dm-raid1.c 中可以清楚地看到镜像恢复提交复制任务前根据 errors_handled(ms) 决定是否 flags |= BIT(DM_KCOPYD_IGNORE_ERROR)。
DM_KCOPYD_WRITE_SEQ(值 2,按序写目标)
要求对目标设备的写入必须严格按照源扇区顺序执行,而不是让各分段并行乱序写。这个标志主要服务于 zoned block device 等对写顺序有强约束的介质。源码中甚至有自动推导逻辑:dm_kcopyd_copy() 在提交时遍历所有目标,若发现某个目标是 host-managed 分区设备(bdev_is_zoned() 为真)就自动补上 DM_KCOPYD_WRITE_SEQ 标志(drivers/md/dm-kcopyd.c)。
需要注意两个标志的联动约束:由于顺序写场景下后续写依赖前面成功写完,一旦要求顺序写就不能忽略错误,所以源码在两者同时出现时会强制清除 DM_KCOPYD_IGNORE_ERROR(drivers/md/dm-kcopyd.c)。而"顺序写正确性"的落实体现在 job 出队逻辑 pop_io_job()(drivers/md/dm-kcopyd.c)中:非顺序写的 job 可任意取出,而顺序写 job 只有在"其写偏移恰好等于 master job 当前推进到的写偏移"时才被允许执行,从而天然保证目标端写入顺序与源端一致。
五、实现纵深:分片调度、页池、任务拆分与零填充优化
5.1 一次提交 → 一个"复制任务"(job)的四种状态
dm_kcopyd_copy() 内部的核心对象是 struct kcopyd_job(drivers/md/dm-kcopyd.c)。每个 client 维护由自旋锁保护的四条 job 链表,构成一个简单而精巧的状态机(对应 drivers/md/dm-kcopyd.c 的注释说明):
| 链表 | 含义 |
|---|---|
pages_jobs |
正在等待获取足够内存页的 job |
io_jobs |
已经拿到页、等待下发实际块设备 IO 的 job |
callback_jobs |
无需任何 IO、只需执行回调的 job |
complete_jobs |
IO 已完成、等待在 kcopyd 线程中回调并回收的 job |
dispatch_job()(drivers/md/dm-kcopyd.c)依据 job 状态把它送入对应链表并唤醒工作队列;工作线程执行 do_work()(drivers/md/dm-kcopyd.c)时按"完成 → 取页 → 下发 IO"的严格顺序批量处理——注释明确指出这个顺序至关重要:完成 job 释放的页可供 pages job 使用,pages job 取到页后转入 io_jobs,环环相扣。处理过程还包裹在 blk_start_plug()/blk_finish_plug() 中,以提升批量 IO 的块层合并效率。
5.2 内存页池:预分配 + 紧急取用保留页
struct dm_kcopyd_client 中维护着一个页链表 pages、空闲页计数 nr_free_pages 与保留页计数 nr_reserved_pages。复制任务执行时通过 kcopyd_get_pages()(drivers/md/dm-kcopyd.c)获取页:它优先尝试低开销、不触发磁盘回收的分配(__GFP_NOWARN | __GFP_NORETRY | __GFP_KSWAPD_RECLAIM);一旦分配失败则动用到 client 预留的保留页,避免因内存紧张导致复制任务无法推进。任务完成时 kcopyd_put_pages()(drivers/md/dm-kcopyd.c)把页归还链表,超过保留上限的页则直接释放回系统。这一设计正是文档所述"预留内存页、复用页池"思想的完整落地。
5.3 子任务拆分:大区域复制如何被切碎调度
如果复制区域很大(超过单个子任务大小),一次性的巨大 IO 既耗页又容易长时间占用 IO 队列。为此 dm_kcopyd_copy() 在提交时做分水岭判断(drivers/md/dm-kcopyd.c):
- 若
source.count <= sub_job_size:直接以一个 job 下发(单次读 + 多次写); - 否则创建 1 个 master job +
SPLIT_COUNT(= 8)个子 job(split_job(),drivers/md/dm-kcopyd.c),把大区域切成多个顺序推进的片段并发复制。
每个片段完成时调用 segment_complete()(drivers/md/dm-kcopyd.c):若整体未出错(或允许忽略错误),继续推进 progress 取出下一片;若所有子片都完成,则把 master job 送入完成队列统一触发用户回调,并回收全部资源。错误会由子片逐级汇聚到 master job 上。这种方式保证了任意大小的复制请求都能以有界、可控的 IO 粒度推进,避免单个复制任务长期霸占 IO 资源。
5.4 零填充优化:dm_kcopyd_zero() 与 WRITE ZEROES
在快照(dm-snap)、精简池(dm-thin)等场景中,经常需要"把目标区域清零"而并非真正搬运数据。为此 kcopyd 提供了便捷封装(drivers/md/dm-kcopyd.c):
void dm_kcopyd_zero(struct dm_kcopyd_client *kc,
unsigned int num_dests, struct dm_io_region *dests,
unsigned int flags, dm_kcopyd_notify_fn fn, void *context);
它等价于以 from = NULL 调用 dm_kcopyd_copy()。此时 kcopyd 不再为任务准备普通数据页,而是直接使用全局共享的 zero_page_list(映射到系统的 ZERO_PAGE),并把操作类型优先设为 REQ_OP_WRITE_ZEROES(drivers/md/dm-kcopyd.c):仅当某个目标设备不支持硬件写零(bdev_write_zeroes_sectors() 返回 0)时才回退为普通 REQ_OP_WRITE。这能让 SSD / NVMe 等支持 Write Zeroes 的存储真正以元数据级操作完成清零,大幅减少实际写放大。典型用法见 dm-snap.c 对例外区域清零、dm-thin.c 对精简块清零的调用。
5.5 节流机制:throttle 与模块参数
对快照回拷、镜像恢复这类后台任务,如果不加节制地满速复制,会抢占前台业务 IO 的带宽。因此 kcopyd 提供了可选的节流机制。struct dm_kcopyd_throttle(include/linux/dm-kcopyd.h)通过统计 IO 忙碌时间占总时间的比例来控制复制速率;多个 client 还可以共享同一个 throttle 实例,从而作为一个整体被统一限速。为了便于在运行时调节,头文件提供了辅助宏 DECLARE_DM_KCOPYD_THROTTLE_WITH_MODULE_PARM()(include/linux/dm-kcopyd.h),它把 throttle 阈值(默认 100,即不限速)注册为 0644 权限的模块参数。
当前各使用方由此暴露了对应的运行时可调参数:
- 快照:
snapshot_copy_throttle(dm-snap.c) - 镜像重同步:
raid1_resync_throttle(dm-raid1.c) - 精简池后台复制:
snapshot_copy_throttle(dm-thin.c) - dm-cache 迁移节流:
cache_copy_throttle(dm-cache-target.c) - dm-clone 水合(hydration)节流:
clone_hydration_throttle(dm-clone-target.c)
限速值取 0~100 的百分比语义(throttle >= 100 时直接跳过限速逻辑),数值越小允许的 IO 占比越低。实现上由 io_job_start() / io_job_finish()(drivers/md/dm-kcopyd.c)在每笔 IO 前后统计 io_period / total_period 并计算"偏斜量(skew)",超出限额时以 fsleep(SLEEP_USEC)(100ms)短暂睡眠,且最多连续睡眠 MAX_SLEEPS(10)次以防活锁。
六、主要调用方速览:快照、镜像与更多现代 DM 目标
| 使用者 | 典型用途 | 创建点 | 附加说明 |
|---|---|---|---|
| dm-snapshot(快照) | 例外块迁移/合并 | dm-snap.c | 原文档列出的经典使用者 |
| dm-mirror / dm-raid1(镜像) | 镜像恢复、重同步 | dm-raid1.c | 原文档列出的经典使用者 |
| dm-thin(精简池) | 手动迁移、按需复制 | dm-thin.c | 以 dm_kcopyd_zero() 做块清零 |
| dm-cache(缓存) | 缓存迁移/清洗 | dm-cache-target.c | 使用节流参数避免影响前台 |
| dm-clone(克隆) | 后台水合(hydration) | dm-clone-target.c | 克隆源到目标的全量复制 |
| dm-zoned-reclaim / dm-writecache / dm-vdo | 各自的后台数据搬移 | 位于 drivers/md | 体现了 kcopyd 的通用价值 |
作为佐证,以 dm-snap 为例,在快照合并(commit)过程中会为新生成的例外区、被覆盖的原始块搬运数据,而这些复制任务的唯一提交入口正是它创建的那个 kcopyd_client;dm-raid1 则在镜像某副本故障后的恢复路径(do_recovery() 附近)构造"1 个源 + N-1 个健康副本目标"的 dm_io_region 数组,并配合 DM_KCOPYD_IGNORE_ERROR 调用 dm_kcopyd_copy()。从这些调用点可以清楚看到,kcopyd 使用方的共性模式就是:构造区域数组 → 提交复制 → 在回调中收尾并触发下一次任务。
七、调优参数与使用约束小结
模块参数
kcopyd_subjob_size_kb(drivers/md/dm-kcopyd.c):单个复制子任务的大小,单位 KB,默认 512,上限 1024,可在加载dm-mod时通过module_param以 0644 权限动态调整;每个 client 预留的内存页数量与该值直接挂钩(预留页数 = 子任务字节数 / PAGE_SIZE 向上取整)。- 各 DM target 暴露的
*_copy_throttle/*_hydration_throttle/raid1_resync_throttle等模块参数:控制后台复制限速,取值 0~100。
对使用者的约束
dm_kcopyd_client_create()传NULLthrottle 即不启用限速;节流实例可在多个 client 间共享以实现"按组限速"。- 回调(
fn)由 kcopyd 工作线程串行执行,回调中可直接安全地推进后续状态机;需要跨上下文投递回调时可使用dm_kcopyd_prepare_callback()/dm_kcopyd_do_callback()。 read_err是布尔错误,write_err是按目标位组织的位图,解读错误时要区分二者编码。struct dm_io_region.count为 0 的区域会被忽略,可据此跳过某些目标。DM_KCOPYD_WRITE_SEQ与DM_KCOPYD_IGNORE_ERROR互斥(顺序写要求不忽略错误),且遇到 zoned 目标时内核会自动补上顺序写标志。dm_kcopyd_client_destroy()会等待该 client 的全部任务完成,DM target 应在自身dtr阶段、确保不再有新任务提交后调用,避免阻塞与误用。
总而言之,kcopyd 为 Device Mapper 的"多块设备间扇区级复制"这一高频需求提供了统一、异步、带错误上报与限速能力的公共内核服务:对外它只有 create / copy(含 zero)/ destroy / 回调寥寥几个概念,简单到文档一页即可讲完;对内它则靠页池复用、四队列状态机、子任务拆分、顺序写约束与节流统计支撑起各 DM target 繁重的后台搬移工作。理解 kcopyd,等于同时读懂了快照、镜像、精简与缓存等大量 DM 设备背后那条共同的"数据搬运流水线"。
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
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00