2. Linux块设备驱动基础
2.1 块设备与字符设备的本质区别
在 Linux 设备驱动模型中,I/O 设备被划分为两大类型:字符设备和块设备。理解两者的区别是编写块设备驱动的第一道门槛。
| 对比维度 | 字符设备 (Char Device) | 块设备 (Block Device) |
|---|---|---|
| 数据访问方式 | 按字节流顺序访问,无缓冲区 | 按固定大小的块(通常为 512B/4KB)随机访问 |
| 核心接口 | file_operations(open/read/write/ioctl) |
block_device_operations(open/release/ioctl) |
| I/O 调度 | 无内核级调度,直接透传 | 通过 request_queue 进行合并、排序、调度 |
| 缓存策略 | 通常无页缓存(raw 模式) | 通过 页缓存 (Page Cache) 和 块层缓存 加速 |
| 挂载文件系统 | 不支持 | 支持(ext4、xfs、btrfs 等) |
| 典型设备 | 串口、键盘、鼠标、传感器 | 硬盘、SSD、SD 卡、RAM 磁盘 |
| 主设备号范围 | 动态分配,无固定限制 | 通常使用固定主设备号(如 8 为 SCSI 磁盘) |
核心认知:字符设备是“流式”的,块设备是“寻址式”的。文件系统只能挂载在块设备之上,因为文件系统需要随机读写、缓存管理和 I/O 调度——这些正是块设备子系统的核心能力。
2.2 块设备驱动框架全景
Linux 块设备驱动由三个核心组件构成,它们共同协作完成从 VFS 到物理设备的 I/O 路径:
- gendisk:代表一个逻辑磁盘或分区,是块设备在内存中的抽象。
- request_queue:I/O 请求队列,负责接收、合并、排序来自文件系统的请求。
- block_device_operations:块设备操作函数表,定义 open/release/ioctl 等回调。
2.3 gendisk:磁盘的抽象表示
struct gendisk 是块设备驱动的核心数据结构,它代表一个独立的磁盘或分区设备。每个 gendisk 实例对应 /dev 下的一个块设备节点。
struct gendisk {
int major; // 主设备号
int first_minor; // 起始次设备号
int minors; // 次设备号数量(分区数+1)
char disk_name[DISK_NAME_LEN]; // 设备名称,如 "vdab"
struct block_device_operations *fops; // 操作函数表
struct request_queue *queue; // 关联的请求队列
void *private_data; // 驱动私有数据
sector_t capacity; // 磁盘容量(扇区数)
int flags; // 标志位(如 GENHD_FL_NO_PART)
struct device *driverfs_dev; // 设备模型中的 device
// ... 其他字段
};
关键字段解读:
major / first_minor / minors:决定了设备节点的编号范围和分区能力。例如minors = 16表示支持 15 个分区 + 1 个整盘。capacity:以 512 字节扇区为单位。若设备容量为 1GB,则capacity = 2097152。private_data:驱动开发者最常用的字段,通常指向自定义的设备结构体。
2.4 request_queue:I/O 请求的调度中枢
struct request_queue 是块设备 I/O 的入口和调度中心。所有发往该设备的读写请求都会先进入队列,经过合并、排序后,再传递给驱动处理。
| 队列特性 | 说明 | 典型配置 |
|---|---|---|
| I/O 调度器 | 决定请求的处理顺序(电梯算法) | mq-deadline / none / kyber |
| 请求合并 | 将相邻扇区的请求合并为一个大请求 | 默认开启,减少中断次数 |
| 队列深度 | 允许同时排队的最大请求数 | NVMe 通常为 1024 |
| make_request_fn | 自定义请求处理函数(绕过调度器) | 常用于 RAM 磁盘或简单块设备 |
| queue_flag | 控制队列行为(如 QUEUE_FLAG_NONROT) | 标记非旋转设备(SSD) |
创建请求队列的标准方式:
// 分配请求队列(blk-mq 方式,现代驱动推荐)
struct request_queue *q;
q = blk_mq_alloc_queue(&tag_set, NULL, NULL);
blk_queue_make_request(q, my_make_request_fn);
// 或传统方式(单队列,适用于简单设备)
q = blk_init_queue(my_request_fn, &my_lock);
避坑提示:传统
blk_init_queue 使用全局锁,多核并发性能差。新驱动应优先使用 blk-mq(多队列)框架,它支持每个 CPU 核独立队列,大幅提升 SMP 扩展性。
2.5 block_device_operations:驱动回调函数表
struct block_device_operations 定义了块设备驱动的标准回调接口。与字符设备的 file_operations 不同,它不包含 read/write——这些由块层通过请求队列处理。
struct block_device_operations {
int (*open)(struct block_device *, fmode_t);
void (*release)(struct gendisk *, fmode_t);
int (*ioctl)(struct block_device *, fmode_t, unsigned, unsigned long);
int (*compat_ioctl)(struct block_device *, fmode_t, unsigned, unsigned long);
unsigned int (*check_events)(struct gendisk *, unsigned int);
int (*getgeo)(struct block_device *, struct hd_geometry *);
int (*set_capacity)(struct gendisk *, unsigned long long);
struct module *owner;
};
| 回调函数 | 触发时机 | 典型实现 |
|---|---|---|
open |
设备被打开(mount / fdisk / dd) | 检查设备状态、增加引用计数 |
release |
设备被关闭 | 减少引用计数、清理资源 |
ioctl |
用户态 ioctl 系统调用 | 处理 BLKGETSIZE64、HDIO_GETGEO 等 |
getgeo |
获取磁盘几何参数 | 返回 CHS(柱面/磁头/扇区)值 |
check_events |
检查介质是否变更 | 用于可移动设备(如 USB 磁盘) |
2.6 块设备驱动初始化流程
一个完整的块设备驱动初始化遵循以下步骤。以简单的 RAM 磁盘驱动为例:
- 分配 gendisk:
alloc_disk(minors) - 设置操作函数表:
disk->fops = &my_bd_ops - 创建请求队列:
disk->queue = blk_mq_alloc_queue(...) - 设置容量:
set_capacity(disk, sectors) - 设置私有数据:
disk->private_data = my_dev - 注册设备:
add_disk(disk)
2.7 完整的最小块设备驱动骨架
以下代码展示了一个极简 RAM 磁盘驱动的核心注册部分,省略了请求处理细节:
#include <linux/module.h>
#include <linux/blkdev.h>
#include <linux/hdreg.h>
static struct my_ramdisk {
struct gendisk *disk;
struct request_queue *queue;
spinlock_t lock;
u8 *data; // 模拟存储空间
unsigned long size; // 设备大小(字节)
} *my_dev;
static const struct block_device_operations my_bd_ops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.getgeo = my_getgeo,
};
static int __init my_init(void)
{
int ret = -ENOMEM;
// 1. 分配设备结构
my_dev = kzalloc(sizeof(*my_dev), GFP_KERNEL);
if (!my_dev)
return -ENOMEM;
spin_lock_init(&my_dev->lock);
// 2. 分配存储空间(64MB)
my_dev->size = 64 * 1024 * 1024;
my_dev->data = vmalloc(my_dev->size);
if (!my_dev->data)
goto err_free_dev;
// 3. 分配请求队列(blk-mq)
my_dev->queue = blk_mq_alloc_queue(&my_tag_set, NULL, NULL);
if (IS_ERR(my_dev->queue)) {
ret = PTR_ERR(my_dev->queue);
goto err_free_data;
}
blk_queue_make_request(my_dev->queue, my_make_request_fn);
// 4. 分配 gendisk(支持 8 个分区)
my_dev->disk = alloc_disk(8);
if (!my_dev->disk)
goto err_free_queue;
// 5. 填充 gendisk 字段
my_dev->disk->major = my_major;
my_dev->disk->first_minor = 0;
my_dev->disk->fops = &my_bd_ops;
my_dev->disk->queue = my_dev->queue;
my_dev->disk->private_data = my_dev;
snprintf(my_dev->disk->disk_name, 32, "myram");
set_capacity(my_dev->disk, my_dev->size / 512);
// 6. 注册设备
add_disk(my_dev->disk);
pr_info("myram: 64MB RAM disk registered\n");
return 0;
err_free_queue:
blk_mq_free_queue(my_dev->queue);
err_free_data:
vfree(my_dev->data);
err_free_dev:
kfree(my_dev);
return ret;
}
module_init(my_init);
学习要点:上述骨架展示了块设备驱动的“注册三部曲”——分配资源 → 填充结构 → 注册设备。实际项目中,请求处理函数
my_make_request_fn 需要实现 BIO 到存储介质的映射,这部分将在后续章节详细展开。
2.8 本章小结
- 块设备与字符设备的本质区别在于:随机块访问 vs 顺序字节流,以及是否支持文件系统挂载。
- 块设备驱动框架由 gendisk(磁盘抽象)、request_queue(请求调度)、block_device_operations(回调函数)三部分组成。
- 初始化必须遵循严格顺序:
alloc_disk → 设置字段 → 创建队列 → set_capacity → add_disk。 - 现代驱动应使用 blk-mq 框架以获得更好的多核扩展性。