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 路径差异:

字符设备 vs 块设备 I/O 路径对比 字符设备路径 用户空间 read() / write() VFS 层 (file_operations) 字符设备驱动 硬件设备 (UART/GPIO) 块设备路径 用户空间 read() / write() VFS + 页缓存 (Page Cache) 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 请求的基本载体,描述一段连续内存缓冲区与磁盘扇区之间的传输。

下图展示了块设备驱动的完整软件架构层次:

Linux 块设备驱动分层架构 用户空间应用程序 (ext4/f2fs/VFAT 文件系统) VFS 层 + 页缓存 (Page Cache) 通用块层 (Generic Block Layer) bio 构造、分区映射、submit_bio I/O 调度层 (I/O Scheduler) 请求合并、排序、排队策略 块设备驱动层 (Block Driver) gendisk + request_queue + block_device_operations 硬件设备层 (SD/eMMC/NAND/SATA) 内核空间
驱动开发核心关注点:作为驱动开发者,主要工作在第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 调度器的工作流程可以概括为以下步骤:

  1. 请求到达:文件系统通过 submit_bio 将 bio 提交到通用块层。
  2. 请求合并:调度器检查新请求是否与队列中已有的请求在扇区上相邻,若相邻则合并为一个更大的请求。
  3. 请求排序:根据调度算法对请求进行排序(如按扇区号升序、按截止时间升序)。
  4. 请求分发:调度器将排序后的请求传递给驱动层的 request_fn 或 make_request_fn。
  5. 硬件处理:驱动将请求转换为硬件命令(如 SD 卡 CMD 命令、eMMC 读写命令),完成数据传输。

下图展示了 I/O 调度器在内核中的位置及其处理流程:

I/O 调度器处理流程 bio 请求流 ① 请求合并 (Merge) ② 请求排序 (Sort) ③ 请求分发 (Dispatch) 驱动 request_fn 处理
嵌入式场景注意事项:在嵌入式 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。