首页
/ Linux Device-Mapper 脏区日志(Dirty Region Log)机制解析:core / disk / userspace 三种实现与内核-用户态交互

Linux Device-Mapper 脏区日志(Dirty Region Log)机制解析:core / disk / userspace 三种实现与内核-用户态交互

2026-09-07 11:46:54作者:申梦珏Efrain

Device-Mapper(DM)日志子系统用于为内核中的 RAID/镜像类 target 跟踪“尚未一致(inconsistent)”的磁盘区域,是 dm-mirror 等 target 正确完成增量恢复与读写调度的基础。本文以 Documentation/admin-guide/device-mapper/dm-log.rst 为主线,结合 include/linux/dm-dirty-log.hdrivers/md/dm-log.cinclude/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_typedm_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 在注册表中查找类型并调用其 ctrdrivers/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.cdrivers/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.cdisk_ctr 的注释与解析逻辑,其参数为:

<log_device> <region_size> [sync|nosync]
  • log_device:保存日志元数据的块设备(dm 内部通过 dm_get_device() 引用,见 drivers/md/dm-log.c);
  • region_size:区域大小,单位为扇区;
  • 可选 sync / nosyncnosync 表示设备已知已同步,跳过首次全量同步;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_diskmagic__le32)+ version__le32)+ nr_regions__le64),内存态镜像为 struct log_header_core

日志设备的布局可概括为:头部扇区记录 magic/version/区域总数,随后存放位图(区域脏净状态)。内核态对日志设备的读写通过 dm_iostruct dm_io_request 完成(对应 struct log_c 中的 io_reqdisk_headerheader_location 等字段,drivers/md/dm-log.c)。

恢复与扩容收缩处理

disk_resume()drivers/md/dm-log.c)展示了持久化日志的典型启动流程:

  1. 读取磁盘头部;
  2. 若头部读取失败,调用 fail_log_device() 并假定所有区域都处于 out-of-sync(避免把脏区域误判为干净),同时通过 dm_table_event() 向用户态发出事件;
  3. 若设备此前增大(header.nr_regions < region_count),按 sync 模式为新区域初始化位:NOSYNC 置为干净,否则置为脏;
  4. 设备缩小则清理多余位;
  5. 把 clean 位图复制为 sync 位图,统计 sync_count
  6. 更新头部并回写(rw_header(REQ_OP_WRITE)),随后 flush_header() 落盘。

struct log_cdrivers/md/dm-log.c)中同时维护三份位图 clean_bitssync_bitsrecovering_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 的实现相比,coredisk 在文档与代码中都注明共享大量实现逻辑(见 drivers/md/dm-log.c 的注释 "Persistent and core logs share a lot of their implementation"),差别主要体现在:core 没有日志设备、没有磁盘头部读写;diskresume 时需要 read_header/write_header/flush_headerdtr 时还需 dm_put_device 释放日志设备并销毁 dm_io client(drivers/md/dm-log.c)。

userspace 日志:把日志 API 搬到用户态

userspace 类型的目的是把整套 dirty log API 导出到用户空间,让日志实现可以在用户态完成。内核端只是一个“代理”:绝大部分日志请求会被转发给用户态,由一个用户态守护进程接收并处理。这样带来的直接好处是:需要集群协同的复杂实现(如 clustered-diskclustered-core)不必以难以维护的内核代码形式存在,而可以复用用户态集群栈(DLM/锁管理等)来保证跨节点一致性。

内核-用户态通信链路:为何选择 connector

内核与用户态之间的交互具有高频、多样、双向的特点,因此文档明确指出内核选择了内核 connector 作为通信界面(基于 netlink)。内核侧支撑代码分两个文件:

用户态侧建立通信链路的示意代码直接记录在 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_USERSPACEtristate "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 2DM_ULOG_CTR 允许返回设备名字符串,供内核通过 dm_get_device 注册为日志设备;
  • version 3DM_ULOG_FLUSH 可以携带 mark-region payload,即 integrated_flush(合并 flush) 特性——把“标记区域”与“flush 落盘”两次交互合并为一次,显著减少内核与用户态之间的通信次数。由于该特性需要构造表里显式传入 integrated_flush 选项才会开启(解析见 drivers/md/dm-log-userspace-base.c),因此向后兼容旧的用户态实现。

集群共享存储场景:clustered-diskclustered-core

文档指出目前有两个基于 userspace 框架的用户态日志实现:clustered-diskclustered-core。它们为共享存储提供集群一致的日志:当多个节点同时访问同一个共享存储上的镜像设备时,必须避免两个节点在同一时刻对同一区域做写复制或恢复操作,否则会产生数据竞争与副本撕裂。

clustered-* 实现正是依托前述机制工作的:

  • 日志实例用 uuid 标识(集群各节点通过它指代同一个日志),而 is_remote_recovering()in_sync()-EWOULDBLOCK/阻塞语义,让本节点可以在无法立即判定远程状态时把判定交给用户态 daemon,由集群协调完成跨节点仲裁(见 include/linux/dm-dirty-log.hinclude/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_errorskeep_loglog_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() 为入口扩展自己的日志类型。

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