2. Linux块设备驱动基础

2.1 块设备与字符设备的本质区别

在 Linux 设备驱动模型中,I/O 设备被划分为两大类型:字符设备和块设备。理解两者的区别是编写块设备驱动的第一道门槛。

对比维度 字符设备 (Char Device) 块设备 (Block Device)
数据访问方式 按字节流顺序访问,无缓冲区 按固定大小的块(通常为 512B/4KB)随机访问
核心接口 file_operations(open/read/write/ioctl) block_device_operations(open/release/ioctl)
I/O 调度 无内核级调度,直接透传 通过 request_queue 进行合并、排序、调度
缓存策略 通常无页缓存(raw 模式) 通过 页缓存 (Page Cache) 和 块层缓存 加速
挂载文件系统 不支持 支持(ext4、xfs、btrfs 等)
典型设备 串口、键盘、鼠标、传感器 硬盘、SSD、SD 卡、RAM 磁盘
主设备号范围 动态分配,无固定限制 通常使用固定主设备号(如 8 为 SCSI 磁盘)
核心认知:字符设备是“流式”的,块设备是“寻址式”的。文件系统只能挂载在块设备之上,因为文件系统需要随机读写、缓存管理和 I/O 调度——这些正是块设备子系统的核心能力。

2.2 块设备驱动框架全景

Linux 块设备驱动由三个核心组件构成,它们共同协作完成从 VFS 到物理设备的 I/O 路径:

  • gendisk:代表一个逻辑磁盘或分区,是块设备在内存中的抽象。
  • request_queue:I/O 请求队列,负责接收、合并、排序来自文件系统的请求。
  • block_device_operations:块设备操作函数表,定义 open/release/ioctl 等回调。
块设备驱动核心框架结构图 VFS (虚拟文件系统) 块设备子系统 (Block Layer) 页缓存 / I/O 调度器 / 请求合并 request_queue (请求队列) make_request_fn / 电梯算法 gendisk (通用磁盘) disk_name / 分区表 / 容量 block_device_operations 驱动开发者关注点 1. 实现 bd_ops 回调 2. 分配并初始化 gendisk 3. 绑定 request_queue 4. 注册块设备

2.3 gendisk:磁盘的抽象表示

struct gendisk 是块设备驱动的核心数据结构,它代表一个独立的磁盘或分区设备。每个 gendisk 实例对应 /dev 下的一个块设备节点。

struct gendisk {
    int major;                  // 主设备号
    int first_minor;            // 起始次设备号
    int minors;                 // 次设备号数量(分区数+1)
    char disk_name[DISK_NAME_LEN]; // 设备名称,如 "vdab"
    struct block_device_operations *fops; // 操作函数表
    struct request_queue *queue; // 关联的请求队列
    void *private_data;         // 驱动私有数据
    sector_t capacity;          // 磁盘容量(扇区数)
    int flags;                  // 标志位(如 GENHD_FL_NO_PART)
    struct device *driverfs_dev; // 设备模型中的 device
    // ... 其他字段
};
关键字段解读:
  • major / first_minor / minors:决定了设备节点的编号范围和分区能力。例如 minors = 16 表示支持 15 个分区 + 1 个整盘。
  • capacity:以 512 字节扇区为单位。若设备容量为 1GB,则 capacity = 2097152。
  • private_data:驱动开发者最常用的字段,通常指向自定义的设备结构体。

2.4 request_queue:I/O 请求的调度中枢

struct request_queue 是块设备 I/O 的入口和调度中心。所有发往该设备的读写请求都会先进入队列,经过合并、排序后,再传递给驱动处理。

队列特性 说明 典型配置
I/O 调度器 决定请求的处理顺序(电梯算法) mq-deadline / none / kyber
请求合并 将相邻扇区的请求合并为一个大请求 默认开启,减少中断次数
队列深度 允许同时排队的最大请求数 NVMe 通常为 1024
make_request_fn 自定义请求处理函数(绕过调度器) 常用于 RAM 磁盘或简单块设备
queue_flag 控制队列行为(如 QUEUE_FLAG_NONROT) 标记非旋转设备(SSD)

创建请求队列的标准方式:

// 分配请求队列(blk-mq 方式,现代驱动推荐)
struct request_queue *q;
q = blk_mq_alloc_queue(&tag_set, NULL, NULL);
blk_queue_make_request(q, my_make_request_fn);

// 或传统方式(单队列,适用于简单设备)
q = blk_init_queue(my_request_fn, &my_lock);
避坑提示:传统 blk_init_queue 使用全局锁,多核并发性能差。新驱动应优先使用 blk-mq(多队列)框架,它支持每个 CPU 核独立队列,大幅提升 SMP 扩展性。

2.5 block_device_operations:驱动回调函数表

struct block_device_operations 定义了块设备驱动的标准回调接口。与字符设备的 file_operations 不同,它不包含 read/write——这些由块层通过请求队列处理。

