title: "Linux 6.12 内核块设备驱动开发实战:从基础到 vmem_disk 实例" date: "2026-05-24" author: "Linux Kernel Developer" tags: ["块设备驱动", "Linux内核", "ARM64", "blk-mq", "嵌入式开发"] categories: ["Linux驱动开发", "内核调试", "ARM64实战"] description: "基于Linux 6.12内核,手把手教你开发虚拟内存块设备驱动,掌握blk-mq架构核心技能"
在嵌入式开发中,我们经常需要存储数据。常见的存储介质包括:
今天我们要开发的 vmem_disk(虚拟内存磁盘),就是一种基于内存的块设备,它将一块内存区域模拟成块设备,提供标准块设备的读写接口。
vmem_disk 的典型应用场景:
块设备驱动是 Linux 内核中最核心的子系统之一。它负责:
在 Linux 6.x 内核中,块设备驱动框架已经全面向 blk-mq(multi-queue block layer)架构迁移,这带来了更好的多核扩展性和性能。
在动手开发之前,让我们先理解几个关键概念。为了让萌新也能理解,我会用通俗的语言解释:
想象一个城市里的停车场系统:
#define VMEM_DISK_MINORS 4 // 支持4个次设备号,即最多4个分区gendisk 就像块设备的"身份证"和"档案卡",记录了设备的所有信息:
major | ||
first_minor | ||
minors | ||
fops | ||
queue | ||
private_data |
什么是 blk-mq?
传统的块设备层是单队列的,就像只有一个收银台的超市,结账要排队。
blk-mq(Block Multi-Queue,多队列块层)就像超市改造后有 8 个收银台,多个收银员可以同时处理结账,效率大幅提升!
Linux 6.12 内核为什么选择 blk-mq?
让我们通过 Mermaid 图来理解整个块设备驱动的架构:

1.3 函数调用时序图
下面是应用程序读写 vmem_disk 的完整调用流程:

1.4 数据结构关系图

