3. 块设备驱动模型
块设备驱动模型是Linux内核I/O子系统的核心骨架。本章深入剖析gendisk结构体、分区表解析、block_device_operations函数集以及设备注册与注销流程,帮助你在存储场景优化中精准控制块设备行为。
3.1 gendisk结构体:块设备的逻辑抽象
gendisk(generic disk)是内核中描述一个逻辑磁盘的核心数据结构。它不直接对应物理硬件,而是代表一个可被分区、可挂载文件系统的块设备实例。每个gendisk实例关联一个request_queue和一组block_device_operations。
关键字段速览:
major/first_minor/minors 定义设备号范围;disk_name 是设备名(如"sda");queue 指向请求队列;fops 指向操作函数集;part 是分区表信息;private_data 供驱动私有使用。
| 字段 | 类型 | 说明 |
|---|---|---|
major | int | 主设备号 |
first_minor | int | 起始次设备号 |
minors | int | 支持的次设备号数量(通常为16或64) |
disk_name | char[32] | 设备名称,如"nvme0n1" |
queue | struct request_queue * | 关联的请求队列 |
fops | const struct block_device_operations * | 块设备操作函数集 |
part | struct disk_part_tbl * | 分区表信息 |
private_data | void * | 驱动私有数据指针 |
flags | unsigned long | 设备标志(如GENHD_FL_NO_PART) |
优化要点:在NVMe SSD场景中,
minors通常设置为64以支持更多分区;而嵌入式设备可设为1以节省内存。
3.2 分区表解析:从MBR到GPT
分区表解析是块设备驱动初始化时的关键步骤。内核通过blkdev_get触发分区扫描,调用check_partition函数识别分区格式。主流分区表格式包括MBR(主引导记录)和GPT(GUID分区表)。
注意:GPT分区表支持超过2TB的磁盘和128个以上分区,现代存储系统应优先使用GPT。内核通过
efi_partition模块处理GPT解析。
3.3 block_device_operations:块设备操作函数集
block_device_operations结构体定义了块设备驱动的操作接口。每个gendisk实例必须关联一个该结构体的实例。与字符设备的file_operations不同,块设备操作集更侧重于打开/关闭/获取介质状态/报告容量等管理操作,而实际数据传输由request_queue处理。
// 内核源码 include/linux/blkdev.h
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 *disk, unsigned int clearing);
int (*getgeo)(struct block_device *, struct hd_geometry *);
int (*set_read_only)(struct block_device *, bool);
void (*swap_slot_free_notify)(struct block_device *, unsigned long);
int (*report_zones)(struct gendisk *, sector_t sector,
unsigned int nr_zones, report_zones_cb cb, void *data);
char *(*devnode)(struct gendisk *disk, umode_t *mode);
struct module *owner;
const struct pr_ops *pr_ops;
};
| 回调函数 | 触发场景 | 典型实现 |
|---|---|---|
open | 设备被挂载或打开 | 检查介质是否存在、递增引用计数 |
release | 设备关闭 | 递减引用计数、清理资源 |
ioctl | 用户态ioctl调用 | 处理BLKGETSIZE、BLKRRPART等命令 |
check_events | 轮询介质状态变化 | 返回DISK_EVENT_MEDIA_CHANGE等标志 |
getgeo | 获取磁盘几何参数 | 填充柱面/磁头/扇区信息 |
report_zones | ZNS(分区命名空间)设备 | 返回zone状态和写指针 |
场景优化:对于NVMe SSD,
report_zones回调在ZNS设备中至关重要;对于热插拔USB设备,check_events必须正确实现以支持介质检测。
3.4 设备注册与注销流程
块设备驱动的生命周期管理遵循严格的注册-使用-注销模式。核心API包括alloc_disk、add_disk、del_gendisk和put_disk。
3.4.1 注册流程
- 分配gendisk:调用
alloc_disk(minors)分配gendisk实例,指定次设备号数量。 - 初始化字段:设置
major、first_minor、disk_name、fops、queue等。 - 分配请求队列:调用
blk_mq_alloc_disk或blk_alloc_disk创建请求队列并关联。 - 注册设备:调用
add_disk(gendisk)将设备注册到内核,触发分区扫描和udev事件。
// 典型注册流程示例
static int __init my_block_driver_init(void)
{
struct gendisk *disk;
struct request_queue *q;
// 1. 分配请求队列(多队列模式)
q = blk_mq_alloc_disk(&my_tag_set, &my_queue_ops, &my_dev);
if (IS_ERR(q))
return PTR_ERR(q);
// 2. 分配gendisk
disk = alloc_disk(16); // 支持16个分区
if (!disk) {
blk_cleanup_queue(q);
return -ENOMEM;
}
// 3. 初始化gendisk
disk->major = MY_MAJOR;
disk->first_minor = 0;
disk->disk_name = "mydisk";
disk->fops = &my_block_ops;
disk->queue = q;
disk->private_data = &my_dev;
// 4. 注册设备
add_disk(disk);
return 0;
}
3.4.2 注销流程
- 删除gendisk:调用
del_gendisk(disk),移除设备节点并刷新未完成I/O。 - 清理请求队列:调用
blk_cleanup_queue(disk->queue)。 - 释放gendisk:调用
put_disk(disk),释放引用计数。
// 典型注销流程示例
static void __exit my_block_driver_exit(void)
{
struct my_device *dev = &my_dev;
// 1. 删除gendisk
del_gendisk(dev->disk);
// 2. 清理请求队列
blk_cleanup_queue(dev->disk->queue);
// 3. 释放gendisk
put_disk(dev->disk);
}
3.5 关键数据结构关系图
块设备驱动模型中,gendisk、request_queue、block_device_operations和block_device之间的关系如下:
核心要点:
gendisk是块设备驱动的入口点,它通过queue连接I/O调度层,通过fops暴露管理接口,通过part管理分区信息。理解这三者的关系是优化块设备性能的基础。