struct block_device_operations {
    int (*open)(struct block_device *, fmode_t);
    void (*release)(struct gendisk *, fmode_t);
    int (*ioctl)(struct block_device *, fmode_t, unsigned, unsigned long);
    int (*compat_ioctl)(struct block_device *, fmode_t, unsigned, unsigned long);
    unsigned int (*check_events)(struct gendisk *, unsigned int);
    int (*getgeo)(struct block_device *, struct hd_geometry *);
    int (*set_capacity)(struct gendisk *, unsigned long long);
    struct module *owner;
};
回调函数 触发时机 典型实现
open 设备被打开(mount / fdisk / dd) 检查设备状态、增加引用计数
release 设备被关闭 减少引用计数、清理资源
ioctl 用户态 ioctl 系统调用 处理 BLKGETSIZE64、HDIO_GETGEO 等
getgeo 获取磁盘几何参数 返回 CHS(柱面/磁头/扇区)值
check_events 检查介质是否变更 用于可移动设备(如 USB 磁盘)

2.6 块设备驱动初始化流程

一个完整的块设备驱动初始化遵循以下步骤。以简单的 RAM 磁盘驱动为例:

  1. 分配 gendisk:alloc_disk(minors)
  2. 设置操作函数表:disk->fops = &my_bd_ops
  3. 创建请求队列:disk->queue = blk_mq_alloc_queue(...)
  4. 设置容量:set_capacity(disk, sectors)
  5. 设置私有数据:disk->private_data = my_dev
  6. 注册设备:add_disk(disk)
块设备驱动初始化流程 ① alloc_disk() 分配 gendisk 结构 ② 设置 fops 绑定 bd_ops 回调 ③ 创建请求队列 blk_mq_alloc_queue ④ set_capacity() 设置磁盘扇区数 ⑤ 设置 private_data 关联驱动私有结构 ⑥ add_disk() 注册到内核块层 ⚠ 初始化顺序不可颠倒 • 必须在 add_disk() 之前完成所有字段设置,否则设备暴露后可能立即被访问导致空指针。 • add_disk() 之后设备立即可用,驱动必须确保此时所有资源已就绪。

2.7 完整的最小块设备驱动骨架

以下代码展示了一个极简 RAM 磁盘驱动的核心注册部分,省略了请求处理细节:

#include <linux/module.h>
#include <linux/blkdev.h>
#include <linux/hdreg.h>

static struct my_ramdisk {
    struct gendisk *disk;
    struct request_queue *queue;
    spinlock_t lock;
    u8 *data;           // 模拟存储空间
    unsigned long size; // 设备大小(字节)
} *my_dev;

static const struct block_device_operations my_bd_ops = {
    .owner   = THIS_MODULE,
    .open    = my_open,
    .release = my_release,
    .getgeo  = my_getgeo,
};

static int __init my_init(void)
{
    int ret = -ENOMEM;

    // 1. 分配设备结构
    my_dev = kzalloc(sizeof(*my_dev), GFP_KERNEL);
    if (!my_dev)
        return -ENOMEM;

    spin_lock_init(&my_dev->lock);

    // 2. 分配存储空间(64MB)
    my_dev->size = 64 * 1024 * 1024;
    my_dev->data = vmalloc(my_dev->size);
    if (!my_dev->data)
        goto err_free_dev;

    // 3. 分配请求队列(blk-mq)
    my_dev->queue = blk_mq_alloc_queue(&my_tag_set, NULL, NULL);
    if (IS_ERR(my_dev->queue)) {
        ret = PTR_ERR(my_dev->queue);
        goto err_free_data;
    }
    blk_queue_make_request(my_dev->queue, my_make_request_fn);

    // 4. 分配 gendisk(支持 8 个分区)
    my_dev->disk = alloc_disk(8);
    if (!my_dev->disk)
        goto err_free_queue;

    // 5. 填充 gendisk 字段
    my_dev->disk->major       = my_major;
    my_dev->disk->first_minor = 0;
    my_dev->disk->fops        = &my_bd_ops;
    my_dev->disk->queue       = my_dev->queue;
    my_dev->disk->private_data = my_dev;
    snprintf(my_dev->disk->disk_name, 32, "myram");
    set_capacity(my_dev->disk, my_dev->size / 512);

    // 6. 注册设备
    add_disk(my_dev->disk);

    pr_info("myram: 64MB RAM disk registered\n");
    return 0;

err_free_queue:
    blk_mq_free_queue(my_dev->queue);
err_free_data:
    vfree(my_dev->data);
err_free_dev:
    kfree(my_dev);
    return ret;
}

module_init(my_init);
学习要点:上述骨架展示了块设备驱动的“注册三部曲”——分配资源 → 填充结构 → 注册设备。实际项目中,请求处理函数 my_make_request_fn 需要实现 BIO 到存储介质的映射,这部分将在后续章节详细展开。

2.8 本章小结

  • 块设备与字符设备的本质区别在于:随机块访问 vs 顺序字节流,以及是否支持文件系统挂载。
  • 块设备驱动框架由 gendisk(磁盘抽象)、request_queue(请求调度)、block_device_operations(回调函数)三部分组成。
  • 初始化必须遵循严格顺序:alloc_disk → 设置字段 → 创建队列 → set_capacity → add_disk。
  • 现代驱动应使用 blk-mq 框架以获得更好的多核扩展性。