1.5 核心 API 一览表
register_blkdev() | ||
blk_mq_alloc_tag_set() | ||
blk_mq_alloc_disk() | ||
add_disk() | ||
blk_mq_start_request() | ||
blk_mq_end_request() |
以下是 [vmem_disk.c](file:///home/rlk/rzg/rz_linux-cip-rz-6.12-cip7-rt/testcode/driver/vmem/vmem_disk.c) 的完整实现,可直接编译使用:
/** * vmem_disk.c - 虚拟内存块设备驱动 * * 功能:创建一个基于内存的虚拟块设备 * 内核版本:Linux 6.12 * 硬件平台:ARM64 (RK3568/RZ/G2L) * * 作者:Linux Kernel Developer * 版本:1.0 */#include<linux/module.h> // 内核模块相关#include<linux/fs.h> // 文件系统相关#include<linux/blkdev.h> // 块设备相关#include<linux/blk-mq.h> // 多队列块层#include<linux/hdreg.h> // 硬盘geometry#include<linux/slab.h> // 内存分配#include<linux/spinlock.h> // 自旋锁#include<linux/vmalloc.h> // 虚拟内存分配/*====================================== * 宏定义 *======================================*/#define VMEM_DISK_NAME "vmem_disk"// 设备名称#define VMEM_DISK_MINORS 4 // 支持4个次设备号#define VMEM_DISK_SIZE (16 * 1024 * 1024) // 16MB虚拟磁盘/*====================================== * 设备结构体定义 *======================================*//** * vmem_disk_dev - 虚拟磁盘设备结构体 * * 这个结构体保存了设备的所有私有信息, * 通过 gendisk->private_data 访问。 */structvmem_disk_dev {unsignedchar *data; // 数据存储区(vmalloc分配)unsignedlong size; // 设备大小spinlock_t lock; // 保护open_count的自旋锁int open_count; // 打开计数,防止意外卸载structblk_mq_tag_settag_set;// blk-mq队列配置structgendisk *disk;// 指向 gendisk 的指针};/*====================================== * 全局变量 *======================================*/staticstructvmem_disk_dev *vmem_dev;// 设备实例指针staticint vmem_major; // 主设备号(动态分配)/*====================================== * I/O 请求处理函数(核心⭐) *======================================*//** * vmem_queue_rq - 处理块 I/O 请求 * * 这是驱动最核心的函数!每当有读写请求时, * 内核就会调用这个函数来处理。 * * @hctx: 硬件队列上下文 * @bd: 请求队列数据 * * 返回值:BLK_STS_OK 表示成功,其他表示错误 */staticblk_status_tvmem_queue_rq(struct blk_mq_hw_ctx *hctx,const struct blk_mq_queue_data *bd){structrequest *req = bd->rq;// 获取请求structbio_vecbvec;// bio段结构structreq_iteratoriter;// 迭代器sector_t sector = blk_rq_pos(req); // 起始扇区号int ret = BLK_STS_OK; // 默认成功// ⭐ 告诉内核我开始处理这个请求了 blk_mq_start_request(req);// 遍历请求中的所有 bio 段// 为什么要遍历?因为一个大请求可能被分成多个段 rq_for_each_segment(bvec, req, iter) {char *buffer = page_address(bvec.bv_page) + bvec.bv_offset;unsignedint seg_len = bvec.bv_len;// 🔒 边界检查:防止访问越界if (sector * SECTOR_SIZE + seg_len > vmem_dev->size) { pr_err("vmem_disk: access beyond device size!\n"); ret = BLK_STS_IOERR;break; }// 📖📝 根据读写方向执行数据拷贝if (rq_data_dir(req) == READ) {// 读:从虚拟内存拷贝到用户缓冲区memcpy(buffer, vmem_dev->data + sector * SECTOR_SIZE, seg_len); } else {// 写:从用户缓冲区拷贝到虚拟内存memcpy(vmem_dev->data + sector * SECTOR_SIZE, buffer, seg_len); }// 更新扇区号,为下一个段做准备 sector += seg_len / SECTOR_SIZE; }// ✅ 请求处理完成,通知内核 blk_mq_end_request(req, ret);return BLK_STS_OK;}/*====================================== * blk-mq 操作集定义 *======================================*//** * blk_mq_ops - blk-mq 操作函数集 * * 定义了驱动支持的回调函数。 * 这里我们只实现了 queue_rq,即请求处理函数。 */staticconststructblk_mq_opsvmem_mq_ops = { .queue_rq = vmem_queue_rq, // 核心:请求处理函数};/*====================================== * 设备操作函数 *======================================*//** * vmem_open - 打开块设备 * * 当应用程序打开 /dev/vmem_disk 时调用 */staticintvmem_open(struct gendisk *disk, blk_mode_t mode){structvmem_disk_dev *dev = disk->private_data; spin_lock(&dev->lock); dev->open_count++; // 增加打开计数 spin_unlock(&dev->lock); pr_info("vmem_disk: device opened (count=%d)\n", dev->open_count);return0;}/** * vmem_release - 释放块设备 * * 当应用程序关闭文件描述符时调用 */staticvoidvmem_release(struct gendisk *disk){structvmem_disk_dev *dev = disk->private_data; spin_lock(&dev->lock); dev->open_count--; // 减少打开计数 spin_unlock(&dev->lock); pr_info("vmem_disk: device released (count=%d)\n", dev->open_count);}/** * vmem_getgeo - 获取磁盘几何信息 * * 用于 fdisk 等分区工具识别设备参数 */staticintvmem_getgeo(struct block_device *bdev, struct hd_geometry *geo){structvmem_disk_dev *dev = bdev->bd_disk->private_data;// 计算柱面数 geo->cylinders = dev->size / (geo->heads * geo->sectors * SECTOR_SIZE); geo->heads = 4; // 磁头数 geo->sectors = 16; // 每磁道扇区数 geo->start = 0; // 起始扇区return0;}/*====================================== * 块设备操作集 *======================================*//** * block_device_operations - 设备操作集 * * 定义了设备支持的所有操作函数 */staticconststructblock_device_operationsvmem_blk_ops = { .owner = THIS_MODULE, .open = vmem_open, // 打开设备 .release = vmem_release, // 释放设备 .getgeo = vmem_getgeo, // 获取几何信息};/*====================================== * 驱动初始化(模块加载时执行) *======================================*//** * vmem_init - 驱动初始化函数 * * 模块加载时,内核会调用这个函数。 * 我们在这里完成所有的准备工作。 */staticint __init vmem_init(void){int ret;// 定义队列限制参数(Linux 6.12 新API)structqueue_limitslim = { .logical_block_size = SECTOR_SIZE, // 逻辑块大小 .physical_block_size = SECTOR_SIZE, // 物理块大小 }; pr_info("vmem_disk: initializing...\n");/* 步骤1:分配设备结构体 */// 使用 kzalloc 分配并清零内存 vmem_dev = kzalloc(sizeof(struct vmem_disk_dev), GFP_KERNEL);if (!vmem_dev) { pr_err("vmem_disk: failed to allocate device structure\n");return -ENOMEM; }/* 步骤2:分配虚拟内存作为存储空间 */ vmem_dev->size = VMEM_DISK_SIZE; vmem_dev->data = vmalloc(vmem_dev->size);if (!vmem_dev->data) { pr_err("vmem_disk: failed to allocate %lu bytes\n", vmem_dev->size); ret = -ENOMEM;goto out_free_dev; }memset(vmem_dev->data, 0, vmem_dev->size); // 清零内存// 初始化自旋锁和计数器 spin_lock_init(&vmem_dev->lock); vmem_dev->open_count = 0;/* 步骤3:注册块设备(动态分配主设备号) */// 第一个参数为0表示动态分配 vmem_major = register_blkdev(0, VMEM_DISK_NAME);if (vmem_major <= 0) { pr_err("vmem_disk: failed to register block device\n"); ret = -EBUSY;goto out_free_data; } pr_info("vmem_disk: registered with major number %d\n", vmem_major);/* 步骤4:配置 blk-mq tag_set */// 这是 Linux 6.x 内核的新方式 vmem_dev->tag_set.ops = &vmem_mq_ops; vmem_dev->tag_set.nr_hw_queues = 1; // 硬件队列数(简单设备用1) vmem_dev->tag_set.queue_depth = 128; // 队列深度 vmem_dev->tag_set.numa_node = NUMA_NO_NODE; // 不绑定特定NUMA节点 vmem_dev->tag_set.flags = BLK_MQ_F_SHOULD_MERGE; // 允许合并相邻请求 vmem_dev->tag_set.driver_data = vmem_dev; // 传递私有数据指针 ret = blk_mq_alloc_tag_set(&vmem_dev->tag_set);if (ret) { pr_err("vmem_disk: failed to allocate tag set: %d\n", ret);goto out_unregister; }/* 步骤5:分配并初始化 gendisk */// Linux 6.12 使用 blk_mq_alloc_disk 一步到位 vmem_dev->disk = blk_mq_alloc_disk(&vmem_dev->tag_set, &lim, vmem_dev);if (IS_ERR(vmem_dev->disk)) { ret = PTR_ERR(vmem_dev->disk); pr_err("vmem_disk: failed to allocate gendisk: %d\n", ret);goto out_free_tag_set; }/* 步骤6:配置 gendisk 参数 */ vmem_dev->disk->major = vmem_major; vmem_dev->disk->first_minor = 0; vmem_dev->disk->minors = VMEM_DISK_MINORS; vmem_dev->disk->fops = &vmem_blk_ops; vmem_dev->disk->private_data = vmem_dev;snprintf(vmem_dev->disk->disk_name, DISK_NAME_LEN, "%s", VMEM_DISK_NAME); set_capacity(vmem_dev->disk, vmem_dev->size / SECTOR_SIZE);/* 步骤7:将设备添加到系统 */ ret = add_disk(vmem_dev->disk);if (ret) { pr_err("vmem_disk: failed to add disk: %d\n", ret);goto out_put_disk; } pr_info("vmem_disk: Virtual block device registered\n"); pr_info(" - Size: %lu MB\n", vmem_dev->size / (1024 * 1024)); pr_info(" - Major: %d\n", vmem_major); pr_info(" - Device: /dev/%s\n", VMEM_DISK_NAME);return0;/* 错误处理:资源释放顺序很重要! */out_put_disk: put_disk(vmem_dev->disk);out_free_tag_set: blk_mq_free_tag_set(&vmem_dev->tag_set);out_unregister: unregister_blkdev(vmem_major, VMEM_DISK_NAME);out_free_data: vfree(vmem_dev->data);out_free_dev: kfree(vmem_dev);return ret;}/*====================================== * 驱动退出(模块卸载时执行) *======================================*//** * vmem_exit - 驱动退出函数 * * 模块卸载时,内核会调用这个函数。 * 我们在这里释放所有申请的资源。 */staticvoid __exit vmem_exit(void){ pr_info("vmem_disk: unloading...\n");// 资源释放顺序与初始化相反 del_gendisk(vmem_dev->disk); // 1. 从系统移除设备 put_disk(vmem_dev->disk); // 2. 释放 gendisk blk_mq_free_tag_set(&vmem_dev->tag_set); // 3. 释放 tag_set unregister_blkdev(vmem_major, VMEM_DISK_NAME); // 4. 注销块设备 vfree(vmem_dev->data); // 5. 释放虚拟内存 kfree(vmem_dev); // 6. 释放设备结构体 pr_info("vmem_disk: unloaded successfully\n");}/*====================================== * 模块入口和出口 *======================================*/module_init(vmem_init); // 指定初始化函数module_exit(vmem_exit); // 指定退出函数/*====================================== * 模块信息 *======================================*/MODULE_LICENSE("GPL"); // 开源协议MODULE_DESCRIPTION("Virtual Memory Disk Block Device Driver"); // 描述MODULE_AUTHOR("Linux Kernel Developer"); // 作者MODULE_VERSION("1.0"); // 版本驱动编译需要交叉编译器,Makefile 如下:
#========================================# vmem_disk Makefile# 适用于 Linux 6.12 内核# 目标平台:ARM64 (RK3568/RZ/G2L)#========================================# 内核源码目录KDIR := /home/rlk/rzg/rz_linux-cip-rz-6.12-cip7-rt# 当前目录PWD := $(shell pwd)# 编译目标obj-m := vmem_disk.o# 默认目标all: @echo "========================================" @echo "Building vmem_disk for ARM64..." @echo "Kernel source: $(KDIR)" @echo "========================================"$(MAKE) -C $(KDIR) M=$(PWD) modules ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- @echo "" @echo "Build complete! Check vmem_disk.ko"# 清理clean: @echo "Cleaning..."$(MAKE) -C $(KDIR) M=$(PWD) clean# 安装到目标板(需要配置IP)install: all scp vmem_disk.ko root@192.168.3.133:/root/# 查看模块信息info: @echo "========================================" @echo "vmem_disk Module Info" @echo "========================================" @echo "Source: $(PWD)/vmem_disk.c" @echo "Output: $(PWD)/vmem_disk.ko" @echo "Target: ARM64 RK3568" @echo "" @if [ -f vmem_disk.ko ]; then \ echo "Module size:"; \ size vmem_disk.ko; \ echo ""; \ echo "Module info:"; \ modinfo vmem_disk.ko; \else \ echo "vmem_disk.ko not found. Run 'make' first."; \ fi环境信息:
检查编译环境:
# 检查交叉编译器aarch64-linux-gnu-gcc --version# 检查内核源码ls -la /home/rlk/rzg/rz_linux-cip-rz-6.12-cip7-rt/Makefile# 检查内核版本cat /home/rlk/rzg/rz_linux-cip-rz-6.12-cip7-rt/include/config/kernel.release# 进入驱动目录cd testcode/driver/vmem# 编译驱动make编译输出示例:
========================================Building vmem_disk for ARM64...Kernel source: /home/rlk/rzg/rz_linux-cip-rz-6.12-cip7-rt========================================make -C /home/rlk/rzg/rz_linux-cip-rz-6.12-cip7-rt M=/home/rlk/rzg/rz_linux-cip-rz-6.12-cip7-rt/testcode/driver/vmem modules ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-make[1]: Entering directory '/home/rlk/rzg/rz_linux-cip-rz-6.12-cip7-rt' CC [M] /home/rlk/rzg/rz_linux-cip-rz-6.12-cip7-rt/testcode/driver/vmem/vmem_disk.o MODPOST /home/rlk/rzg/rz_linux-cip-rz-6.12-cip7-rt/testcode/driver/vmem/Module.symvers CC [M] /home/rlk/rzg/rz_linux-cip-rz-6.12-cip7-rt/testcode/driver/vmem/vmem_disk.mod.o LD [M] /home/rlk/rzg/rz_linux-cip-rz-6.12-cip7-rt/testcode/driver/vmem/vmem_disk.komake[1]: Leaving directory '/home/rlk/rzg/rz_linux-cip-rz-6.12-cip7-rt'Build complete! Check vmem_disk.ko# 查看编译产物-rw-r--r-- 1 rlk rlk 129120 May 24 22:34 vmem_disk.ko# 拷贝驱动到目标设备scp vmem_disk.ko root@192.168.3.133:/root/# SSH 登录到目标设备ssh root@192.168.3.133# 加载驱动insmod /root/vmem_disk.ko# 查看内核日志dmesg | tail -10预期输出:
vmem_disk: initializing...vmem_disk: registered with major number 253vmem_disk: Virtual block device registered - Size: 16 MB - Major: 253 - Device: /dev/vmem_disk# 查看已加载的模块lsmod | grep vmem_disk# 查看块设备cat /proc/partitions# 查看设备节点ls -la /dev/vmem_disk# 查看内核日志dmesg | grep vmem_disk预期输出:
# lsmodvmem_disk 12288 0 - Live 0xffff800079b60000 (O)# cat /proc/partitionsmajor minor #blocks name 253 0 16384 vmem_disk# ls -la /dev/vmem_diskbrw------- 1 root root 253, 0 Jan 1 08:03 /dev/vmem_disk# 1. 写入测试数据echo'Hello from vmem_disk!' > /tmp/test.txtdd if=/tmp/test.txt of=/dev/vmem_disk bs=512 count=1# 2. 读回数据验证dd if=/dev/vmem_disk of=/tmp/readback.txt bs=512 count=1cat /tmp/readback.txt# 3. 卸载驱动(确保设备未被使用)rmmod vmem_disk测试结果:
# 写入测试0+1 records in0+1 records out22 bytes (22B) copied, 0.000440 seconds, 48.8KB/s# 读回测试1+0 records in1+0 records out512 bytes (512B) copied, 0.000394 seconds, 1.2MB/sHello from vmem_disk!# 卸载测试vmem_disk: unloading...vmem_disk: unloaded successfully错误信息:
vmem_disk: version magic '6.12.43-cip7-rt12-gd5afead4d776-dirty' should be '6.12.43-cip7-rt12'insmod: init_module failed: Invalid parameters原因:驱动编译时使用的内核版本与目标系统运行的内核版本不一致
解决方案:
# 方案1:在目标系统上重新编译驱动# 方案2:使用 --force-vermagic 强制加载(不推荐)insmod --force-vermagic vmem_disk.ko# 方案3:确保使用相同版本的内核源码编译错误信息:
fatal error: linux/blkdev.h: No such file or directory解决方案:
# 确保 KDIR 指向正确的内核源码目录KDIR := /path/to/your/kernel/source错误信息:
aarch64-linux-gnu-gcc: command not found解决方案:
# 安装交叉编译工具链(Ubuntu/Debian)sudo apt-get install gcc-aarch64-linux-gnu# 或使用内核自带的工具链export CROSS_COMPILE=aarch64-linux-gnu-export ARCH=arm64症状:/dev/vmem_disk 不存在
原因:驱动加载后没有自动创建设备节点(需要 root 权限或使用 mdev)
解决方案:
# 方法1:手动创建设备节点mknod /dev/vmem_disk b 253 0chmod 666 /dev/vmem_disk# 方法2:使用 udev 规则(需要将规则文件拷贝到目标板)# 创建 /etc/udev/rules.d/99-vmem.rulesecho'KERNEL=="vmem_disk", SUBSYSTEM=="block", MODE="666"' > /etc/udev/rules.d/99-vmem.rulesudevadm settle症状:/dev/vmem_disk: Permission denied
解决方案:
# 修改设备权限chmod 666 /dev/vmem_disk# 或以 root 身份运行su -alloc_disk()blk_init_queue() | blk_mq_alloc_disk() | |
fmode_t | blk_mode_t | |
struct request_queue | blk_mq_alloc_disk() 间接创建 |
vmem_dev->tag_set.queue_depth = 256; // 默认128,可增大到256或512vmem_dev->tag_set.flags = BLK_MQ_F_SHOULD_MERGE; // 允许合并相邻请求对于真实硬件,考虑使用 DMA 减少 CPU 开销:
// 启用 DMA(需要硬件支持)blk_queue_flag_set(QUEUE_FLAG_NOMERGES, queue); // 禁用合并以便 DMA通过本文,你应该掌握了以下核心知识点:
✅ 块设备驱动基础
✅ blk-mq 架构
✅ 驱动开发流程
✅ 调试技巧
如果你对块设备驱动感兴趣,可以继续学习:
📚 进阶内容:
🔧 相关项目: