1. 块设备驱动基础:块设备与字符设备的区别、块设备驱动在内核中的位置

1.1 块设备与字符设备的本质区别

在 Linux 内核中,I/O 设备被抽象为两大类:块设备(Block Device)和字符设备(Character Device)。理解二者的差异是调优块设备驱动请求队列的前提。

对比维度 块设备(Block Device) 字符设备(Character Device)
数据访问单元 以固定大小的块(通常为 512B / 4KB)为单位进行读写 以字节流为单位,无固定块边界
随机访问能力 支持随机寻址(通过块号定位) 通常只支持顺序访问(如键盘、鼠标)
缓冲区 / 缓存 内核通过页缓存(Page Cache)和块 I/O 层管理数据 通常不经过页缓存,直接与设备交互
设备节点类型 主设备号 + 次设备号,节点类型为 b 主设备号 + 次设备号,节点类型为 c
典型设备 SSD、NVMe、HDD、SD 卡、U 盘 串口、键盘、鼠标、音频设备、GPIO
驱动注册接口 register_blkdev() + blk_alloc_queue() register_chrdev() 或 cdev_add()
I/O 调度器 需要 I/O 调度器(如 mq-deadline、kyber、none) 无 I/O 调度器,直接调用驱动 read()/write()
关键认知:块设备驱动必须管理请求队列(Request Queue),而字符设备驱动不需要。请求队列深度(Queue Depth)的调优直接影响块设备的 I/O 延迟和吞吐量,这也是本课程的核心关注点。

1.2 块设备驱动在内核中的层次位置

块设备驱动并非独立工作,它处于 Linux 存储栈的底层,向上对接通用块层(Generic Block Layer),向下直接控制硬件控制器。下图展示了块设备驱动在内核 I/O 路径中的精确位置:

Linux 存储栈层次结构(块设备驱动位置) 用户空间应用程序(VFS / syscall 接口) VFS 层 + 页缓存(Page Cache) 通用块层(Generic Block Layer / submit_bio) I/O 调度层(mq-deadline / kyber / none) 块设备驱动层(Block Device Driver) 请求队列管理 · 队列深度控制 · 硬件提交 硬件设备层(NVMe / SATA / virtio-blk 控制器) 本课程 调优焦点
调优视角:块设备驱动层是 I/O 请求从内核到达硬件的最后一道软件关卡。请求队列深度(Queue Depth)在此层通过 blk_queue_max_hw_sectors()、blk_mq_init_queue() 等接口配置,直接影响调度器初始化延迟和硬件利用率。

1.3 块设备驱动的核心数据结构

块设备驱动在内核中通过以下关键数据结构与通用块层交互:

  • struct gendisk — 代表一个块设备磁盘实例,包含设备号、容量、分区表、请求队列指针。
  • struct request_queue — 请求队列,是 I/O 调度器与驱动之间的核心通道,包含队列深度、调度算法、回调函数。
  • struct blk_mq_tag_set — 多队列(blk-mq)标签集,管理硬件队列的深度和并发请求数。
  • struct bio — 底层 I/O 请求描述符,携带扇区号、数据方向、页指针。
  • struct request — 经过 I/O 调度器合并后的请求,包含多个 bio。

1.4 字符设备 vs 块设备:驱动开发视角对比

从驱动开发者的角度看,两种设备类型的初始化流程差异显著:

字符设备驱动初始化(简化):
// 1. 分配设备号
alloc_chrdev_region(&devno, 0, 1, "my_char_dev");
// 2. 初始化 cdev 结构
cdev_init(&my_cdev, &my_fops);
// 3. 添加设备到内核
cdev_add(&my_cdev, devno, 1);
// 4. 创建设备类(可选)
device_create(my_class, NULL, devno, NULL, "my_dev");
块设备驱动初始化(简化):
// 1. 注册块设备
register_blkdev(MAJOR_NR, "my_block_dev");
// 2. 分配请求队列(blk-mq 方式)
my_tag_set = kzalloc(sizeof(struct blk_mq_tag_set), GFP_KERNEL);
my_tag_set->ops = &my_mq_ops;
my_tag_set->nr_hw_queues = 4;      // 硬件队列数
my_tag_set->queue_depth = 128;     // 队列深度(调优关键)
blk_mq_alloc_tag_set(my_tag_set);
// 3. 分配 gendisk
my_disk = alloc_disk(16);
my_disk->queue = blk_mq_init_queue(my_tag_set);
// 4. 添加磁盘
add_disk(my_disk);
注意:块设备驱动初始化中,queue_depth 和 nr_hw_queues 的配置直接影响 I/O 调度器的初始化延迟。过大的队列深度会导致调度器在启动时分配大量内存和标签,增加系统启动时间;过小则限制并发 I/O 性能。本课程后续章节将深入分析这一权衡。

1.5 块设备驱动在存储性能调优中的角色

块设备驱动层是存储性能调优的关键瓶颈点,原因如下:

  1. 请求队列深度直接决定硬件可以并行处理的 I/O 数量,影响延迟和吞吐量。
  2. I/O 调度器初始化发生在驱动加载阶段,队列深度越大,初始化耗时越长。
  3. 中断处理 / 轮询模式的选择由驱动层控制,影响 CPU 利用率。
  4. 硬件命令提交路径(如 NVMe SQ 提交)的优化依赖驱动实现。

下图展示了块设备驱动层内部请求处理的核心流程:

块设备驱动层内部请求处理流程 通用块层下发 bio 请求队列(request_queue) I/O 调度器(合并/排序) 驱动提交到硬件(queue_rq) 调优关键参数 queue_depth: 128~1024 nr_hw_queues: 1~CPU核数 调度器类型: none/deadline/kyber

1.6 本章小结

  • 块设备以块为单位、支持随机访问、需要 I/O 调度器;字符设备以字节流为单位、通常顺序访问、无调度器。
  • 块设备驱动位于 Linux 存储栈的第四层(通用块层之下,硬件层之上),是 I/O 路径的关键枢纽。
  • 驱动初始化时配置的队列深度和硬件队列数直接影响调度器初始化延迟和系统性能。
  • 本课程后续章节将围绕请求队列深度调优,深入分析初始化延迟的成因与优化方法。