Linux DeviceMapper框架解析:从原理到实战开发

Linux DeviceMapper框架解析:从原理到实战开发
1. DeviceMapper 到底是什么解决了什么问题如果你在 Linux 系统里敲过mount或lsblk命令大概率会看到一堆名字像dm-0、dm-1的设备。这些就是 DeviceMapper简称 DM创建的虚拟块设备。DM 不是某个具体功能而是一个内核级的框架它的核心价值是让存储管理变得模块化、可插拔。简单说DM 框架解决了存储场景中“策略与机制分离”的问题。用户空间工具比如 LVM、dm-crypt负责定义存储策略——像“怎么加密”“如何做快照”“怎样条带化”而内核里的 DM 框架只提供通用的映射机制。这样做的好处是新增一种存储功能时不需要重写整个块设备层只要实现一个对应的“target driver”插件就行。DM 最典型的应用包括磁盘加密dm-crypt逻辑卷管理LVM 底层用的 dm-linear快照与克隆dm-snapshot磁盘校验dm-verityAndroid 系统完整性保护缓存加速dm-cache如果你需要在内核层面定制块设备的 IO 行为或者想理解现有存储工具的背后原理DM 框架是必须啃下来的部分。2. DM 框架里三个关键对象Mapped Device、Mapping Table、Target DeviceDM 的架构围绕三个核心概念展开理解它们的关系比直接看代码更重要。2.1 Mapped Device你看到的虚拟设备/dev/dm-0、/dev/dm-1这些就是 Mapped Device。它不对应任何物理硬件而是内核生成的一个逻辑块设备。用户态的程序可以像读写普通硬盘一样操作它但所有的 IO 请求都会被 DM 拦截并转发。Mapped Device 的关键数据结构是mapped_device定义在drivers/md/dm.c。这个结构体里存了请求队列request_queue指向当前激活的映射表dm_table的指针内存池用于 IO 请求的克隆锁和引用计数// 示例代码非完整源码 struct mapped_device { struct dm_table *map; struct request_queue *queue; struct bio_set *bio_split; // ... };2.2 Mapping Table映射规则表每个 Mapped Device 都关联一张映射表dm_table这张表定义了“逻辑地址区间”到“物理设备处理规则”的对应关系。表里按 B 树结构存放多个dm_target每个dm_target负责一段连续的逻辑区间。比如一个 10GB 的 Mapped Device可能前 5GB 映射到/dev/sda1后 5GB 映射到/dev/sdb1这就是两个dm_target。2.3 Target Device实际干活的设备Target Device 不是指整块物理硬盘而是指“物理设备的一段区间”“处理规则”。规则由具体的target_type实现。比如linear线性映射直接转发 IOcrypt加密/解密后再转发striped条带化分发 IOdm_target结构里最重要的字段是sector_t begin本 target 负责的逻辑起始扇区sector_t len负责的扇区长度struct target_type *type处理规则的插件void *private指向插件私有数据的指针struct dm_target { struct dm_table *table; struct target_type *type; sector_t begin; sector_t len; void *private; // ... };3. 从零创建一个 DM 设备的完整流程光看结构可能还是抽象我们走一遍创建一个 DM 设备的内核流程。这里以最简单的linear映射为例。3.1 用户空间准备映射表首先用户态工具比如dmsetup需要计算好映射关系然后通过 IOCTL 把规则传给内核。规则格式类似0 1024 linear /dev/sda1 0 1024 2048 linear /dev/sdb1 0意思是逻辑 0~1024 扇区映射到/dev/sda1的 0~1024 扇区逻辑 1024~2048 扇区映射到/dev/sdb1的 0~1024 扇区3.2 内核创建 mapped_device当内核收到DM_DEV_CREATEIOCTL 时会调用dm_create函数分配一个mapped_device结构体初始化它的请求队列、锁、内存池在/dev下创建设备节点此时设备还没映射表所以还不能 IO。3.3 加载映射表接着用户空间发送DM_TABLE_LOADIOCTL内核流程解析每行规则为每个dm_target分配内存根据类型名如linear找到对应的target_type调用target_type-ctr构造函数初始化private数据把所有的dm_target按 B 树组织成dm_table把表设置到mapped_device-map3.4 激活设备最后发送DM_DEV_SUSPENDIOCTL带激活标志设备就进入可用的状态。这时你可以格式化、挂载这个/dev/dm-X设备了。注意DM 设备创建过程中最常出问题的是扇区计算错误。比如物理设备的大小不够、扇区对齐问题、多个 target 之间有间隙或重叠。用户空间工具通常会自动检查但自己写驱动时务必验算。4. IO 请求在 DM 框架里怎么流动DM 框架最核心的价值体现在 IO 路径上。当一个 bio 请求发给 Mapped Device 时会发生以下步骤4.1 请求入口bio 到达mapped_device的请求队列后DM 的dm_make_request函数被调用根据 bio 的扇区偏移在映射表的 B 树中找到对应的dm_target克隆一个或多个新 bio因为原始 bio 可能跨多个 target 区间调整克隆 bio 的扇区偏移指向 target device 的物理位置调用dm_target-type-map方法4.2 Target 的 map 方法每个 target type 都必须实现map方法。这个方法决定如何处理 biolinear直接发给底层设备crypt加密后发给底层设备snapshot可能转发到原设备或快照区map方法返回一个DM_MAPIO_*状态DM_MAPIO_SUBMITTEDtarget 已经提交了 bioDM_MAPIO_REMAPPED需要 DM 重新映射比如条带化DM_MAPIO_KILL丢弃这个 bio4.3 IO 完成回调当底层设备处理完 bio 后完成中断会触发dm_end_io如果 bio 是克隆的释放克隆资源向上层报告 IO 完成如果是最后一个克隆 bio完成原始 bio这个“克隆-转发-回调”机制使得 DM 可以实现复杂的 IO 链比如加密后再做快照。5. 自己写一个 Target Driver 的实战要点DM 框架的扩展性体现在可以自定义 target driver。假设我们要实现一个简单的“日志记录 target”记录所有经过的 IO。5.1 定义 target_typestatic struct target_type log_target { .name log, .version {1, 0, 0}, .module THIS_MODULE, .ctr log_ctr, // 设备构造 .dtr log_dtr, // 设备销毁 .map log_map, // IO 处理 .status log_status, // 状态查询 };5.2 实现构造和销毁static int log_ctr(struct dm_target *ti, unsigned int argc, char **argv) { struct log_c *lc; // 检查参数个数 if (argc ! 1) { ti-error Invalid argument count; return -EINVAL; } // 分配私有数据结构 lc kzalloc(sizeof(*lc), GFP_KERNEL); if (!lc) return -ENOMEM; // 初始化日志文件简化版实际要用内核文件接口 lc-log_enabled true; ti-private lc; return 0; } static void log_dtr(struct dm_target *ti) { struct log_c *lc ti-private; kfree(lc); }5.3 实现 map 方法static int log_map(struct dm_target *ti, struct bio *bio) { struct log_c *lc ti-private; if (lc-log_enabled) { printk(KERN_INFO LOG: bio op%d sector%llu size%u\n, bio_op(bio), bio-bi_iter.bi_sector, bio-bi_iter.bi_size); } // 直接转发给下一个设备 dm_bio_restore(get_bio_details_from_bio(bio), bio); bio_set_dev(bio, ti-table-devices-bdev); return DM_MAPIO_REMAPPED; }5.4 注册和测试在模块初始化时注册static int __init dm_log_init(void) { int r dm_register_target(log_target); if (r 0) DMERR(register failed %d, r); return r; } module_init(dm_log_init);然后用 dmsetup 测试# 创建测试设备 echo 0 1024 log /dev/sda1 0 | dmsetup create mylog写 target driver 最容易踩的坑是 bio 克隆和内存管理。一定要清楚每个 bio 的生命周期该克隆时克隆该释放时释放。另外map 方法不能阻塞因为它在块 IO 软中断上下文中执行。6. DM 框架的边界条件和性能考量DM 很强大但不是万能的。在实际使用中要注意这些边界6.1 性能开销每个 IO 请求在 DM 框架中至少经历一次映射查找B 树搜索一次 bio 克隆如果跨 target一次 target 的 map 方法调用在高速存储设备上如 NVMe这个开销可能成为瓶颈。解决方法避免过多的 target 层级简化 map 方法逻辑使用 DM 的“批量提交”优化新版本内核支持6.2 内存占用每个dm_target和映射表都要占用内核内存。如果映射表非常庞大比如数百万个条目可能耗尽内核空间。这时需要考虑使用更大的扇区粒度减少条目数或者换用其他映射方案6.3 错误处理DM 框架的错误处理相对复杂因为一个 IO 可能涉及多个设备某个底层设备故障时需要正确上报错误快照 target 的 COW 操作可能失败加密 target 可能遇到加密算法错误好的 target driver 应该能妥善处理各种错误场景避免内核崩溃。7. 实际案例Android 中的 dm-verityAndroid 从 4.4 开始使用 dm-verity 来保护系统分区完整性。它的工作原理是系统分区被分成多个 4KB 块每个块计算一个哈希值形成默克尔树根哈希值烧录在系统信任区启动时dm-verity 验证每个读取块的哈希当检测到篡改时dm-verity 可以配置为返回 I/O 错误普通模式重启设备重启模式标记分区为损坏禁用模式这就是 DM 框架的典型应用在块层透明地添加安全功能上层应用无感知。理解 DM 框架后你再看到/dev/dm-*设备时就能想到背后是一套完整的映射机制和可插拔的 target driver。这种模块化设计正是 Linux 内核在很多领域的成功之道。