[Linux] Linux标准块设备驱动详解:从原理到实现
Linux标准块设备驱动详解:从原理到实现
在Linux系统中,块设备是存储系统的核心组成部分,涵盖了硬盘、固态硬盘(SSD)、U盘、SD卡等各类持久化存储介质。与字符设备不同,块设备以固定大小的“块”为单位进行数据读写,支持随机访问,并通过复杂的I/O调度机制提升性能和设备寿命。本文将深入剖析Linux块设备驱动的架构、核心数据结构、注册流程及请求处理机制,并通过一个完整的基于内存的RAM磁盘驱动示例,帮助开发者掌握块设备驱动开发的关键技术。
文章目录
- Linux标准块设备驱动详解:从原理到实现
- 一、块设备概述:理解I/O模型的本质差异
- 二、核心数据结构解析
- 1. `block_device_operations`:设备操作接口
- 2. `gendisk`:磁盘设备的抽象
- 三、驱动注册与注销流程详解
- 1. 注册流程
- 2. 注销流程
- 四、I/O请求处理机制
- 1. 核心组件
- 2. 多队列(blk-mq)处理模式
- 定义多队列操作集
- 请求处理函数示例
- 五、完整示例:基于内存的RAM磁盘驱动
- 编译与测试
- 六、关键要点总结
- 结语
一、块设备概述:理解I/O模型的本质差异
在Linux设备模型中,设备主要分为字符设备、块设备和网络设备三类。其中,块设备(Block Device) 的显著特征是:
- 以块为单位传输数据:通常以512字节或4KB为基本单位(扇区),即使应用层请求非对齐数据,内核也会自动进行填充和裁剪。
- 支持随机访问:可以任意读写任意扇区,无需按顺序操作。
- 使用缓冲区缓存(Buffer Cache):内核通过Page Cache和Buffer Head机制缓存频繁访问的数据,减少对物理设备的直接访问,提升性能并延长设备寿命(尤其是SSD)。
- 依赖I/O调度器:内核提供多种I/O调度算法(如CFQ、Deadline、NOOP、BFQ),用于合并相邻请求、优化请求顺序,降低磁头寻道时间或提升SSD的并行性。
与之对比,字符设备(如串口、键盘)通常以字节流方式工作,不经过块层调度,也不支持随机访问。因此,块设备驱动需要更复杂的软件栈来处理请求的排队、合并、调度和完成通知。
二、核心数据结构解析
Linux内核通过一组关键数据结构来抽象和管理块设备。掌握这些结构是编写块设备驱动的基础。
1. block_device_operations
:设备操作接口
该结构体定义了用户空间与块设备交互的操作接口,类似于字符设备中的file_operations
。
struct block_device_operations {int (*open)(struct block_device *bdev, fmode_t mode);void (*release)(struct gendisk *disk, fmode_t mode);int (*ioctl)(struct block_device *bdev, fmode_t mode, unsigned cmd, unsigned long arg);int (*compat_ioctl)(struct block_device *bdev, fmode_t mode, unsigned cmd, unsigned long arg);unsigned int (*check_events)(struct gendisk *disk, unsigned int clearing);int (*revalidate_disk)(struct gendisk *disk);int (*getgeo)(struct block_device *bdev, struct hd_geometry *geo);void (*swap_slot_free_notify)(struct block_device *, unsigned long);struct module *owner;
};
open
/release
:设备打开和关闭时的回调,用于初始化硬件或释放资源。ioctl
:处理设备特定的控制命令,例如获取磁盘几何信息(CHS)、执行设备诊断等。getgeo
:返回磁盘的物理几何参数(柱面、磁头、扇区),主要用于兼容旧系统。owner
:指向所属模块,防止模块在使用中被卸载。
注意:现代驱动中,
open
和release
通常为空,因为块设备的打开由内核自动管理。
2. gendisk
:磁盘设备的抽象
gendisk
结构体代表一个完整的磁盘设备,包括主设备和所有分区。
struct gendisk {int major; // 主设备号int first_minor; // 起始次设备号int minors; // 支持的分区数量(1表示无分区)char disk_name[32]; // 设备名称,如 "myblk"struct block_device_operations *fops; // 操作函数集struct request_queue *queue; // 请求队列sector_t capacity; // 容量(以512字节扇区为单位)struct disk_part_tbl *part_tbl; // 分区表struct hd_struct part0; // 主设备信息// 其他成员...
};
关键操作流程:
- 分配:使用
alloc_disk(minors)
动态分配一个gendisk
对象。 - 初始化:设置设备号、名称、操作函数、请求队列和容量。
- 设置容量:通过
set_capacity(disk, sectors)
指定设备总扇区数。例如,1MB内存磁盘对应:set_capacity(disk, (1 * 1024 * 1024) / 512); // = 2048 扇区
- 注册:调用
add_disk(disk)
将设备注册到内核,此后设备节点(如/dev/myblk
)将自动出现在/dev
目录下。
重要提示:一旦调用
add_disk()
,驱动必须确保设备可正常响应I/O请求,否则可能导致系统挂起。
三、驱动注册与注销流程详解
块设备驱动的生命周期管理涉及设备号分配、磁盘对象初始化和内核注册。
1. 注册流程
static dev_t dev_num; // 设备号
static struct gendisk *disk;
static struct request_queue *queue;static int __init myblk_init(void)
{int ret;// 1. 动态分配设备号ret = register_blkdev(0, "myblk");if (ret <= 0) {printk(KERN_ERR "Failed to register block device\n");return -EIO;}dev_num = MKDEV(ret, 0); // 主设备号由内核返回// 2. 分配并初始化gendiskdisk = alloc_disk(1); // 支持1个分区if (!disk) {unregister_blkdev(MAJOR(dev_num), "myblk");return -ENOMEM;}disk->major = MAJOR(dev_num);disk->first_minor = 0;strcpy(disk->disk_name, "myblk");disk->fops = &my_blk_fops; // 操作函数disk->queue = queue; // 请求队列set_capacity(disk, 2048); // 1MB容量// 3. 注册到内核add_disk(disk);printk(KERN_INFO "myblk: Registered block device with major %d\n", MAJOR(dev_num));return 0;
}
2. 注销流程
static void __exit myblk_exit(void)
{if (disk) {del_gendisk(disk); // 从内核移除设备put_disk(disk); // 释放gendisk}if (queue) {blk_cleanup_queue(queue); // 清理请求队列}unregister_blkdev(MAJOR(dev_num), "myblk"); // 释放设备号
}
注意:
del_gendisk()
会阻止新的I/O请求进入,但不会等待正在进行的请求完成。因此,驱动应确保在调用此函数前所有请求已处理完毕。
四、I/O请求处理机制
块设备驱动的核心任务是处理来自文件系统的I/O请求。现代Linux内核采用多队列(Multi-Queue, blk-mq) 架构以提升多核系统的并发性能。
1. 核心组件
request_queue
:请求队列,由blk_mq_init_queue()
创建,管理所有待处理的I/O请求。bio
(Block I/O)结构体:描述一个I/O操作的基本单元,包含:bi_sector
:起始逻辑扇区号bi_size
:数据长度(字节)bi_io_vec
:指向bio_vec
数组,描述分散/聚集(scatter-gather)的内存页bi_end_io
:完成回调函数
2. 多队列(blk-mq)处理模式
传统请求队列使用request_fn
处理合并后的请求,而blk-mq直接处理bio
,简化了驱动逻辑。
定义多队列操作集
static struct blk_mq_ops my_mq_ops = {.queue_rq = my_queue_rq, // 核心请求处理函数.complete = my_complete_rq, // 可选:完成回调
};
请求处理函数示例
static blk_status_t my_queue_rq(struct blk_mq_hw_ctx *hctx, const struct blk_mq_queue_data *bd)
{struct request *req = bd->rq;struct bio *bio;sector_t sector = blk_rq_pos(req);unsigned int nr_bytes = blk_rq_bytes(req);// 遍历所有bio(支持合并请求)__rq_for_each_bio(bio, req) {void *data = bio_data(bio);unsigned int len = bio->bi_iter.bi_size;if (bio_data_dir(bio) == READ) {// 模拟读操作:从模拟存储区复制数据memcpy(data, disk_data + sector * 512, len);} else {// 模拟写操作memcpy(disk_data + sector * 512, data, len);}sector += len >> 9; // 转换为扇区数(512B/sector)}// 标记请求完成blk_mq_end_request(req, BLK_STS_OK);return BLK_STS_OK;
}
说明:
blk_mq_end_request()
会自动调用bio
的完成回调并释放资源。
五、完整示例:基于内存的RAM磁盘驱动
以下是一个可编译加载的完整RAM磁盘驱动,模拟一个1MB的块设备。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/blkdev.h>
#include <linux/genhd.h>
#include <linux/vmalloc.h>#define DEV_NAME "myramdisk"
#define DISK_SIZE (1 * 1024 * 1024) // 1MBstatic dev_t dev_num;
static struct request_queue *queue;
static struct gendisk *disk;
static unsigned char *disk_data;// 请求处理函数
static blk_status_t my_queue_rq(struct blk_mq_hw_ctx *hctx, const struct blk_mq_queue_data *bd)
{struct request *req = bd->rq;struct bio *bio;sector_t sector = blk_rq_pos(req);__rq_for_each_bio(bio, req) {void *data = bio_data(bio);unsigned int len = bio->bi_iter.bi_size;if (sector + (len >> 9) > DISK_SIZE / 512) {return BLK_STS_IOERR; // 越界检查}if (bio_data_dir(bio) == READ) {memcpy(data, disk_data + sector * 512, len);} else {memcpy(disk_data + sector * 512, data, len);}sector += len >> 9;}blk_mq_end_request(req, BLK_STS_OK);return BLK_STS_OK;
}// 多队列操作集
static struct blk_mq_ops my_mq_ops = {.queue_rq = my_queue_rq,
};// 模块初始化
static int __init myramdisk_init(void)
{int ret;// 1. 分配设备号ret = register_blkdev(0, DEV_NAME);if (ret < 0) return ret;dev_num = MKDEV(ret, 0);// 2. 分配模拟存储空间disk_data = vmalloc(DISK_SIZE);if (!disk_data) {unregister_blkdev(MAJOR(dev_num), DEV_NAME);return -ENOMEM;}memset(disk_data, 0, DISK_SIZE);// 3. 初始化请求队列queue = blk_mq_init_sq_queue(&tag_set, &my_mq_ops, 0, BLK_MQ_F_SHOULD_MERGE);if (IS_ERR(queue)) {vfree(disk_data);unregister_blkdev(MAJOR(dev_num), DEV_NAME);return PTR_ERR(queue);}// 4. 分配并初始化gendiskdisk = alloc_disk(1);if (!disk) {blk_cleanup_queue(queue);vfree(disk_data);unregister_blkdev(MAJOR(dev_num), DEV_NAME);return -ENOMEM;}disk->major = MAJOR(dev_num);disk->first_minor = 0;strcpy(disk->disk_name, DEV_NAME);disk->fops = &my_fops;disk->queue = queue;set_capacity(disk, DISK_SIZE / 512);disk->private_data = NULL;// 5. 注册设备add_disk(disk);printk(KERN_INFO "%s: RAM disk initialized (%d MB)\n", DEV_NAME, DISK_SIZE >> 20);return 0;
}// 模块退出
static void __exit myramdisk_exit(void)
{if (disk) {del_gendisk(disk);put_disk(disk);}if (queue) {blk_cleanup_queue(queue);}if (disk_data) {vfree(disk_data);}unregister_blkdev(MAJOR(dev_num), DEV_NAME);printk(KERN_INFO "%s: unloaded\n", DEV_NAME);
}module_init(myramdisk_init);
module_exit(myramdisk_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple RAM block device driver");
编译与测试
-
编译模块:
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
-
加载模块:
sudo insmod myramdisk.ko
-
验证设备:
ls /dev/myramdisk dmesg | tail
-
格式化并挂载:
sudo mkfs.ext4 /dev/myramdisk sudo mkdir /mnt/ramdisk sudo mount /dev/myramdisk /mnt/ramdisk
六、关键要点总结
- 设备号管理:使用
register_blkdev(0, ...)
实现主设备号动态分配,避免冲突。 - 多队列优先:现代驱动应使用
blk-mq
架构,直接处理bio
,提高并发性能。 - 内存分配:大容量设备应使用
vmalloc
而非kmalloc
,避免内存碎片。 - 错误处理:在
queue_rq
中进行边界检查,返回适当的blk_status_t
。 - 生命周期同步:确保
del_gendisk()
调用前无活跃I/O,防止内存访问错误。 - 性能优化:合理配置队列深度、硬件上下文数,启用I/O调度器(如Deadline用于SSD)。
结语
Linux块设备驱动是连接上层文件系统与底层存储硬件的桥梁。通过理解gendisk
、request_queue
和bio
等核心结构,掌握blk-mq请求处理机制,开发者可以构建高效、稳定的存储驱动。本文的RAM磁盘示例为学习和调试提供了基础框架,实际开发中可将其扩展为支持真实硬件(如PCIe SSD、NAND控制器)的复杂驱动。
更多细节可参考内核源码树中的drivers/block/
目录,如brd.c
(RAM磁盘)、null_blk.c
(空设备)等经典实现。
研究学习不易,点赞易。
工作生活不易,收藏易,点收藏不迷茫 :)