1. 块设备驱动基础
1.1 块设备概念
块设备(Block Device)是 Linux 内核中一类以固定大小数据块为基本传输单元、支持随机访问的 I/O 设备。与字符设备不同,块设备内部维护了请求队列和 I/O 调度器,能够对读写请求进行合并、排序和重排,从而大幅提升吞吐量。
典型的块设备包括:
- 硬盘(HDD / SSD)
- NVMe 固态盘
- SD/MMC 存储卡
- USB 大容量存储设备
- RAM 磁盘(brd / zram)
- MD 软件 RAID 和 DM 设备映射器
关键指标:块设备的最小寻址单元称为扇区(sector),传统为 512 字节,现代设备普遍使用 4096 字节(4K 扇区)。内核中所有块 I/O 操作都以扇区为单位进行偏移计算。
1.2 字符设备与块设备区别
字符设备(Char Device)和块设备是 Linux 设备驱动模型的两大基石,二者在设计哲学、数据流模型和内核接口上存在本质差异。下表从多个维度进行对比:
| 对比维度 | 字符设备 | 块设备 |
|---|---|---|
| 数据传输单位 | 字节流(无固定大小) | 固定大小数据块(扇区) |
| 访问模式 | 顺序流式访问为主 | 随机访问 + 顺序访问 |
| 内核接口 | file_operations(read/write/ioctl) |
block_device_operations + 请求队列 |
| I/O 调度 | 无调度,直接透传 | 有 I/O 调度器(电梯算法、CFQ、mq-deadline、BFQ) |
| 缓存策略 | 通常无内核缓存 | 通过页缓存(Page Cache)实现缓存 |
| 典型设备 | 串口、键盘、鼠标、音频 | 硬盘、SSD、SD 卡、软盘 |
| 设备号范围 | 主设备号 + 次设备号(无分区概念) | 主设备号 + 次设备号(次设备号用于分区) |
| 用户态访问 | open/read/write/ioctl |
open/read/write/ioctl + mmap + fsync |
核心差异总结:字符设备处理无结构字节流,驱动直接处理每次系统调用;块设备处理结构化数据块,驱动通过请求队列与 I/O 调度器交互,支持缓存、合并、排序和 DMA 传输。
1.3 块设备驱动架构概览
Linux 块设备驱动采用分层架构,从上到下依次为:
- VFS 层(虚拟文件系统)—— 处理文件操作,通过
address_space与页缓存交互。 - 页缓存层(Page Cache)—— 缓存文件数据块,减少物理 I/O。
- 通用块层(Generic Block Layer)—— 将文件系统请求转换为
bio结构,提交到块设备。 - I/O 调度层(I/O Scheduler)—— 对
bio进行合并、排序、分发。 - 块设备驱动层(Block Device Driver)—— 处理请求队列,与硬件交互完成数据传输。
- 硬件层(Hardware)—— 物理存储介质。
下图展示了块设备驱动在 Linux 存储栈中的位置与数据流路径:
1.4 块设备驱动核心数据结构
块设备驱动开发涉及以下关键内核数据结构,理解它们之间的关系是编写驱动的基础:
| 数据结构 | 所在头文件 | 作用描述 |
|---|---|---|
struct gendisk |
<linux/genhd.h> |
代表一个块设备实例,包含设备号、分区表、请求队列、操作函数集等 |
struct request_queue |
<linux/blkdev.h> |
管理 I/O 请求的队列,包含 I/O 调度器、限速、合并策略等 |
struct request |
<linux/blkdev.h> |
代表一个经过合并后的 I/O 请求,包含多个 bio 链 |
struct bio |
<linux/blk_types.h> |
块 I/O 操作的基本单元,描述一段连续内存与磁盘扇区之间的传输 |
struct block_device_operations |
<linux/blkdev.h> |
块设备操作函数集(open/release/ioctl/getgeo/check_events) |
驱动开发注意:在 Linux 5.x 之后,
struct request_queue 的分配方式从 blk_init_queue() 逐步迁移到 blk_mq_alloc_queue()(多队列块层)。新驱动应优先使用 blk-mq 框架,以获得更好的多核扩展性和 DMA 亲和性。
1.5 块设备驱动注册流程
一个典型的块设备驱动初始化流程包含以下步骤:
- 分配 gendisk:
alloc_disk(minors)— 分配struct gendisk实例,指定分区数量。 - 设置操作函数:填充
disk->fops指向block_device_operations结构体。 - 分配请求队列:
blk_mq_alloc_queue()或blk_init_queue(),绑定请求处理函数。 - 设置队列限速:配置
disk->queue的逻辑块大小、物理块大小、最大扇区数等。 - 注册设备:
add_disk(disk)— 将 gendisk 注册到内核,创建设备节点。 - 实现请求处理:在请求处理函数中解析
request或bio,完成数据传输。
最小驱动骨架示例(blk-mq 模式):
#include <linux/module.h>
#include <linux/blkdev.h>
#include <linux/blk-mq.h>
static struct gendisk *my_disk;
static struct blk_mq_tag_set my_tag_set;
static blk_status_t my_queue_rq(struct blk_mq_hw_ctx *hctx,
const struct blk_mq_queue_data *bd)
{
struct request *rq = bd->rq;
struct bio *bio = rq->bio;
// 处理 bio 中的数据
blk_mq_end_request(rq, BLK_STS_OK);
return BLK_STS_OK;
}
static const struct blk_mq_ops my_mq_ops = {
.queue_rq = my_queue_rq,
};
static int __init my_init(void)
{
// 1. 初始化 tag_set
my_tag_set.ops = &my_mq_ops;
my_tag_set.nr_hw_queues = 1;
my_tag_set.queue_depth = 128;
blk_mq_alloc_tag_set(&my_tag_set);
// 2. 分配请求队列
struct request_queue *q = blk_mq_alloc_queue(&my_tag_set, NULL, NULL);
blk_queue_logical_block_size(q, 512);
// 3. 分配 gendisk
my_disk = alloc_disk(1); // 1 个分区
my_disk->major = 0; // 动态分配主设备号
my_disk->first_minor = 0;
my_disk->fops = &my_fops;
my_disk->queue = q;
sprintf(my_disk->disk_name, "myblk");
// 4. 注册设备
add_disk(my_disk);
return 0;
}
static void __exit my_exit(void)
{
del_gendisk(my_disk);
put_disk(my_disk);
blk_mq_free_tag_set(&my_tag_set);
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");