1、块设备驱动基础:块设备与字符设备的区别、块设备驱动核心数据结构
块设备驱动是 Linux 存储子系统的基石,理解其核心概念与数据结构是进行性能调优的前提。本节将首先厘清块设备与字符设备的本质区别,然后深入剖析 gendisk、request_queue 和 bio 三大核心数据结构的设计意图与协作关系。
1.1 块设备与字符设备的区别
在 Linux 设备驱动模型中,设备被抽象为两大类:块设备(Block Device) 和 字符设备(Character Device)。两者的根本差异在于数据访问的粒度与方式。
| 对比维度 | 块设备(Block Device) | 字符设备(Character Device) |
|---|---|---|
| 数据访问粒度 | 以固定大小的块(通常为 512B、4KB)为单位 | 以字节为最小单位,可任意读写 |
| 随机访问能力 | 支持,通过块号定位任意位置 | 通常不支持,数据流按顺序处理 |
| 缓冲区策略 | 内核维护页高速缓存(Page Cache),数据先缓存再落盘 | 通常直接与硬件交互,无内核级缓存 |
| I/O 调度 | 请求经过 I/O 调度器(如 CFQ、mq-deadline)合并与排序 | 无调度,按用户态写入顺序直接传递 |
| 典型设备 | 硬盘(HDD/SSD)、NVMe、SD 卡、loop 设备 | 键盘、鼠标、串口、传感器 |
| 设备节点类型 | ls -l 显示为 b |
ls -l 显示为 c |
| 驱动核心接口 | block_device_operations + make_request_fn |
file_operations 中的 read/write |
性能调优启示: 块设备的缓存与调度机制带来了吞吐优势,但也引入了延迟不确定性。对于 NVMe SSD 这类低延迟设备,绕过 I/O 调度器(如使用
none 调度器)或使用轮询模式(polling)可显著降低尾延迟。
下图展示了块设备与字符设备在内核 I/O 路径上的核心差异:
1.2 块设备驱动核心数据结构
Linux 块设备层围绕三个核心数据结构构建:gendisk(通用磁盘描述)、request_queue(请求队列)和 bio(块 I/O 请求)。三者形成“设备 → 队列 → 请求”的层次化架构。
1.2.1 gendisk:通用磁盘描述
struct gendisk 是内核中代表一个逻辑磁盘的顶层结构。每个块设备(如 /dev/sda、/dev/nvme0n1)在内核中都对应一个 gendisk 实例。
struct gendisk {
int major; // 主设备号
int first_minor; // 起始次设备号
int minors; // 次设备号数量(分区数量上限)
char disk_name[DISK_NAME_LEN]; // 设备名称,如 "sda"
struct disk_part_tbl *part_tbl; // 分区表
struct block_device_operations *fops; // 块设备操作函数集
struct request_queue *queue; // 指向关联的请求队列
void *private_data; // 驱动私有数据
int flags; // 设备标志(如 GENHD_FL_NO_PART)
struct device *driverfs_dev; // 关联的 device 结构
// ... 其他字段
};
调优关注点:
minors 字段决定了分区数量上限。对于多分区场景(如 Ceph OSD),若 minors 设置过小,动态创建分区时会失败。建议根据实际分区需求设置为 64 或 256。
1.2.2 request_queue:请求队列与 I/O 调度
struct request_queue 是块设备 I/O 的核心调度与分发枢纽。所有发往该设备的 bio 都会先进入此队列,经过 I/O 调度器处理后,再以 request 的形式传递给驱动。
struct request_queue {
struct request_list rq; // 请求池
struct elevator_queue *elevator; // I/O 调度器(如 mq-deadline)
struct blk_mq_ctx *mq_ctx; // 多队列上下文
struct blk_mq_hw_ctx **queue_hw_ctx; // 硬件队列数组
make_request_fn *make_request_fn; // 自定义请求处理函数
unsigned long nr_requests; // 队列中最大请求数
unsigned int queue_depth; // 队列深度(对 NVMe 等至关重要)
// ... 其他字段
};
下图展示了 gendisk、request_queue 与 bio 之间的协作关系:
1.2.3 bio:块 I/O 请求的基本单元
struct bio 是 Linux 块层中最底层的 I/O 描述结构,代表一次独立的、不可分割的块 I/O 操作。它直接面向文件系统和块设备驱动。
struct bio {
struct bio *bi_next; // 链表指针,用于 bio 合并
struct block_device *bi_bdev; // 目标块设备
unsigned long bi_flags; // 标志位(如 BIO_UPTODATE)
unsigned short bi_vcnt; // bi_io_vec 中向量数量
unsigned short bi_idx; // 当前处理的向量索引
unsigned int bi_size; // 剩余 I/O 字节数
struct bio_vec *bi_io_vec; // 数据向量数组(页 + 偏移 + 长度)
struct bio_set *bi_pool; // 所属内存池
bio_end_io_t *bi_end_io; // I/O 完成回调函数
void *bi_private; // 驱动私有数据
// ... 其他字段
};
性能陷阱:
bio 的 bi_io_vec 向量数量(bi_vcnt)直接影响内存分配与 DMA 映射开销。对于高性能 NVMe 驱动,建议通过 blk_queue_max_segments() 限制最大段数,避免单次 bio 携带过多分散的物理页,导致 DMA 映射失败或性能下降。
1.3 核心数据结构关系总结
下表总结了三大数据结构在块设备 I/O 路径中的角色与关键性能属性:
| 数据结构 | 核心角色 | 关键性能字段 | 调优影响 |
|---|---|---|---|
| gendisk | 磁盘抽象与分区管理 | minors、flags |
分区数量上限、设备热插拔行为 |
| request_queue | I/O 调度与分发 | nr_requests、queue_depth、elevator |
队列深度决定并发能力,调度器影响延迟与吞吐 |
| bio | 单次 I/O 操作描述 | bi_vcnt、bi_size、bi_io_vec |
向量数量影响 DMA 效率,大小影响合并效果 |
性能调优核心原则: 块设备驱动的性能优化本质上是平衡吞吐与延迟的过程。通过调整
queue_depth 控制并发深度,选择合适的 I/O 调度器(如 NVMe 使用 none),以及优化 bio 的向量布局,可以显著提升设备利用率并降低尾延迟。