1. 块设备驱动基础

1.1 块设备概念

块设备(Block Device)是 Linux 内核中一类以固定大小数据块为基本传输单元、支持随机访问的 I/O 设备。与字符设备不同,块设备内部维护了请求队列和 I/O 调度器,能够对读写请求进行合并、排序和重排,从而大幅提升吞吐量。

典型的块设备包括:

  • 硬盘(HDD / SSD)
  • NVMe 固态盘
  • SD/MMC 存储卡
  • USB 大容量存储设备
  • RAM 磁盘(brd / zram)
  • MD 软件 RAID 和 DM 设备映射器
关键指标:块设备的最小寻址单元称为扇区(sector),传统为 512 字节,现代设备普遍使用 4096 字节(4K 扇区)。内核中所有块 I/O 操作都以扇区为单位进行偏移计算。

1.2 字符设备与块设备区别

字符设备(Char Device)和块设备是 Linux 设备驱动模型的两大基石,二者在设计哲学、数据流模型和内核接口上存在本质差异。下表从多个维度进行对比:

对比维度 字符设备 块设备
数据传输单位 字节流(无固定大小) 固定大小数据块(扇区)
访问模式 顺序流式访问为主 随机访问 + 顺序访问
内核接口 file_operations(read/write/ioctl) block_device_operations + 请求队列
I/O 调度 无调度,直接透传 有 I/O 调度器(电梯算法、CFQ、mq-deadline、BFQ)
缓存策略 通常无内核缓存 通过页缓存(Page Cache)实现缓存
典型设备 串口、键盘、鼠标、音频 硬盘、SSD、SD 卡、软盘
设备号范围 主设备号 + 次设备号(无分区概念) 主设备号 + 次设备号(次设备号用于分区)
用户态访问 open/read/write/ioctl open/read/write/ioctl + mmap + fsync
核心差异总结:字符设备处理无结构字节流,驱动直接处理每次系统调用;块设备处理结构化数据块,驱动通过请求队列与 I/O 调度器交互,支持缓存、合并、排序和 DMA 传输。

1.3 块设备驱动架构概览

Linux 块设备驱动采用分层架构,从上到下依次为:

  1. VFS 层(虚拟文件系统)—— 处理文件操作,通过 address_space 与页缓存交互。
  2. 页缓存层(Page Cache)—— 缓存文件数据块,减少物理 I/O。
  3. 通用块层(Generic Block Layer)—— 将文件系统请求转换为 bio 结构,提交到块设备。
  4. I/O 调度层(I/O Scheduler)—— 对 bio 进行合并、排序、分发。
  5. 块设备驱动层(Block Device Driver)—— 处理请求队列,与硬件交互完成数据传输。
  6. 硬件层(Hardware)—— 物理存储介质。

下图展示了块设备驱动在 Linux 存储栈中的位置与数据流路径:

Linux 块设备驱动架构 — 存储栈分层 VFS 层(虚拟文件系统) 页缓存层(Page Cache) 通用块层(Generic Block Layer) I/O 调度层(I/O Scheduler) 块设备驱动层(Block Driver) 硬件层(HDD / SSD / NVMe) 核心数据结构 struct bio struct request struct request_queue struct gendisk

1.4 块设备驱动核心数据结构

块设备驱动开发涉及以下关键内核数据结构,理解它们之间的关系是编写驱动的基础:

数据结构 所在头文件 作用描述
struct gendisk <linux/genhd.h> 代表一个块设备实例,包含设备号、分区表、请求队列、操作函数集等
struct request_queue <linux/blkdev.h> 管理 I/O 请求的队列,包含 I/O 调度器、限速、合并策略等
struct request <linux/blkdev.h> 代表一个经过合并后的 I/O 请求,包含多个 bio 链
struct bio <linux/blk_types.h> 块 I/O 操作的基本单元,描述一段连续内存与磁盘扇区之间的传输
struct block_device_operations <linux/blkdev.h> 块设备操作函数集(open/release/ioctl/getgeo/check_events)
驱动开发注意:在 Linux 5.x 之后,struct request_queue 的分配方式从 blk_init_queue() 逐步迁移到 blk_mq_alloc_queue()(多队列块层)。新驱动应优先使用 blk-mq 框架,以获得更好的多核扩展性和 DMA 亲和性。

1.5 块设备驱动注册流程

一个典型的块设备驱动初始化流程包含以下步骤:

  1. 分配 gendisk:alloc_disk(minors) — 分配 struct gendisk 实例,指定分区数量。
  2. 设置操作函数:填充 disk->fops 指向 block_device_operations 结构体。
  3. 分配请求队列:blk_mq_alloc_queue() 或 blk_init_queue(),绑定请求处理函数。
  4. 设置队列限速:配置 disk->queue 的逻辑块大小、物理块大小、最大扇区数等。
  5. 注册设备:add_disk(disk) — 将 gendisk 注册到内核,创建设备节点。
  6. 实现请求处理:在请求处理函数中解析 request 或 bio,完成数据传输。
最小驱动骨架示例(blk-mq 模式):
#include <linux/module.h>
#include <linux/blkdev.h>
#include <linux/blk-mq.h>

static struct gendisk *my_disk;
static struct blk_mq_tag_set my_tag_set;

static blk_status_t my_queue_rq(struct blk_mq_hw_ctx *hctx,
                                const struct blk_mq_queue_data *bd)
{
    struct request *rq = bd->rq;
    struct bio *bio = rq->bio;
    // 处理 bio 中的数据
    blk_mq_end_request(rq, BLK_STS_OK);
    return BLK_STS_OK;
}

static const struct blk_mq_ops my_mq_ops = {
    .queue_rq = my_queue_rq,
};

static int __init my_init(void)
{
    // 1. 初始化 tag_set
    my_tag_set.ops = &my_mq_ops;
    my_tag_set.nr_hw_queues = 1;
    my_tag_set.queue_depth = 128;
    blk_mq_alloc_tag_set(&my_tag_set);

    // 2. 分配请求队列
    struct request_queue *q = blk_mq_alloc_queue(&my_tag_set, NULL, NULL);
    blk_queue_logical_block_size(q, 512);

    // 3. 分配 gendisk
    my_disk = alloc_disk(1);  // 1 个分区
    my_disk->major = 0;       // 动态分配主设备号
    my_disk->first_minor = 0;
    my_disk->fops = &my_fops;
    my_disk->queue = q;
    sprintf(my_disk->disk_name, "myblk");

    // 4. 注册设备
    add_disk(my_disk);
    return 0;
}

static void __exit my_exit(void)
{
    del_gendisk(my_disk);
    put_disk(my_disk);
    blk_mq_free_tag_set(&my_tag_set);
}

module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");

1.6 本章小结