Linux Device-Mapper 脏区日志(Dirty Region Log)机制解析:core / disk / userspace 三种实现与内核-用户态交互
Device-Mapper(DM)日志子系统用于为内核中的 RAID/镜像类 target 跟踪“尚未一致(inconsistent)”的磁盘区域,是 dm-mirror 等 target 正确完成增量恢复与读写调度的基础。本文以 Documentation/admin-guide/device-mapper/dm-log.rst 为主线,结合 include/linux/dm-dirty-log.h、drivers/md/dm-log.c、include/uapi/linux/dm-log-userspace.h 等源码,系统讲解区域(region)与一致性日志的概念、dm_dirty_log_type 统一接口、disk/core/userspace 三类官方实现,以及镜像 target 如何通过该框架运作,读完即可理解日志类型参数、持久化与集群一致性方案的取舍与原理。
为什么需要“脏区日志”
dm-log 解决的问题是:设备映射中的一块磁盘地址空间,在某一时刻可能并未完全一致(consistent)。导致不一致的原因主要有两种:
- 某条 RAID 条带(stripe)正处于写操作中间,各副本还没有全部更新完成;
- 机器在某个区域被改写时崩溃/断电,导致重启后副本之间出现差异。
以镜像(mirror)为例:当你向镜像写入时,写入必须先被复制到镜像的所有“腿”(leg),而这些写盘请求到达各腿的时间并不相同,因此在写期间,该区域应被标记为 dirty(脏/不一致);当所有腿的写入都完成后,该区域才重新变为 clean(干净/一致)。
正因如此,target 需要一套“区域位图”来精确记录哪些区域脏、哪些区域干净:
- 读路径用它在故障腿恢复前只读同步的腿;
- 写路径用它在写满所有腿后把区域标净;
- 恢复(resync)路径用它在启动或掉线后找出需要重建的区域并逐个恢复。
这套逻辑在文档与代码中被统称为 dirty region log(脏区日志),其核心数据模型是:把整个设备按 region_size(扇区为单位)划分成若干等长的区域(region),每个区域用一个或多个位(bit)表示其状态(clean/sync/dirty/recovering)。
统一抽象:dm_dirty_log_type 与 dm_dirty_log
DM 通过一组类型化接口把“日志行为”抽象出来,具体实现在运行期按类型名注册。接口定义位于 include/linux/dm-dirty-log.h:
struct dm_dirty_log:一个日志实例,持有指向具体类型的指针type、可选的回调flush_callback_fn(由 target 注入,用于 flush 时回调 target)以及私有上下文context;struct dm_dirty_log_type:类型描述符,包含类型名name、所属模块module,以及一张完整的操作函数表。
接口方法及语义如下(字段均来自 include/linux/dm-dirty-log.h):
| 方法 | 作用 |
|---|---|
ctr / dtr |
构造/析构一个日志实例,ctr 负责解析 target 传入的参数; |
presuspend / postsuspend / resume |
挂起前后与恢复时的钩子,例如 disk 日志在 resume 时读写磁盘头部; |
get_region_size |
返回该日志能够处理的最小区域大小(以扇区为单位); |
is_clean |
谓词:某区域是否干净,可能阻塞; |
in_sync |
谓词:[sector, sector+len) 区间是否已同步;返回 0/1,在无法立刻判定(如集群场景)时返回 -EWOULDBLOCK,读请求随后会被转交 daemon 处理,因为 daemon 允许阻塞; |
flush |
把当前日志状态刷出(如落盘),可能阻塞; |
mark_region / clear_region |
把区域标记为脏/净;允许阻塞,但出于性能考虑应极少阻塞(如偶发内存分配); |
get_resync_work |
让日志告诉调用者“下一个需要本机恢复的区域”,返回 <0(错误)/0(没有)/1(有);注意它与 in_sync 的区别:前者分配恢复任务,后者只查询是否同步; |
set_region_sync |
通知日志某区域的同步状态已变化,并把该区域从 recovering 列表中移除(若有); |
get_sync_count |
返回当前已同步(in-sync)的区域个数,供镜像状态查询使用; |
status |
支撑 mirror target 的 dmsetup status/table 输出; |
is_remote_recovering |
专供集群镜像:探测另一节点是否正在恢复某区域,避免并发写;返回 0/1(集群日志场景下该函数可能阻塞)。 |
创建与销毁日志实例时不直接调用 type->ctr/dtr,而是走封装函数:
dm_dirty_log_create(type_name, ti, flush_callback_fn, argc, argv):按type_name在注册表中查找类型并调用其ctr(drivers/md/dm-log.c);dm_dirty_log_destroy(log):调用dtr后释放类型引用与实例(drivers/md/dm-log.c)。
类型通过 dm_dirty_log_type_register() / dm_dirty_log_type_unregister() 注册进内核维护的类型链表。这套注册机制意味着第三方模块可以在不修改 RAID/mirror target 的前提下,提供全新的日志实现——这正是 userspace 日志存在的基础。
三种官方日志类型一览
文档列出内核中可直接使用的日志实现(见 dm-log.rst):
| 类型 | 实现文件 |
|---|---|
disk |
drivers/md/dm-log.c |
core |
drivers/md/dm-log.c |
userspace |
drivers/md/dm-log-userspace-base.c、drivers/md/dm-log-userspace-transfer.c,头文件 include/uapi/linux/dm-log-userspace.h |
从 drivers/md/dm-log.c 可以看到内核在模块加载时依次注册了 .name = "core" 与 .name = "disk" 两个类型;userspace 类型则由 dm-log-userspace 模块在初始化时注册。三者的取舍核心就是日志状态放哪里:内存、日志设备、还是交给用户态守护进程。
disk 日志:状态落盘,崩溃可恢复
disk 类型会把日志状态提交(commit)到磁盘,从而保证日志在重启或系统崩溃后仍然存活——设备曾处于不一致状态的区域不会被误判为干净,重启后镜像能够据此精确恢复。
构造函数参数
按 drivers/md/dm-log.c 中 disk_ctr 的注释与解析逻辑,其参数为:
<log_device> <region_size> [sync|nosync]
log_device:保存日志元数据的块设备(dm 内部通过dm_get_device()引用,见 drivers/md/dm-log.c);region_size:区域大小,单位为扇区;- 可选
sync/nosync:nosync表示设备已知已同步,跳过首次全量同步;sync强制进行同步;缺省为DEFAULTSYNC(必要时才同步)。同步三态在代码中对应enum sync { DEFAULTSYNC, NOSYNC, FORCESYNC }(drivers/md/dm-log.c)。
磁盘上的元数据布局
日志设备头部元数据定义在 drivers/md/dm-log.c:
- 魔数
MIRROR_MAGIC = 0x4D695272,即 ASCII 串"MiRr"; - 磁盘版元数据版本
MIRROR_DISK_VERSION = 2; LOG_OFFSET = 2:头部写在日志设备第 2 个扇区之后,供dm_io定位;- 头部内容
struct log_header_disk:magic(__le32)+version(__le32)+nr_regions(__le64),内存态镜像为struct log_header_core。
日志设备的布局可概括为:头部扇区记录 magic/version/区域总数,随后存放位图(区域脏净状态)。内核态对日志设备的读写通过 dm_io 的 struct dm_io_request 完成(对应 struct log_c 中的 io_req、disk_header、header_location 等字段,drivers/md/dm-log.c)。
恢复与扩容收缩处理
disk_resume()(drivers/md/dm-log.c)展示了持久化日志的典型启动流程:
- 读取磁盘头部;
- 若头部读取失败,调用
fail_log_device()并假定所有区域都处于 out-of-sync(避免把脏区域误判为干净),同时通过dm_table_event()向用户态发出事件; - 若设备此前增大(
header.nr_regions < region_count),按 sync 模式为新区域初始化位:NOSYNC置为干净,否则置为脏; - 设备缩小则清理多余位;
- 把 clean 位图复制为 sync 位图,统计
sync_count; - 更新头部并回写(
rw_header(REQ_OP_WRITE)),随后flush_header()落盘。
struct log_c(drivers/md/dm-log.c)中同时维护三份位图 clean_bits、sync_bits、recovering_bits,分别刻画“写路径上的干净/脏”“镜像是否已同步”以及“正处于恢复中的区域”。
core 日志:纯内存日志
core 类型把日志状态保存在内存中,状态在重启或崩溃后不复存在,但可能带来小幅性能提升;同时它不依赖任何存储设备,因此当没有可用设备保存日志状态时也可使用(例如纯内存/临时镜像场景)。
其构造函数 core_ctr() 直接复用与 disk 共享的 create_log_context()(drivers/md/dm-log.c),参数为:
<region_size> [sync|nosync]
create_log_context 的核心动作包括:
- 参数个数必须为 1 或 2(第二个可选参数只接受
sync/nosync); - 解析并校验
region_size(扇区),非法则报invalid region size; - 按
region_count = dm_sector_div_up(ti->len, region_size)计算区域总数,并要求不超过UINT_MAX; - 分配
log_c上下文及各状态位图。
与 disk 的实现相比,core 与 disk 在文档与代码中都注明共享大量实现逻辑(见 drivers/md/dm-log.c 的注释 "Persistent and core logs share a lot of their implementation"),差别主要体现在:core 没有日志设备、没有磁盘头部读写;disk 在 resume 时需要 read_header/write_header/flush_header,dtr 时还需 dm_put_device 释放日志设备并销毁 dm_io client(drivers/md/dm-log.c)。
userspace 日志:把日志 API 搬到用户态
userspace 类型的目的是把整套 dirty log API 导出到用户空间,让日志实现可以在用户态完成。内核端只是一个“代理”:绝大部分日志请求会被转发给用户态,由一个用户态守护进程接收并处理。这样带来的直接好处是:需要集群协同的复杂实现(如 clustered-disk、clustered-core)不必以难以维护的内核代码形式存在,而可以复用用户态集群栈(DLM/锁管理等)来保证跨节点一致性。
内核-用户态通信链路:为何选择 connector
内核与用户态之间的交互具有高频、多样、双向的特点,因此文档明确指出内核选择了内核 connector 作为通信界面(基于 netlink)。内核侧支撑代码分两个文件:
- drivers/md/dm-log-userspace-base.c:实现
dm_dirty_log_type接口并把各方法封装成请求; - drivers/md/dm-log-userspace-transfer.c:实现具体的发送/接收与回调注册(入口为 drivers/md/dm-log-userspace-transfer.c 的
dm_ulog_tfr_init())。
用户态侧建立通信链路的示意代码直接记录在 UAPI 头文件注释中(include/uapi/linux/dm-log-userspace.h),本质是打开 netlink connector socket 并加入 CN_IDX_DM 组:
fd = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_CONNECTOR);
addr.nl_family = AF_NETLINK;
addr.nl_groups = CN_IDX_DM;
addr.nl_pid = 0;
r = bind(fd, (struct sockaddr *) &addr, sizeof(addr));
opt = addr.nl_groups;
setsockopt(fd, SOL_NETLINK, NETLINK_ADD_MEMBERSHIP, &opt, sizeof(opt));
内核侧封装请求/应答的核心函数签名见 drivers/md/dm-log-userspace-transfer.h:
int dm_consult_userspace(const char *uuid, uint64_t luid, int request_type,
char *data, size_t data_size,
char *rdata, size_t *rdata_size);
配置上,userspace 日志模块对应 CONFIG_DM_LOG_USERSPACE(tristate "Mirror userspace logging",depends on DM_MIRROR && NET,并 select CONNECTOR),见 drivers/md/Kconfig。
请求报文结构:dm_ulog_request
内核发给用户态的消息格式为 struct dm_ulog_request(头部)+ 可选的附加数据(payload),定义于 include/uapi/linux/dm-log-userspace.h:
| 字段 | 含义 |
|---|---|
luid(__u64) |
本地唯一标识(local unique id),在同一台机器上区分不同日志实例; |
uuid[DM_UUID_LEN] |
全局唯一标识,集群日志依赖它进行跨节点通信定位同一日志;DM_UUID_LEN 为 129,故结构体用 3 字节 padding 对齐; |
version |
协议版本,见 DM_ULOG_REQUEST_VERSION; |
error |
处理结果的错误码回传; |
seq |
请求序号,用于将异步应答与请求配对; |
request_type |
请求类型(DM_ULOG_*),低 8 位有效; |
data_size |
本次请求 payload 的数据量(不含本结构体本身); |
data[] |
灵活数组,承载具体请求/应答的 payload。 |
注释进一步说明:luid 与 uuid 配合可以区分“交换中的两个日志”——典型场景是设备映射的 live 表与 inactive 表 各自持有一份同 uuid 的日志,靠 luid 区分。uuid 同时也是所有后续请求引用某个日志实例的钥匙,构造请求 DM_ULOG_CTR 的应答中若返回一个设备名字符串,内核会对该设备调用 dm_get_device,并在 DM_ULOG_DTR 后自动 dm_put_device。
17 类请求与 dirty log API 的映射
UAPI 头文件把日志接口函数逐一映射为带编号的请求类型。为保持前向兼容,请求类型只使用 32 位字段中的低 8 位(DM_ULOG_REQUEST_MASK 0xFF),其余 24 位保留,用户态应始终通过 DM_ULOG_REQUEST_TYPE(request_type) 宏取类型。完整对应关系如下(详见 include/uapi/linux/dm-log-userspace.h):
| 请求宏 | 值 | 对应的接口(dm-dirty-log.h) | Payload 要点 |
|---|---|---|---|
DM_ULOG_CTR |
1 | ctr() |
内核→用户:空格分隔的全部 argv;用户→内核:日志设备名或空 |
DM_ULOG_DTR |
2 | dtr() |
无 |
DM_ULOG_PRESUSPEND |
3 | presuspend() |
无 |
DM_ULOG_POSTSUSPEND |
4 | postsuspend() |
无 |
DM_ULOG_RESUME |
5 | resume() |
无 |
DM_ULOG_GET_REGION_SIZE |
6 | get_region_size() |
用户→内核:__u64 区域大小 |
DM_ULOG_IS_CLEAN |
7 | is_clean() |
入参 __u64 区域;返回 __s64 1=干净 |
DM_ULOG_IN_SYNC |
8 | in_sync() |
入参 __u64 区域;返回 __s64 1=已同步 |
DM_ULOG_FLUSH |
9 | flush() |
见下文 integrated_flush |
DM_ULOG_MARK_REGION |
10 | mark_region() |
__u64[] 脏区域列表 |
DM_ULOG_CLEAR_REGION |
11 | clear_region() |
__u64[] 净区域列表 |
DM_ULOG_GET_RESYNC_WORK |
12 | get_resync_work() |
返回 {__s64 是否需要恢复; __u64 区域} |
DM_ULOG_SET_REGION_SYNC |
13 | set_region_sync() |
入参 {__u64 区域; __s64 是否同步} |
DM_ULOG_GET_SYNC_COUNT |
14 | get_sync_count() |
返回 __u64 已同步区域数 |
DM_ULOG_STATUS_INFO |
15 | status(STATUSTYPE_INFO) |
返回状态字符串 |
DM_ULOG_STATUS_TABLE |
16 | status(STATUSTYPE_TABLE) |
返回 table 字符串 |
DM_ULOG_IS_REMOTE_RECOVERING |
17 | is_remote_recovering() |
入参 __u64 区域;返回 {__s64; __u64 in_sync_hint} |
其中 mark/clear/set_region_sync 等请求的 payload 中可以携带一批区域(数量由 data_size / sizeof(__u64) 计算得出),借此减少往返消息数。
协议版本演进与 integrated_flush
DM_ULOG_REQUEST_VERSION 目前为 3,UAPI 头文件记录了演进历史(include/uapi/linux/dm-log-userspace.h):
- version 1:初始实现;
- version 2:
DM_ULOG_CTR允许返回设备名字符串,供内核通过dm_get_device注册为日志设备; - version 3:
DM_ULOG_FLUSH可以携带 mark-region payload,即integrated_flush(合并 flush) 特性——把“标记区域”与“flush 落盘”两次交互合并为一次,显著减少内核与用户态之间的通信次数。由于该特性需要构造表里显式传入integrated_flush选项才会开启(解析见 drivers/md/dm-log-userspace-base.c),因此向后兼容旧的用户态实现。
集群共享存储场景:clustered-disk 与 clustered-core
文档指出目前有两个基于 userspace 框架的用户态日志实现:clustered-disk 与 clustered-core。它们为共享存储提供集群一致的日志:当多个节点同时访问同一个共享存储上的镜像设备时,必须避免两个节点在同一时刻对同一区域做写复制或恢复操作,否则会产生数据竞争与副本撕裂。
clustered-* 实现正是依托前述机制工作的:
- 日志实例用 uuid 标识(集群各节点通过它指代同一个日志),而
is_remote_recovering()与in_sync()的-EWOULDBLOCK/阻塞语义,让本节点可以在无法立即判定远程状态时把判定交给用户态 daemon,由集群协调完成跨节点仲裁(见 include/linux/dm-dirty-log.h 与 include/linux/dm-dirty-log.h 中对这两个函数的注释); - 集群日志实现位于用户态,因此可以方便地复用用户态集群基础设施(如分布式锁管理器)来维护跨节点的恢复互斥;
- 在使用集群日志实现时,device-mapper 镜像(mirror)即可运行在共享存储环境上(对应文档结论 "Device-mapper mirroring can be used in a shared-storage environment when the cluster log implementations are employed")。
简而言之,clustered-core 提供的是集群一致的“内存”日志(不额外落盘),clustered-disk 则在集群一致之上再把日志提交到共享存储设备,两者按是否需要跨崩溃保存状态来取舍。
上层消费者:镜像 target 如何接入日志
在内核源码树中,dirty log 最典型的消费方是 mirror(镜像)target,实现于 drivers/md/dm-raid1.c。它通过 dirty region hash(dm_rh_*,见 struct mirror_set 中的 rh 字段)把 I/O 按区域组织,日志类型则通过 create_dirty_log() 解析,其代码注释明确给出了表参数语法(drivers/md/dm-raid1.c):
Create dirty log: log_type #log_params <log_params>
即在镜像设备映射表中,紧随 target 名之后出现的是日志类型、日志参数个数、日志参数,随后才是镜像腿设备列表。日志参数的具体内容由具体类型决定:core 为 <region_size> [sync|nosync],disk 为 <log_device> <region_size> [sync|nosync]。区域大小一经日志解析,就会通过 get_sync_count/in_sync/get_resync_work/set_region_sync 等接口驱动整个镜像的恢复流程。
运行期集成细节在 drivers/md/dm-raid1.c 中随处可见,例如:
- 通过
log->type->get_sync_count(log)判断镜像是否已完全同步(drivers/md/dm-raid1.c); - 读路径用
log->type->in_sync(log, region, 0)决定是否可从某腿读取(drivers/md/dm-raid1.c); - 恢复路径用
log->type->is_remote_recovering()探测远端是否正在恢复同一区域(drivers/md/dm-raid1.c); - 写路径在
dm_rh_flush()失败时置log_failure,进入“日志故障”处理分支(drivers/md/dm-raid1.c)。
镜像 target 对外暴露的 dmsetup status 也包含日志状态字段,这一点可从安全/IMA 文档中 mirror target 的属性格式得到印证:其 target_attributes 中包含 handle_errors、keep_log、log_type_status 等项(见 Documentation/admin-guide/device-mapper/dm-ima.rst),说明日志类型与日志运行状态是镜像 target 管理与审计模型中的一等公民。
小结:如何选择日志类型
结合文档与源码,可以把三类日志的选型准则归纳为一张决策表:
| 场景 | 推荐日志 | 理由 |
|---|---|---|
| 需要跨重启/崩溃保持区域状态 | disk |
状态提交到日志设备,"MiRr" 头 + 位图可持久化;代价是每次 flush 需写日志设备 |
| 无需持久化、追求低开销 | core |
纯内存位图;适合无日志设备可用或对崩溃恢复要求不高的场景 |
| 共享存储上的多节点镜像 | clustered-core / clustered-disk |
通过 userspace 框架 + connector 把一致性仲裁放到用户态集群栈 |
技术要点回顾:dirty region log 的本质是一张“按固定区域大小划分的脏净位图”;disk/core 共享同一套内存位图实现(clean_bits/sync_bits/recovering_bits),差别仅在于是否落盘持久化;userspace 通过 netlink connector 与用户态 daemon 通信,用 17 类 DM_ULOG_* 请求完整映射 dm_dirty_log_type 接口,并为集群日志提供了 uuid 定位、is_remote_recovering 探测和 integrated_flush 批量合并等专门能力。理解这一层抽象,就能正确解读镜像/RAID 设备表里的日志参数,也能在需要自定义区域一致性策略时,以 dm_dirty_log_type_register() 为入口扩展自己的日志类型。
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 StartedRust0624
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