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 路径上的核心差异:

块设备 vs 字符设备 I/O 路径对比 字符设备路径 用户态 read/write (字节流) VFS -> 字符设备驱动 硬件直接交互 (无缓存) 无 I/O 调度,无缓存 块设备路径 用户态 read/write (文件偏移) VFS -> 页高速缓存 (Page Cache) 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 之间的协作关系:

块设备驱动核心数据结构协作关系 gendisk 通用磁盘描述 request_queue 请求队列 + I/O 调度器 bio (块 I/O 请求) 单个 I/O 操作 request (合并后请求) 多个 bio 合并而成 queue 指针 make_request_fn I/O 调度器分发 核心流程:bio 提交 → request_queue 排队 → I/O 调度器合并/排序 → request 分发 → 驱动处理

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 的向量布局,可以显著提升设备利用率并降低尾延迟。