1. 块设备驱动概述

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

在 Linux 内核中,设备驱动按数据访问特性分为两大类别:块设备(Block Device)和字符设备(Character Device)。理解二者的本质差异,是优化块设备加载延迟的前提。

对比维度 块设备 字符设备
数据组织 以固定大小的块(通常为 512B ~ 4KB)为单位 以字节流(stream)为单位,无固定边界
访问模式 支持随机访问(seek + read/write) 顺序访问为主,不支持 lseek 或随机定位
缓存策略 内核页缓存(Page Cache) + I/O 调度器 通常无内核级缓存,直接传递用户数据
设备节点 /dev/sda、/dev/nvme0n1 等 /dev/ttyS0、/dev/input/mouse0 等
主设备号范围 主设备号 + 次设备号(分区管理) 主设备号 + 次设备号(无分区概念)
典型设备 NVMe SSD、SATA 硬盘、SD 卡、U 盘 串口、键盘、鼠标、声卡、GPU
关键认知:块设备必须通过 请求队列(request queue) 与 I/O 调度器交互,而字符设备直接调用驱动提供的 read/write 回调。这一架构差异直接决定了块设备驱动的 probe 流程比字符设备复杂得多,也是加载延迟的主要来源。
块设备与字符设备访问路径对比 字符设备访问路径 用户态应用 (open/read/write) VFS 层 (file_operations) 字符设备驱动 (直接回调) 硬件设备 (无缓存/无调度) 块设备访问路径 用户态应用 (open/read/write) VFS + 页缓存 (Page Cache) I/O 调度器 + 请求队列 块设备驱动 (blk-mq / request_fn) 硬件设备 (NVMe/SATA/SD)

1.2 块设备驱动在 Linux 内核中的角色

块设备驱动是 Linux 存储栈中连接 通用块层(Generic Block Layer) 与 物理存储设备 的关键桥梁。其核心职责包括:

  • 设备发现与初始化: 通过 probe 回调识别硬件、分配数据结构、注册磁盘(gendisk)。
  • 请求队列管理: 创建并初始化请求队列(request_queue),绑定 I/O 调度器或 blk-mq 接口。
  • I/O 请求处理: 将内核下发的 bio/request 转换为硬件可执行的命令(如 NVMe SQ Entry、ATA Command)。
  • 中断与完成通知: 处理硬件中断,调用 blk_mq_complete_request 或 bio_endio 完成 I/O。
  • 电源管理与错误恢复: 处理 suspend/resume、设备热插拔、命令超时与重置。
性能视角: 块设备驱动的 probe 阶段不仅要完成硬件初始化,还必须完成请求队列的分配、blk-mq 的 tag 初始化、以及 I/O 调度器的绑定。这些步骤在传统驱动中串行执行,是加载延迟的主要瓶颈。本课程后续章节将逐一拆解并优化这些环节。
块设备驱动在内核存储栈中的位置 用户态应用 / 文件系统 (VFS / ext4 / XFS / btrfs) 通用块层 (Generic Block Layer) — submit_bio / bio 处理 I/O 调度层 (blk-mq / 请求队列 / I/O 调度器) 块设备驱动 (Block Device Driver) — probe / request_fn / 中断处理 物理存储设备 (NVMe SSD / SATA HDD / SD 卡 / U 盘)

1.3 典型块设备驱动架构

一个完整的块设备驱动通常包含以下核心组件和数据结构。以 NVMe 驱动为例,其架构具有代表性:

组件 数据结构 / 函数 职责说明
设备结构体 struct nvme_dev / struct myblock_dev 保存硬件寄存器基址、中断号、队列状态、私有数据
probe 函数 probe(struct pci_dev *pdev, ...) 设备发现、资源分配、请求队列创建、磁盘注册
请求队列 struct request_queue 管理 I/O 请求的排队、合并、分发;blk-mq 时包含多个队列
gendisk struct gendisk 代表一个块设备磁盘,包含分区表、设备号、fops
I/O 回调 queue_rq / request_fn 将 bio/request 转换为硬件命令并提交到设备
中断处理 irq_handler_t 处理硬件完成中断,调用 blk_mq_complete_request
remove 函数 remove(struct pci_dev *pdev) 设备卸载、资源释放、磁盘删除
延迟关键点: 在 probe 阶段,请求队列的创建(blk_mq_alloc_queue / blk_init_queue)和 gendisk 的注册(device_add_disk)是最耗时的两个操作。前者涉及内存分配和 tag 初始化,后者涉及 sysfs 创建和 uevent 发送。本课程第 3~5 章将深入分析这些操作的延迟分布。
典型块设备驱动架构组件关系 块设备驱动 (Block Driver) probe() 入口 分配设备结构体 创建请求队列 注册 gendisk struct request_queue struct gendisk struct block_device_operations blk_mq_tag_set 硬件设备 (PCIe / NVMe / SATA)

1.4 块设备驱动的加载流程(probe 到就绪)

从内核触发 probe 到请求队列完全就绪,典型流程包含以下步骤。每个步骤都可能成为延迟瓶颈:

  1. 设备匹配与 probe 调用: 内核总线驱动(如 PCI 核心)匹配设备 ID 表,调用驱动的 probe 回调。
  2. 硬件资源获取: 使能 PCI 设备、获取 MMIO 基址、分配中断向量(MSI-X)。
  3. 设备私有结构体分配: kzalloc 分配驱动私有数据,初始化锁、列表、状态变量。
  4. 请求队列创建: 调用 blk_mq_alloc_queue 或 blk_init_queue,分配 tag 集、初始化 I/O 调度器。
  5. gendisk 分配与配置: 调用 alloc_disk,设置 fops、队列指针、设备号。
  6. 设备添加与磁盘注册: 调用 device_add_disk,创建 sysfs 条目、发送 uevent、触发分区扫描。
  7. 中断注册与使能: 注册 IRQ 处理函数,使能设备中断。
  8. 请求队列就绪: 驱动标记设备为 online,I/O 调度器开始接受请求。
优化焦点: 步骤 4(请求队列创建)和步骤 6(device_add_disk)通常占据 probe 总耗时的 60%~80%。本课程将重点介绍如何通过预分配、延迟初始化、异步 probe 等手段压缩这两个阶段的延迟。
块设备驱动 probe 到请求队列就绪流程 1. 设备匹配 2. 硬件资源获取 3. 私有结构体分配 4. 请求队列创建 5. gendisk 分配 6. device_add_disk 7. 请求队列就绪 延迟热点区域 步骤 4 和步骤 6 占总延迟 60%~80%

1.5 小结

本章从块设备与字符设备的本质区别出发,明确了块设备驱动在内核存储栈中的桥梁角色,并剖析了典型块设备驱动的架构组成与 probe 加载流程。核心要点如下:

  • 块设备以固定块为单位、支持随机访问、依赖页缓存和 I/O 调度器,这些特性决定了其驱动架构比字符设备更复杂。
  • 块设备驱动位于通用块层与物理设备之间,负责请求队列管理、I/O 转换、中断处理等关键任务。
  • 典型驱动架构包含设备结构体、probe/remove 函数、请求队列、gendisk、I/O 回调和中断处理等组件。
  • 从 probe 到请求队列就绪的流程中,请求队列创建和device_add_disk 是延迟最集中的两个阶段,也是本课程后续优化的主要目标。