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 以及底层硬件之间的协作关系:

块设备核心数据结构关系图 块设备核心数据结构协作关系 VFS / 文件系统层 struct gendisk major / minors / disk_name / private_data queue 指针 struct request_queue elevator / request_fn / make_request_fn nr_requests / queue_depth / tag_set bio 链表 struct bio #1 bi_sector / bi_io_vec struct bio #2 bi_sector / bi_io_vec bi_next bio_vec 数组 bv_page / bv_len / bv_offset bio_vec 数组 bv_page / bv_len / bv_offset 物理内存页框 物理内存页框 DMA 传输方向 内存 ↔ 设备 通过 SG 列表

1.4 数据流全景:从系统调用到块设备

理解上述数据结构后,我们可以梳理一次完整的块设备读写路径:

  1. 系统调用层: 用户态进程调用 read() / write(),进入 VFS 层。
  2. 文件系统层: 将文件偏移转换为 逻辑块号,构造 struct bio,填充 bi_sector 和 bi_io_vec。
  3. 块层(Block Layer): bio 被提交到目标 gendisk 的 request_queue。I/O 调度器对 bio 进行合并(merge)和排序(sort),生成 struct request。
  4. 驱动层: 驱动从队列中取出 request,解析其中的 bio 链表,提取 bio_vec 中的物理页地址和长度,构造 DMA 描述符。
  5. 硬件层: 启动 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 映射方式