2. 块设备核心数据结构
块设备驱动的核心在于三个相互关联的数据结构:block_device_operations(操作函数表)、gendisk(通用磁盘描述)和 request_queue(请求队列)。它们构成了块设备驱动与内核块设备层之间的契约,理解这三者的关系是编写正确、高效块设备驱动的前提。
核心关系速览: 每个
gendisk 实例代表一个逻辑磁盘或分区,它内部包含一个指向 block_device_operations 的指针(定义该磁盘支持的操作),以及一个指向 request_queue 的指针(管理该磁盘的 I/O 请求排队与调度)。
2.1 block_device_operations 结构体
block_device_operations 是块设备驱动向内核暴露的操作接口,定义在 <linux/blkdev.h> 中。与字符设备的 file_operations 不同,块设备的操作函数数量较少,因为数据读写不通过这里,而是通过请求队列完成。
| 成员 | 函数原型 | 说明 |
|---|---|---|
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) |
处理设备特定 ioctl 命令 |
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) |
check_events |
unsigned int (*check_events)(struct gendisk *, unsigned int) |
检查介质状态变化(如光盘弹出) |
revalidate_disk |
int (*revalidate_disk)(struct gendisk *) |
重新验证磁盘容量或介质变化 |
owner |
struct module *owner |
通常设置为 THIS_MODULE |
关键理解:
open 和 release 用于管理设备的打开计数和状态;ioctl 用于非标准控制命令;getgeo 用于传统分区工具(如 fdisk)获取 CHS 参数。数据读写操作不在此结构体中定义。
典型初始化示例:
static int my_open(struct block_device *bdev, fmode_t mode) {
struct my_disk *disk = bdev->bd_disk->private_data;
if (!disk) return -ENXIO;
disk->users++;
return 0;
}
static void my_release(struct gendisk *gd, fmode_t mode) {
struct my_disk *disk = gd->private_data;
disk->users--;
}
static int my_getgeo(struct block_device *bdev, struct hd_geometry *geo) {
geo->heads = 4;
geo->sectors = 16;
geo->cylinders = 1024;
geo->start = 0;
return 0;
}
static const struct block_device_operations my_bdops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.ioctl = my_ioctl,
.getgeo = my_getgeo,
};
2.2 gendisk 结构体详解
struct gendisk 是内核中表示一个通用磁盘设备的核心结构体。它封装了磁盘的标识、容量、分区信息、请求队列以及驱动私有数据。每个块设备驱动必须为每个逻辑磁盘分配并注册一个 gendisk 实例。
gendisk 生命周期管理:
| 步骤 | 函数 | 说明 |
|---|---|---|
| 1. 分配 | alloc_disk(int minors) |
分配 gendisk 实例,指定次设备号数量 |
| 2. 初始化 | 手动赋值 | 设置 major、first_minor、disk_name、fops、queue、private_data、capacity |
| 3. 注册 | add_disk(struct gendisk *) |
将磁盘注册到内核,创建设备节点并激活 I/O |
| 4. 卸载 | del_gendisk(struct gendisk *) |
删除磁盘,清理分区信息和设备节点 |
| 5. 释放 | put_disk(struct gendisk *) |
减少引用计数,计数归零时释放内存 |
常见错误: 在调用
add_disk() 之后,驱动必须确保所有 gendisk 字段已经正确初始化,因为 add_disk() 会立即触发内核对该磁盘的访问(如分区扫描)。不要在 add_disk() 之后再修改 capacity 或 fops。
典型初始化代码:
struct my_disk {
spinlock_t lock;
struct request_queue *queue;
struct gendisk *gd;
int users;
// 其他驱动私有数据
};
static int __init my_init(void) {
struct my_disk *disk;
// 1. 分配驱动私有结构体
disk = kzalloc(sizeof(*disk), GFP_KERNEL);
if (!disk) return -ENOMEM;
spin_lock_init(&disk->lock);
// 2. 分配请求队列
disk->queue = blk_alloc_queue(GFP_KERNEL);
if (!disk->queue) goto err_queue;
blk_queue_make_request(disk->queue, my_make_request);
// 3. 分配 gendisk(支持 16 个分区)
disk->gd = alloc_disk(16);
if (!disk->gd) goto err_gd;
// 4. 初始化 gendisk 字段
disk->gd->major = my_major;
disk->gd->first_minor = 0;
disk->gd->fops = &my_bdops;
disk->gd->queue = disk->queue;
disk->gd->private_data = disk;
snprintf(disk->gd->disk_name, 32, "mydisk");
set_capacity(disk->gd, 1024 * 1024); // 512MB
// 5. 注册磁盘
add_disk(disk->gd);
return 0;
err_gd:
blk_cleanup_queue(disk->queue);
err_queue:
kfree(disk);
return -ENOMEM;
}
2.3 request_queue 结构体
struct request_queue 是块设备 I/O 请求的调度与管理中心。每个 gendisk 拥有一个独立的请求队列,所有发往该磁盘的 bio 都会先进入这个队列,经过 I/O 调度器(elevator)的合并与排序后,再传递给驱动处理。
request_queue 核心成员:
| 成员 | 类型 | 说明 |
|---|---|---|
make_request_fn |
函数指针 | 接收 bio 并决定如何处理(直接处理或入队) |
request_fn |
函数指针 | 从队列中取出 request 并执行(传统模式) |
elevator |
struct elevator_queue * |
指向 I/O 调度器实例 |
queue_flags |
unsigned long | 队列状态标志(如 QUEUE_FLAG_STOPPED) |
queue_limits |
struct queue_limits |
设备硬件限制(最大扇区数、对齐要求等) |
nr_requests |
unsigned int | 队列中最大请求数 |
queuedata |
void * | 驱动私有数据指针 |
两种 I/O 处理模式:
1. 传统 request 模式: 驱动实现
2. make_request_fn 模式(BIO 直通): 驱动实现
1. 传统 request 模式: 驱动实现
request_fn,内核将 bio 合并为 request 后批量提交。适合机械硬盘等需要 I/O 调度的设备。2. make_request_fn 模式(BIO 直通): 驱动实现
make_request_fn,直接接收 bio,绕过 I/O 调度器。适合 SSD、RAM disk 等不需要调度的设备。
队列创建与绑定示例:
// 方式一:传统 request 模式(需要 I/O 调度器)
struct request_queue *q;
q = blk_init_queue(my_request_fn, &my_lock);
if (!q) goto err;
disk->queue = q;
// 方式二:make_request_fn 模式(BIO 直通,无调度器)
struct request_queue *q;
q = blk_alloc_queue(GFP_KERNEL);
if (!q) goto err;
blk_queue_make_request(q, my_make_request);
disk->queue = q;
// 设置队列限制
blk_queue_logical_block_size(q, 512);
blk_queue_physical_block_size(q, 4096);
blk_queue_max_hw_sectors(q, 256);
性能提示: 对于基于 RAM 的块设备或 NVMe 等高性能设备,推荐使用
make_request_fn 模式,避免 I/O 调度器的额外开销。对于传统机械硬盘,使用 request_fn 模式以利用 I/O 调度器的合并与排序能力。
2.4 三者协作关系总结
在块设备驱动中,这三个结构体的协作关系可以概括为:
- gendisk 是"身份证":标识设备的存在、容量和设备号范围。
- block_device_operations 是"控制面板":提供打开、关闭、ioctl 等控制操作。
- request_queue 是"数据管道":管理所有读写 I/O 请求的排队、调度与分发。
驱动初始化时,先分配 gendisk 和 request_queue,将两者通过指针关联,再将 block_device_operations 赋值给 gendisk->fops。最后调用 add_disk() 使设备生效。卸载时,先调用 del_gendisk() 移除设备,再清理队列和释放内存。
注意事项: 在调用
add_disk() 之前,必须确保 gendisk->queue 和 gendisk->fops 已经正确赋值。内核在 add_disk() 中会立即使用这些指针,如果为 NULL 将导致内核崩溃。