2. 块设备核心数据结构
在嵌入式 Linux 块设备驱动中,三个核心数据结构构成了驱动与内核块 I/O 层之间的桥梁:gendisk(通用磁盘描述)、block_device_operations(块设备操作函数表)和 request_queue(请求队列)。理解这三者的角色、成员关系以及初始化流程,是编写任何块设备驱动的第一步。
核心关系速记:每个块设备对应一个
gendisk 实例;gendisk 内部绑定一个 request_queue 用于 I/O 请求排队与调度;block_device_operations 则提供打开、关闭、获取介质信息等控制操作。三者协同完成从 VFS → 块层 → 驱动的完整路径。
2.1 gendisk 结构体详解
gendisk 是内核中代表“一个磁盘”或“一个分区表容器”的核心对象。它位于 <linux/genhd.h>,驱动通过 alloc_disk 系列函数分配,并在注册时与块设备号绑定。
2.1.1 关键成员
| 成员 | 类型 | 说明 |
|---|---|---|
major | int | 主设备号 |
first_minor | int | 起始次设备号 |
minors | int | 支持的次设备号数量(分区数 + 1) |
disk_name | char[32] | 设备名称,如 mmcblk0 |
fops | const struct block_device_operations * | 指向操作函数表 |
queue | struct request_queue * | 指向该磁盘的请求队列 |
private_data | void * | 驱动私有数据指针,通常指向宿主结构体 |
flags | int | 标志位,如 GENHD_FL_REMOVABLE |
part | struct disk_part_tbl * | 分区表信息 |
part0 | struct hd_struct | 代表整个磁盘的分区结构 |
驱动初始化典型流程:
alloc_disk(minors) → 设置 major、fops、queue、private_data → add_disk(disk) 使设备对用户空间可见。
2.1.2 gendisk 生命周期与分区管理
每个 gendisk 通过 minors 字段声明支持的次设备号数量。例如 SD 卡驱动通常设置 minors = 16,其中 part0 代表整卡,part1 ~ part15 对应分区。内核块层在 add_disk 时自动扫描分区表并创建分区块设备。
常见错误:忘记设置
disk->queue 或 disk->fops 就调用 add_disk,会导致内核 oops。务必在注册前完成所有核心字段的赋值。
2.2 block_device_operations 结构体
block_device_operations(简称 bd_ops)定义在 <linux/blkdev.h>,是块设备驱动向内核暴露的控制接口。与字符设备的 file_operations 不同,块设备的读写操作不通过此结构体,而是通过 request_queue 处理。
2.2.1 核心回调函数
| 回调 | 原型 | 用途 |
|---|---|---|
open | int (*open)(struct block_device *, fmode_t) | 设备打开时调用,可用于电源管理或独占访问控制 |
release | void (*release)(struct gendisk *, fmode_t) | 最后一个引用关闭时调用 |
ioctl | int (*ioctl)(struct block_device *, fmode_t, unsigned, unsigned long) | 处理自定义控制命令,如 BLKGETSIZE |
compat_ioctl | int (*compat_ioctl)(struct block_device *, fmode_t, unsigned, unsigned long) | 32位兼容 ioctl |
getgeo | int (*getgeo)(struct block_device *, struct hd_geometry *) | 返回 CHS 几何参数(磁头、柱面、扇区) |
media_changed | int (*media_changed)(struct gendisk *) | 检查可移动介质是否更换(如 SD 卡) |
revalidate_disk | int (*revalidate_disk)(struct gendisk *) | 介质更换后重新验证容量与分区表 |
SD 卡驱动典型实现:
open 中增加电源计数;media_changed 检测卡检测引脚电平;revalidate_disk 重新读取 CSD 寄存器更新容量;getgeo 返回固定 CHS 参数(如 255 磁头、63 扇区/磁道)。
2.2.2 与 file_operations 的对比
| 维度 | block_device_operations | file_operations |
|---|---|---|
| 读写路径 | 不包含 read/write,由 request_queue 处理 | 包含 read/write,直接处理 |
| 打开语义 | open 可被多次调用,需引用计数 | open 每次打开都调用 |
| 主要用途 | 控制、几何、介质管理 | 数据读写与控制 |
| 注册方式 | 赋值给 gendisk->fops | 赋值给 cdev->ops |
2.3 request_queue 结构体
request_queue 是块设备 I/O 数据路径的核心调度与排队单元。驱动通过 blk_init_queue 或 blk_alloc_queue + blk_queue_make_request 创建队列,并将队列绑定到 gendisk。
2.3.1 关键成员与功能
| 成员/接口 | 说明 |
|---|---|
make_request_fn | 驱动自定义的请求处理函数(非标准队列模式) |
request_fn | 标准请求处理函数,由 blk_init_queue 注册 |
elevator | I/O 调度器实例(如 CFQ、deadline、noop) |
queue_flags | 队列标志,如 QUEUE_FLAG_NONROT(非旋转设备) |
limits | struct queue_limits,包含最大扇区数、对齐要求等 |
queuedata | 驱动私有数据指针,通常指向宿主结构体 |
2.3.2 两种请求处理模式
- 标准请求队列(request_fn 模式):调用
blk_init_queue(request_fn, lock),内核将合并排序后的struct request传递给驱动的request_fn。驱动遍历请求并处理每个 bio。 - 自制请求队列(make_request_fn 模式):调用
blk_alloc_queue(GFP_KERNEL)+blk_queue_make_request(q, make_request_fn),驱动直接接收struct bio,绕过 I/O 调度器。适用于简单设备(如 ramdisk、MTD 块层)。
选择建议:SD/MMC 卡驱动通常使用标准请求队列,以便利用 I/O 调度器优化顺序访问;而简单的内存模拟块设备或 MTD 块设备常使用 make_request_fn 模式以减少开销。
2.4 三者协作:驱动初始化代码骨架
以下代码片段展示了一个简化的块设备驱动初始化过程,突出 gendisk、block_device_operations 和 request_queue 的绑定关系。
#include <linux/module.h>
#include <linux/blkdev.h>
#include <linux/hdreg.h>
static struct my_block_dev {
spinlock_t lock;
struct request_queue *queue;
struct gendisk *disk;
/* 模拟存储缓冲区 */
unsigned char *data;
} my_dev;
static int my_open(struct block_device *bdev, fmode_t mode) {
/* 增加引用计数或电源管理 */
return 0;
}
static void my_release(struct gendisk *disk, fmode_t mode) {
/* 减少引用计数 */
}
static int my_getgeo(struct block_device *bdev, struct hd_geometry *geo) {
geo->heads = 2;
geo->sectors = 32;
geo->cylinders = 1024;
return 0;
}
static const struct block_device_operations my_bd_ops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.getgeo = my_getgeo,
};
static void my_request_fn(struct request_queue *q) {
struct request *req;
while ((req = blk_fetch_request(q)) != NULL) {
/* 处理请求:获取 bio、数据传输 */
blk_end_request_all(req, 0);
}
}
static int __init my_init(void) {
/* 1. 分配 gendisk */
my_dev.disk = alloc_disk(16);
if (!my_dev.disk)
return -ENOMEM;
/* 2. 初始化请求队列 */
my_dev.queue = blk_init_queue(my_request_fn, &my_dev.lock);
if (!my_dev.queue) {
put_disk(my_dev.disk);
return -ENOMEM;
}
/* 3. 绑定核心字段 */
my_dev.disk->major = 240; /* 自定义主设备号 */
my_dev.disk->first_minor = 0;
my_dev.disk->minors = 16;
strcpy(my_dev.disk->disk_name, "myblk");
my_dev.disk->fops = &my_bd_ops;
my_dev.disk->queue = my_dev.queue;
my_dev.disk->private_data = &my_dev;
/* 4. 设置容量(以 512 字节扇区为单位) */
set_capacity(my_dev.disk, 1024 * 1024); /* 512 MB */
/* 5. 注册设备 */
add_disk(my_dev.disk);
return 0;
}
static void __exit my_exit(void) {
del_gendisk(my_dev.disk);
blk_cleanup_queue(my_dev.queue);
put_disk(my_dev.disk);
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
初始化顺序要点:
① 先分配
② 再设置
③ 调用
④ 最后调用
① 先分配
gendisk 和 request_queue;② 再设置
gendisk 的 major、fops、queue、private_data;③ 调用
set_capacity 设置扇区数;④ 最后调用
add_disk 使设备可见。顺序颠倒可能导致内核崩溃。
2.5 本章小结
- gendisk 是块设备在内核中的“身份证”,承载设备号、名称、操作表、队列和私有数据。
- block_device_operations 负责控制路径(打开、关闭、几何、介质检测),不参与数据读写。
- request_queue 是数据路径的核心,支持标准
request_fn和自制make_request_fn两种模式。 - 三者在驱动初始化时通过
gendisk的fops和queue字段紧密绑定,缺一不可。