1. Linux块设备驱动概述
块设备是嵌入式 Linux 系统中存储子系统的核心抽象。与字符设备不同,块设备以固定大小的块为单位进行 I/O 操作,并支持随机访问。本章从概念对比、驱动架构和 I/O 调度三个维度建立块设备驱动的全景认知。
1.1 块设备与字符设备的区别
Linux 设备驱动模型将硬件设备抽象为三类:字符设备、块设备和网络设备。字符设备以字节流方式顺序读写,而块设备以块为单位随机访问。下表从多个维度对比两者的核心差异:
| 对比维度 | 字符设备 (Char Device) | 块设备 (Block Device) |
|---|---|---|
| I/O 基本单位 | 字节 (byte) | 块 (block,通常 512B ~ 4KB) |
| 访问模式 | 顺序流式访问 | 随机访问 + 顺序访问 |
| 数据缓冲 | 无内核缓冲(直接读写) | 内核页缓存 (page cache) 缓冲 |
| 设备节点类型 | c (character) |
b (block) |
| 主设备号范围 | 1–255 | 1–255(与字符设备独立) |
| 典型设备 | UART、键盘、触摸屏 | SD 卡、eMMC、NAND Flash、硬盘 |
| 驱动接口 | file_operations |
block_device_operations + request_queue |
| I/O 调度 | 无(直接传递) | I/O 调度器合并/排序请求 |
关键理解:块设备之所以需要内核缓冲和 I/O 调度,是因为存储介质的物理特性(寻道时间、擦除块大小、写放大)要求对上层 I/O 进行合并与排序,以提升吞吐量并延长设备寿命。字符设备则没有这些约束。
下图展示了字符设备与块设备在 Linux 内核中的 I/O 路径差异:
1.2 块设备驱动架构
Linux 块设备驱动采用分层架构设计,从上到下依次为:VFS 层 → 页缓存层 → 通用块层 → I/O 调度层 → 块设备驱动层。每一层都有明确的职责边界。
块设备驱动的核心数据结构包括:
gendisk:通用磁盘描述结构,代表一个块设备实例,包含设备号、容量、分区表、请求队列等。block_device_operations:块设备操作函数集,包含open、release、ioctl、getgeo等回调。request_queue:请求队列,挂载 I/O 调度器,驱动通过request_fn或make_request_fn处理 I/O。bio:块 I/O 请求的基本载体,描述一段连续内存缓冲区与磁盘扇区之间的传输。
下图展示了块设备驱动的完整软件架构层次:
驱动开发核心关注点:作为驱动开发者,主要工作在第5层——注册
gendisk、初始化 request_queue、实现 block_device_operations 回调,并处理来自 I/O 调度层的 request 或 bio。上层的 VFS、页缓存和 I/O 调度器由内核通用代码管理,驱动无需干预。
1.3 I/O 调度器简介
I/O 调度器位于通用块层与块设备驱动之间,负责对上层下发的 I/O 请求进行合并和排序,以优化磁盘寻道时间、减少写放大、提升整体吞吐量。Linux 内核支持多种 I/O 调度算法,每种算法针对不同的存储介质和应用场景进行了权衡。
下表对比了主流 I/O 调度器的核心特性:
| 调度器名称 | 内核选项 | 核心策略 | 适用场景 |
|---|---|---|---|
| NOOP | CONFIG_IOSCHED_NOOP |
先进先出 (FIFO),仅做简单合并 | SSD、NVMe、RAM 盘(无寻道代价) |
| Deadline | CONFIG_IOSCHED_DEADLINE |
按请求截止时间排序,读请求优先 | 数据库、实时性要求高的场景 |
| CFQ | CONFIG_IOSCHED_CFQ |
完全公平队列,按进程分配时间片 | 传统 HDD、多进程桌面/服务器 |
| BFQ | CONFIG_IOSCHED_BFQ |
基于权重的公平排队,低延迟 | 交互式系统、Android、多媒体 |
| Kyber | CONFIG_IOSCHED_KYBER |
基于延迟的调度,自适应 | 快速固态存储、低延迟需求 |
| MQ-Deadline | CONFIG_IOSCHED_MQ_DEADLINE |
多队列版 Deadline,支持 blk-mq | 多核系统、NVMe、高性能存储 |
I/O 调度器的工作流程可以概括为以下步骤:
- 请求到达:文件系统通过
submit_bio将 bio 提交到通用块层。 - 请求合并:调度器检查新请求是否与队列中已有的请求在扇区上相邻,若相邻则合并为一个更大的请求。
- 请求排序:根据调度算法对请求进行排序(如按扇区号升序、按截止时间升序)。
- 请求分发:调度器将排序后的请求传递给驱动层的
request_fn或make_request_fn。 - 硬件处理:驱动将请求转换为硬件命令(如 SD 卡 CMD 命令、eMMC 读写命令),完成数据传输。
下图展示了 I/O 调度器在内核中的位置及其处理流程:
嵌入式场景注意事项:在嵌入式 Linux 中,存储介质多为 SD 卡、eMMC 或 NAND Flash,这些设备没有机械寻道时间,但存在擦除块大小、写放大和磨损均衡等约束。因此,NOOP 或 MQ-Deadline 通常是更合适的选择,而 CFQ/BFQ 的公平排队策略反而可能引入不必要的延迟和 CPU 开销。在后续章节中,我们将结合 SD 卡驱动实例,深入分析如何为嵌入式块设备选择合适的 I/O 调度策略。
本章小结
本章建立了块设备驱动的三个基础认知:
- 块设备 vs 字符设备:块设备以块为单位、支持随机访问、经过页缓存和 I/O 调度,与字符设备的字节流模型有本质区别。
- 块设备驱动架构:从 VFS 到硬件共六层,驱动开发者聚焦于
gendisk、request_queue和block_device_operations的实现。 - I/O 调度器:通过合并和排序优化 I/O 性能,嵌入式场景应优先选择 NOOP 或 MQ-Deadline。