Linux块设备驱动调优:I O吞吐与DMA传输深度优化 - 第1节
1.1 块设备与字符设备的本质区别
在 Linux 设备驱动模型中,块设备(Block Device)与字符设备(Character Device)是两大核心抽象。理解二者的差异是掌握块设备驱动调优的前提。
| 对比维度 | 块设备 | 字符设备 |
|---|---|---|
| 数据访问方式 | 以固定大小的块(通常为 512B ~ 4KB)为单位进行读写 | 以字节流形式顺序或随机访问 |
| 随机访问能力 | 支持通过 LBA(逻辑块地址) 直接定位到任意块 | 通常不支持随机定位,依赖文件指针顺序移动 |
| 缓存与缓冲 | 内核 Page Cache 和 I/O 调度器 介入,支持合并与排序 | 通常直接传递数据,无内核级缓存调度 |
| 主设备号范围 | 使用 block major 注册(如 8:0 表示 SCSI 磁盘) | 使用 char major 注册(如 4:0 表示 tty0) |
| 典型设备 | 硬盘(HDD/SSD)、NVMe、SD 卡、软盘 | 键盘、鼠标、串口、声卡、GPU |
| 驱动接口 | 实现 request_fn 或 make_request_fn,处理 bio 结构 |
实现 file_operations 中的 read/write 回调 |
关键洞察: 块设备的核心价值在于批量、有序、可缓存的 I/O 处理能力。字符设备每次系统调用直接触发硬件操作,而块设备通过 I/O 调度层将多个请求合并、排序后再下发,极大提升吞吐量。
1.1.1 块设备为何需要“块”抽象?
块设备底层硬件(如磁盘扇区、NAND Flash 页)天然以固定大小物理单元寻址。内核通过 块层(Block Layer) 将物理块映射为逻辑块,并向上提供统一的 bio 接口。这种抽象带来的好处:
- I/O 合并: 将多个相邻的小请求合并为一次大请求,减少硬件交互次数。
- 电梯调度: 按 LBA 排序请求,减少磁头寻道时间(HDD)或擦写次数(SSD)。
- 缓存一致性: 通过 Page Cache 保证同一块数据在内存中只有一份副本。
1.2 块设备驱动核心数据结构
Linux 块设备驱动的实现围绕三个核心数据结构展开:gendisk、request_queue、bio。它们构成了从 VFS 到物理设备的完整数据通路。
1.2.1 gendisk:块设备的逻辑描述
struct gendisk 是内核中代表一个逻辑块设备的核心结构。每个块设备(如 /dev/sda)对应一个 gendisk 实例。
struct gendisk {
int major; // 主设备号
int first_minor; // 起始次设备号
int minors; // 次设备号数量(分区数)
char disk_name[DISK_NAME_LEN]; // 设备名称,如 "sda"
struct block_device *part0; // 整个磁盘的块设备对象
const struct block_device_operations *fops; // 块设备操作函数集
struct request_queue *queue; // 指向该设备对应的请求队列
void *private_data; // 驱动私有数据
int flags; // 设备标志(如 GENHD_FL_REMOVABLE)
struct device *driverfs_dev; // 关联的 sysfs 设备
// ... 其他字段
};
关键字段说明:
major / first_minor / minors:决定设备在/dev下的编号范围。queue:每个 gendisk 必须绑定一个request_queue,用于接收和调度 I/O 请求。fops:提供open/release/ioctl/getgeo等操作,但不包含read/write——块设备的读写由 request_fn 处理。
调优提示: 在驱动初始化时,使用
alloc_disk(minors) 分配 gendisk,并调用 add_disk() 将其注册到系统。注意 minors 参数决定了该设备支持的最大分区数,过大会浪费内存,过小则限制分区能力。
1.2.2 request_queue:I/O 请求的调度中枢
struct request_queue 是块设备 I/O 的调度与排队中心。所有发往该设备的 bio 都会先进入队列,经过 I/O 调度器处理后,再以 struct request 的形式传递给驱动。
struct request_queue {
struct list_head queue_head; // 请求链表头
struct elevator_queue *elevator; // I/O 调度器实例
request_fn_proc *request_fn; // 传统请求处理函数
make_request_fn *make_request_fn; // 直接处理 bio 的函数(blk-mq 使用)
struct blk_mq_tag_set *tag_set; // 多队列标签集
unsigned int nr_requests; // 队列中最大请求数
unsigned int queue_depth; // 硬件队列深度
// ... 大量调优参数
};
两种工作模式:
| 模式 | 回调函数 | 适用场景 | 性能特征 |
|---|---|---|---|
| 单队列(legacy) | request_fn |
传统 HDD、简单块设备 | 单线程处理,锁竞争大 |
| 多队列(blk-mq) | make_request_fn |
NVMe、多核 SSD、高性能存储 | 每 CPU 队列,无锁或细粒度锁 |
性能陷阱: 在 blk-mq 模式下,
queue_depth 设置过小会导致硬件队列饥饿,过大则可能引发内存压力。建议根据硬件支持的 最大队列深度 和 平均 I/O 延迟 动态调整。
1.2.3 bio:块 I/O 的基本操作单元
struct bio 是 Linux 块层中最底层的 I/O 描述结构,代表一次内存与块设备之间的数据传输请求。它直接映射到物理内存中的页(page),是 DMA 传输的直接参与者。
struct bio {
sector_t bi_sector; // 起始扇区号(LBA)
struct bio *bi_next; // 链表指针,用于 bio 合并
struct block_device *bi_bdev; // 目标块设备
unsigned long bi_flags; // 标志位(如 BIO_UPTODATE)
unsigned short bi_vcnt; // bio_vec 数组中的段数
unsigned short bi_idx; // 当前处理的 bio_vec 索引
struct bio_vec *bi_io_vec; // 指向 bio_vec 数组
struct bio_vec bi_inline_vecs[0]; // 内联 bio_vec(避免额外分配)
int bi_error; // I/O 完成时的错误码
// ... 其他字段
};
bio_vec 结构:
struct bio_vec {
struct page *bv_page; // 指向物理页框
unsigned int bv_len; // 该段数据长度(字节)
unsigned int bv_offset; // 页内偏移
};
一个 bio 可以包含多个 bio_vec,每个 bio_vec 描述一段连续的内存区域。这种设计允许一次 bio 请求跨越多个不连续的物理页,实现分散/聚集(Scatter/Gather)DMA。
1.3 核心数据结构关系图
下图展示了 gendisk、request_queue、bio 以及底层硬件之间的协作关系:
1.4 数据流全景:从系统调用到块设备
理解上述数据结构后,我们可以梳理一次完整的块设备读写路径:
- 系统调用层: 用户态进程调用
read()/write(),进入 VFS 层。 - 文件系统层: 将文件偏移转换为 逻辑块号,构造
struct bio,填充bi_sector和bi_io_vec。 - 块层(Block Layer): bio 被提交到目标 gendisk 的
request_queue。I/O 调度器对 bio 进行合并(merge)和排序(sort),生成struct request。 - 驱动层: 驱动从队列中取出 request,解析其中的 bio 链表,提取 bio_vec 中的物理页地址和长度,构造 DMA 描述符。
- 硬件层: 启动 DMA 引擎,在内存页框与设备寄存器/缓冲区之间传输数据。传输完成后触发中断,驱动回调
bio_endio()通知上层。
性能关键路径: 步骤 3 中的 I/O 调度和步骤 4 中的 DMA 描述符构造是调优的核心。使用 blk-mq 和 SG(Scatter/Gather)DMA 可以显著减少锁竞争和 CPU 开销,是高性能块设备驱动的标配。
1.5 本章小结
| 数据结构 | 核心职责 | 调优关注点 |
|---|---|---|
| gendisk | 逻辑设备抽象,管理分区与设备属性 | minors 数量、private_data 布局 |
| request_queue | I/O 请求调度与排队 | 队列深度、调度器选择、blk-mq 配置 |
| bio | 描述一次内存与设备间的数据传输 | bio_vec 数量、页对齐、DMA 映射方式 |