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 流程比字符设备复杂得多,也是加载延迟的主要来源。
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 调度器的绑定。这些步骤在传统驱动中串行执行,是加载延迟的主要瓶颈。本课程后续章节将逐一拆解并优化这些环节。
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 章将深入分析这些操作的延迟分布。
1.4 块设备驱动的加载流程(probe 到就绪)
从内核触发 probe 到请求队列完全就绪,典型流程包含以下步骤。每个步骤都可能成为延迟瓶颈:
- 设备匹配与 probe 调用: 内核总线驱动(如 PCI 核心)匹配设备 ID 表,调用驱动的 probe 回调。
- 硬件资源获取: 使能 PCI 设备、获取 MMIO 基址、分配中断向量(MSI-X)。
- 设备私有结构体分配: kzalloc 分配驱动私有数据,初始化锁、列表、状态变量。
- 请求队列创建: 调用
blk_mq_alloc_queue或blk_init_queue,分配 tag 集、初始化 I/O 调度器。 - gendisk 分配与配置: 调用
alloc_disk,设置 fops、队列指针、设备号。 - 设备添加与磁盘注册: 调用
device_add_disk,创建 sysfs 条目、发送 uevent、触发分区扫描。 - 中断注册与使能: 注册 IRQ 处理函数,使能设备中断。
- 请求队列就绪: 驱动标记设备为 online,I/O 调度器开始接受请求。
优化焦点: 步骤 4(请求队列创建)和步骤 6(device_add_disk)通常占据 probe 总耗时的 60%~80%。本课程将重点介绍如何通过预分配、延迟初始化、异步 probe 等手段压缩这两个阶段的延迟。
1.5 小结
本章从块设备与字符设备的本质区别出发,明确了块设备驱动在内核存储栈中的桥梁角色,并剖析了典型块设备驱动的架构组成与 probe 加载流程。核心要点如下:
- 块设备以固定块为单位、支持随机访问、依赖页缓存和 I/O 调度器,这些特性决定了其驱动架构比字符设备更复杂。
- 块设备驱动位于通用块层与物理设备之间,负责请求队列管理、I/O 转换、中断处理等关键任务。
- 典型驱动架构包含设备结构体、probe/remove 函数、请求队列、gendisk、I/O 回调和中断处理等组件。
- 从 probe 到请求队列就绪的流程中,请求队列创建和device_add_disk 是延迟最集中的两个阶段,也是本课程后续优化的主要目标。