6. 块设备驱动框架与开发流程

块设备驱动是Linux存储IO栈的最终落地环节,是内核块设备子系统与物理存储硬件之间的桥梁,向上接入blk-mq多队列框架,向下对接硬件的寄存器、DMA、中断逻辑,完成IO请求从内核指令到物理硬件读写的最终转换。
Linux 6.0内核完全移除了传统单队列块层的request_fn驱动接口,所有块设备驱动必须基于blk-mq多队列框架开发。本章节基于Linux 6.6 LTS内核,完整拆解blk-mq块设备驱动的核心架构、数据结构、标准开发流程,提供可运行的最小化虚拟块设备驱动示例,同时总结生产环境驱动开发的最佳实践与避坑指南。

6.1 块设备驱动的核心架构与职责

块设备驱动的核心职责,是把blk-mq层下发的struct requestIO请求,转换为物理存储硬件可识别的指令,控制硬件完成数据读写,并在IO完成后通知内核,完成整个IO链路的闭环。
6.1.1 驱动在IO栈中的位置
衔接前序章节的IO全链路,块设备驱动在IO栈中的定位如下:
通用块层 → BIO封装为request → blk-mq软件队列 → IO调度器 → blk-mq硬件队列 → 【块设备驱动】 → 物理存储硬件
驱动是内核IO栈的最后一环,也是唯一和硬件直接交互的环节,所有内核的IO优化最终都需要驱动的配合才能落地。
6.1.2 blk-mq驱动的分层设计
基于blk-mq的块设备驱动采用分层设计,彻底解耦了通用块层逻辑、多队列调度逻辑与硬件相关逻辑,各层职责清晰:
  1. 通用块层适配层:实现块设备的标准操作接口(block_device_operations),处理块设备的打开、关闭、ioctl、介质检查等通用操作,向上对VFS/通用块层屏蔽硬件差异;
  2. blk-mq核心适配层:实现blk_mq_ops操作函数集,接入blk-mq多队列框架,处理IO请求的下发、完成、超时、错误处理,是驱动的核心逻辑;
  3. 硬件操作层:硬件相关的逻辑,包括寄存器读写、DMA映射、中断处理、硬件初始化/复位、电源管理,完全和硬件强相关,不同的存储硬件(NVMe/SATA/RAID/虚拟设备)实现完全不同。
6.1.3 驱动的核心设计原则
  1. 无阻塞设计:核心的queue_rq请求下发函数必须是非阻塞的,不能在中断上下文执行睡眠/阻塞操作,否则会导致内核死锁;
  2. 异步完成:IO请求下发给硬件后立即返回,硬件执行完成后通过中断通知CPU,在中断上下文中完成IO请求的收尾,避免阻塞内核线程;
  3. 错误处理与重试:必须处理硬件IO错误、超时、总线异常等场景,支持请求重试、错误恢复、硬件复位,保证驱动的鲁棒性;
  4. 内存屏障与并发安全:多核场景下,必须通过内存屏障保证硬件寄存器读写、DMA缓冲区的内存一致性,避免竞态导致的数据损坏;
  5. 热插拔支持:企业级驱动必须支持设备热插拔、动态上下线,处理设备意外移除的场景,避免内核panic。

6.2 驱动开发核心数据结构

本章节基于Linux 6.6内核源码,聚焦驱动开发必须掌握的核心数据结构,重点讲解驱动开发视角下的字段含义与使用方法,前序章节已详细拆解的结构仅补充驱动相关的核心要点。
6.2.1 驱动核心操作集:struct blk_mq_ops
struct blk_mq_ops是blk-mq驱动的核心入口,驱动必须实现这套接口,才能接入blk-mq框架,前序章节已给出完整定义,这里重点讲解驱动开发中必须实现的核心回调函数:
struct blk_mq_ops {
    // 【必须实现】IO请求下发入口,blk-mq把request请求下发给驱动
    blk_status_t (*queue_rq)(struct blk_mq_hw_ctx *hctx, const struct blk_mq_queue_data *bd);
    // 【必须实现】IO完成回调,硬件IO完成后触发,处理请求收尾
    void (*complete)(struct request *rq, blk_status_t error);
    // 硬件队列初始化,设备加载时为每个硬件队列执行一次
    int (*init_hctx)(struct blk_mq_hw_ctx *hctx, void *driver_data, unsigned int hctx_idx);
    // 硬件队列清理,设备卸载时执行
    void (*exit_hctx)(struct blk_mq_hw_ctx *hctx, unsigned int hctx_idx);
    // 检查硬件队列是否繁忙,用于blk-mq的负载均衡
    int (*busy)(struct blk_mq_hw_ctx *hctx);
    // 超时处理,IO请求超时后触发,用于错误恢复
    enum blk_eh_timer_return (*timeout)(struct request *rq, bool reserved);
};

驱动开发核心要点:

  • queue_rq是驱动的核心入口,所有IO请求都会通过这个函数下发,必须保证非阻塞,执行失败时返回对应的错误码,不能睡眠;
  • complete回调用于处理IO完成后的收尾工作,比如释放DMA缓冲区、更新统计信息,运行在软中断上下文,不能执行阻塞操作;
  • timeout回调是驱动鲁棒性的核心,必须处理IO超时的场景,比如硬件卡死、请求丢失,需要实现硬件复位、请求重试逻辑。
6.2.2 IO请求:struct request
struct request是blk-mq层下发给驱动的IO请求封装,包含了本次IO的所有信息,驱动的核心工作就是解析这个结构体,转换为硬件指令。核心字段(驱动开发视角):
struct request {
    // 该请求所属的硬件队列上下文
    struct blk_mq_hw_ctx *mq_hctx;
    // 该请求所属的IO队列
    struct request_queue *q;
    // 该请求包含的BIO链表,所有IO数据都在这里
    struct bio *bio;
    // IO操作类型与标志位,和BIO的bi_opf一致
    unsigned int cmd_flags;
    // IO的起始扇区,单位512字节
    sector_t __sector;
    // IO的总字节数
    unsigned int __data_len;
    // 硬件私有数据,驱动可以存储自定义的硬件命令、DMA地址等信息
    void *end_io_data;
    // 请求的超时时间
    unsigned long deadline;
    // 用于把请求链接到硬件队列的链表节点
    struct list_head queuelist;
    // 引用计数
    refcount_t ref_count;
};

驱动开发核心API与要点:

1.请求解析核心API:

  • blk_rq_pos(rq):获取IO的起始扇区;
  • blk_rq_sectors(rq):获取IO的总扇区数;
  • blk_rq_bytes(rq):获取IO的总字节数;
  • rq_data_dir(rq):获取IO方向,0=读,1=写;
  • bio_for_each_segment(bvec, bio, iter):遍历请求中所有BIO的内存缓冲区,是驱动处理分散-聚集IO的核心循环。

2.核心要点:

  • 一个request请求可以包含多个BIO,每个BIO可以包含多个不连续的内存页,驱动必须遍历所有BIO和bio_vec,才能获取完整的IO缓冲区信息;
  • 驱动不能直接修改request的核心字段,只能通过内核提供的API访问,否则会破坏blk-mq的内部状态;
  • 硬件私有数据可以存储在end_io_data字段,在IO完成回调中可以读取,用于传递硬件命令、DMA地址等上下文。
6.2.3 通用磁盘结构体:struct gendisk
struct gendisk是块设备的顶层抽象,驱动必须创建并初始化gendisk实例,才能向内核注册一个块设备,在/dev目录下创建设备节点。驱动开发中,gendisk的核心初始化要点:
  1. 必须设置disk_name,对应/dev下的设备节点名称;
  2. 必须绑定初始化完成的request_queue;
  3. 必须实现fops字段,也就是struct block_device_operations操作集;
  4. 必须设置capacity字段,也就是块设备的总容量,单位512字节扇区;
  5. 必须设置major主设备号,可静态指定,也可由内核动态分配;
  6. 必须设置minors次设备号数量,对应设备支持的最大分区数。
6.2.4 块设备操作集:struct block_device_operations
struct block_device_operations是块设备对通用块层/VFS层的标准接口,处理设备的打开、关闭、ioctl、介质检查等操作,驱动必须实现这套接口,核心函数:
struct block_device_operations {
    // 设备打开时调用,对应用户态open()设备节点
    int (*open) (struct block_device *, fmode_t);
    // 设备关闭时调用,对应用户态close()
    void (*release) (struct gendisk *, fmode_t);
    // ioctl处理,对应用户态ioctl(),用于设备配置、参数查询
    int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
    // 检查介质是否变化,比如U盘插拔、光盘更换
    int (*media_changed) (struct gendisk *);
    // 介质变化后重新验证设备
    int (*revalidate_disk) (struct gendisk *);
    // 所有者模块,一般设置为THIS_MODULE,防止驱动被意外卸载
    struct module *owner;
};

6.3 blk-mq块设备驱动完整开发流程

基于Linux 6.6内核,一个标准的blk-mq块设备驱动,从加载到卸载的完整开发流程分为7个核心步骤,所有步骤都有对应的内核标准API,严格遵循内核开发规范。
步骤1:定义并实现blk_mq_ops核心回调函数
这是驱动开发的第一步,也是核心逻辑所在,必须实现queue_rq和complete两个核心回调,根据硬件需求实现其他可选回调。
核心实现要点:
  • queue_rq函数:解析request请求,构建硬件命令,下发给硬件,立即返回执行结果,不能阻塞;
  • 执行结果返回值:
    • BLK_STS_OK:请求下发成功,等待硬件完成;
    • BLK_STS_IOERR:硬件IO错误;
    • BLK_STS_RESOURCE:硬件资源不足,请求需要重新入队;
    • BLK_STS_DEV_RESOURCE:设备繁忙,请求需要延迟重试。
  • complete函数:处理IO完成后的收尾工作,释放驱动分配的资源,更新统计信息。
步骤2:分配并初始化request_queue IO队列
gendisk必须绑定一个初始化完成的request_queue,驱动通过blk_mq_init_queue()API创建队列,这是Linux 6.6内核的标准API,替代了旧的blk_init_queue()。
API原型:
struct request_queue *blk_mq_init_queue(struct blk_mq_tag_set *set);

核心流程:

1.定义并初始化struct blk_mq_tag_set标签集,这是blk-mq队列的配置结构体,核心字段:

struct blk_mq_tag_set tag_set = {
    .ops = &my_blk_mq_ops, // 我们实现的blk_mq_ops回调集
    .nr_hw_queues = 1,     // 硬件队列数量,NVMe设备通常等于CPU核心数,虚拟设备默认1
    .queue_depth = 128,    // 每个硬件队列的最大队列深度
    .numa_node = NUMA_NO_NODE, // NUMA节点
    .cmd_size = sizeof(struct my_hw_cmd), // 硬件命令私有数据大小
    .flags = BLK_MQ_F_SHOULD_MERGE, // 开启IO合并
    .driver_data = my_dev, // 驱动私有设备结构体
};

2.调用blk_mq_alloc_tag_set(&tag_set)分配标签集资源;

3.调用blk_mq_init_queue(&tag_set)创建request_queue队列;

4.初始化队列的参数:逻辑块大小、物理块大小、最大IO大小、队列标志等。

步骤3:分配并初始化gendisk通用磁盘实例
通过alloc_disk()API分配gendisk实例,Linux 6.6内核的API原型:
struct gendisk *alloc_disk(int minors);
  • minors参数:次设备号的数量,也就是设备支持的最大分区数,通常设置为16,对应1个整盘+15个分区。
初始化gendisk的核心动作:
  1. 设置disk_name:设备节点名称,比如myramdisk,对应/dev/myramdisk;
  2. 绑定queue:把步骤2创建的request_queue赋值给gendisk->queue;
  3. 绑定fops:把实现的block_device_operations赋值给gendisk->fops;
  4. 设置major主设备号:可通过register_blkdev()静态申请,或由内核动态分配;
  5. 设置first_minor和minors:次设备号的起始值和数量;
  6. 设置capacity:设备的总容量,单位512字节扇区,必须正确设置,否则内核无法识别设备大小;
  7. 设置owner:一般设置为THIS_MODULE,防止驱动被意外卸载。
步骤4:注册gendisk,创建设备节点
gendisk初始化完成后,调用add_disk()API向内核注册块设备:
int add_disk(struct gendisk *disk);
  • 调用成功后,内核会自动在/dev目录下创建设备节点,同时在/sys/block/目录下创建设备的sysfs接口;
  • 注册完成后,设备就可以被用户态打开、挂载、读写了。
步骤5:实现中断处理与IO完成逻辑
硬件执行完IO请求后,会触发硬件中断,驱动必须实现中断处理函数,完成IO的闭环:
  1. 中断顶半部:快速响应中断,确认中断来源,清除中断标志,触发软中断,不能执行耗时操作;
  2. 中断底半部:通过软中断/工作队列处理IO完成逻辑,解析硬件执行结果,设置request的状态,调用blk_mq_complete_request()API通知blk-mq层IO完成;
  3. blk_mq_complete_request()会触发我们实现的complete回调函数,完成最终的收尾工作。
核心API:
// 通知blk-mq层IO请求完成 
void blk_mq_complete_request(struct request *rq, blk_status_t status);
  • status参数:IO执行结果,BLK_STS_OK表示成功,其他值表示对应的错误码。
步骤6:实现设备卸载与资源清理
驱动模块卸载时,必须按正确的顺序释放资源,否则会导致内存泄漏、内核panic,正确的释放顺序:
  1. 调用del_gendisk():从内核注销gendisk,删除/dev下的设备节点,阻止新的IO请求进入;
  2. 调用blk_cleanup_queue():清理request_queue,等待所有正在处理的IO请求完成;
  3. 调用blk_mq_free_tag_set():释放blk-mq标签集资源;
  4. 调用put_disk():释放gendisk实例的引用计数;
  5. 调用unregister_blkdev():释放申请的主设备号;
  6. 释放驱动分配的硬件资源、内存缓冲区、中断线等。
步骤7:实现错误处理、超时处理与热插拔逻辑
企业级驱动必须实现这部分逻辑,保证驱动的鲁棒性:
  1. 超时处理:实现blk_mq_ops->timeout回调,处理IO请求超时的场景,比如硬件卡死、请求丢失,需要实现硬件复位、请求重试、错误恢复逻辑;
  2. 错误处理:处理硬件IO错误、总线错误、DMA错误,实现错误统计、降级、重试机制;
  3. 热插拔处理:实现设备的热插拔、意外移除的处理逻辑,避免设备被意外拔出时导致内核panic;
  4. 统计与监控:实现IO统计、错误统计、硬件状态监控,通过sysfs接口暴露给用户态,方便运维监控。

6.4 最小化blk-mq虚拟块设备驱动示例

下面提供一个基于Linux 6.6内核的最小化RAMDISK虚拟块设备驱动,完整实现了blk-mq驱动的核心流程,可直接编译运行,用于学习和理解驱动开发的核心逻辑。
注意:这是示例代码,仅用于学习,未做错误处理和鲁棒性优化,禁止直接用于生产环境。
6.4.1 完整驱动代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/blkdev.h>
#include <linux/blk-mq.h>
#include <linux/genhd.h>
#include <linux/vmalloc.h>
#include <linux/hdreg.h>

// 模块参数:RAMDISK大小,单位MB,默认64MB
static int rd_size_mb = 64;
module_param(rd_size_mb, int, 0444);
MODULE_PARM_DESC(rd_size_mb, "RAMDisk size in MB, default 64MB");

// 设备名称
#define RD_DEV_NAME "myramdisk"
// 主设备号,0表示内核动态分配
static int rd_major = 0;
// 次设备号数量,支持15个分区
#define RD_MINORS 16
// 每个硬件队列的深度
#define RD_QUEUE_DEPTH 128
// 扇区大小,固定512字节
#define RD_SECTOR_SIZE 512

// 驱动私有设备结构体
struct rd_dev {
    struct gendisk *gd;             // 通用磁盘实例
    struct request_queue *queue;     // IO请求队列
    struct blk_mq_tag_set tag_set;   // blk-mq标签集
    unsigned char *data_buf;         // RAMDISK的内存缓冲区
    sector_t total_sectors;          // 总扇区数
    spinlock_t lock;                  // 保护缓冲区的自旋锁
};
static struct rd_dev *my_rd_dev;

// ====================== 块设备操作集实现 ======================
static int rd_open(struct block_device *bdev, fmode_t mode)
{
    pr_info("%s: RAMDisk device opened\n", RD_DEV_NAME);
    return 0;
}

static void rd_release(struct gendisk *gd, fmode_t mode)
{
    pr_info("%s: RAMDisk device closed\n", RD_DEV_NAME);
}

static const struct block_device_operations rd_fops = {
    .owner = THIS_MODULE,
    .open = rd_open,
    .release = rd_release,
};

// ====================== blk-mq核心回调实现 ======================
// IO完成回调
static void rd_complete_rq(struct request *rq, blk_status_t error)
{
    pr_debug("%s: IO request completed, status=%d\n", RD_DEV_NAME, error);
}

// IO请求下发核心函数
static blk_status_t rd_queue_rq(struct blk_mq_hw_ctx *hctx, const struct blk_mq_queue_data *bd)
{
    struct request *rq = bd->rq;
    struct rd_dev *dev = rq->q->queuedata;
    struct bio_vec bvec;
    struct bvec_iter iter;
    sector_t start_sector = blk_rq_pos(rq);
    unsigned int total_sectors = blk_rq_sectors(rq);
    unsigned long offset;
    unsigned long flags;
    blk_status_t status = BLK_STS_OK;

    // 开始处理请求,通知blk-mq层
    blk_mq_start_request(rq);

    // 检查IO范围是否超出设备容量
    if (start_sector + total_sectors > dev->total_sectors) {
        pr_err("%s: IO out of range, sector=%llu, count=%u\n",
               RD_DEV_NAME, start_sector, total_sectors);
        status = BLK_STS_IOERR;
        goto complete;
    }

    // 计算IO在内存缓冲区中的起始偏移
    offset = start_sector * RD_SECTOR_SIZE;
    spin_lock_irqsave(&dev->lock, flags);

    // 遍历所有BIO的内存段,执行读写
    bio_for_each_segment(bvec, rq->bio, iter) {
        void *buf = page_address(bvec.bv_page) + bvec.bv_offset;
        unsigned int len = bvec.bv_len;

        if (rq_data_dir(rq) == WRITE) {
            // 写操作:从用户缓冲区复制到RAMDISK缓冲区
            memcpy(dev->data_buf + offset, buf, len);
        } else {
            // 读操作:从RAMDISK缓冲区复制到用户缓冲区
            memcpy(buf, dev->data_buf + offset, len);
        }
        offset += len;
    }

    spin_unlock_irqrestore(&dev->lock, flags);

complete:
    // 通知blk-mq层IO完成
    blk_mq_end_request(rq, status);
    return BLK_STS_OK;
}

// blk-mq操作集
static const struct blk_mq_ops rd_mq_ops = {
    .queue_rq = rd_queue_rq,
    .complete = rd_complete_rq,
};

// ====================== 模块初始化与卸载 ======================
static int __init rd_driver_init(void)
{
    int ret;
    sector_t total_sectors;
    unsigned long total_bytes;

    pr_info("%s: Initializing RAMDisk driver, size=%dMB\n", RD_DEV_NAME, rd_size_mb);

    // 计算总容量
    total_bytes = rd_size_mb * 1024 * 1024;
    total_sectors = total_bytes / RD_SECTOR_SIZE;

    // 分配驱动私有设备结构体
    my_rd_dev = kzalloc(sizeof(struct rd_dev), GFP_KERNEL);
    if (!my_rd_dev) {
        pr_err("%s: Failed to alloc device struct\n", RD_DEV_NAME);
        return -ENOMEM;
    }
    my_rd_dev->total_sectors = total_sectors;
    spin_lock_init(&my_rd_dev->lock);

    // 分配RAMDISK的内存缓冲区
    my_rd_dev->data_buf = vmalloc(total_bytes);
    if (!my_rd_dev->data_buf) {
        pr_err("%s: Failed to alloc data buffer\n", RD_DEV_NAME);
        ret = -ENOMEM;
        goto err_free_dev;
    }
    memset(my_rd_dev->data_buf, 0, total_bytes);

    // 1. 动态申请主设备号
    rd_major = register_blkdev(rd_major, RD_DEV_NAME);
    if (rd_major <= 0) {
        pr_err("%s: Failed to register block device\n", RD_DEV_NAME);
        ret = -EIO;
        goto err_free_buf;
    }
    pr_info("%s: Allocated major number %d\n", RD_DEV_NAME, rd_major);

    // 2. 初始化blk-mq标签集
    memset(&my_rd_dev->tag_set, 0, sizeof(my_rd_dev->tag_set));
    my_rd_dev->tag_set.ops = &rd_mq_ops;
    my_rd_dev->tag_set.nr_hw_queues = 1; // 单硬件队列
    my_rd_dev->tag_set.queue_depth = RD_QUEUE_DEPTH;
    my_rd_dev->tag_set.numa_node = NUMA_NO_NODE;
    my_rd_dev->tag_set.cmd_size = 0;
    my_rd_dev->tag_set.flags = BLK_MQ_F_SHOULD_MERGE;
    my_rd_dev->tag_set.driver_data = my_rd_dev;

    ret = blk_mq_alloc_tag_set(&my_rd_dev->tag_set);
    if (ret) {
        pr_err("%s: Failed to alloc tag set\n", RD_DEV_NAME);
        goto err_unregister_blkdev;
    }

    // 3. 创建request_queue
    my_rd_dev->queue = blk_mq_init_queue(&my_rd_dev->tag_set);
    if (IS_ERR(my_rd_dev->queue)) {
        pr_err("%s: Failed to init request queue\n", RD_DEV_NAME);
        ret = PTR_ERR(my_rd_dev->queue);
        goto err_free_tag_set;
    }
    my_rd_dev->queue->queuedata = my_rd_dev;
    // 设置队列的逻辑块大小
    blk_queue_logical_block_size(my_rd_dev->queue, RD_SECTOR_SIZE);
    blk_queue_physical_block_size(my_rd_dev->queue, RD_SECTOR_SIZE);
    // 设置最大IO大小
    blk_queue_max_hw_sectors(my_rd_dev->queue, 1024);

    // 4. 分配gendisk实例
    my_rd_dev->gd = alloc_disk(RD_MINORS);
    if (!my_rd_dev->gd) {
        pr_err("%s: Failed to alloc gendisk\n", RD_DEV_NAME);
        ret = -ENOMEM;
        goto err_cleanup_queue;
    }

    // 5. 初始化gendisk
    my_rd_dev->gd->major = rd_major;
    my_rd_dev->gd->first_minor = 0;
    my_rd_dev->gd->minors = RD_MINORS;
    my_rd_dev->gd->fops = &rd_fops;
    my_rd_dev->gd->queue = my_rd_dev->queue;
    my_rd_dev->gd->private_data = my_rd_dev;
    // 设置设备名称
    snprintf(my_rd_dev->gd->disk_name, DISK_NAME_LEN, RD_DEV_NAME);
    // 设置设备容量
    set_capacity(my_rd_dev->gd, total_sectors);

    // 6. 注册gendisk,创建设备节点
    add_disk(my_rd_dev->gd);
    pr_info("%s: RAMDisk initialized successfully, /dev/%s created, total %llu sectors\n",
            RD_DEV_NAME, RD_DEV_NAME, total_sectors);

    return 0;

// 错误处理:按申请的反顺序释放资源
err_cleanup_queue:
    blk_cleanup_queue(my_rd_dev->queue);
err_free_tag_set:
    blk_mq_free_tag_set(&my_rd_dev->tag_set);
err_unregister_blkdev:
    unregister_blkdev(rd_major, RD_DEV_NAME);
err_free_buf:
    vfree(my_rd_dev->data_buf);
err_free_dev:
    kfree(my_rd_dev);
    return ret;
}

static void __exit rd_driver_exit(void)
{
    pr_info("%s: Exiting RAMDisk driver\n", RD_DEV_NAME);

    // 按正确顺序释放资源
    del_gendisk(my_rd_dev->gd);
    put_disk(my_rd_dev->gd);
    blk_cleanup_queue(my_rd_dev->queue);
    blk_mq_free_tag_set(&my_rd_dev->tag_set);
    unregister_blkdev(rd_major, RD_DEV_NAME);
    vfree(my_rd_dev->data_buf);
    kfree(my_rd_dev);

    pr_info("%s: RAMDisk driver exited\n", RD_DEV_NAME);
}

module_init(rd_driver_init);
module_exit(rd_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Linux Block Driver Demo");
MODULE_DESCRIPTION("Minimal blk-mq RAMDisk Block Device Driver");
MODULE_VERSION("1.0");

6.4.2 驱动编译与运行

1.Makefile编写:

obj-m += myramdisk.o
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

all:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules

clean:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) clean

2.编译驱动:执行make命令,生成myramdisk.ko内核模块;

3.加载驱动:执行sudo insmod myramdisk.ko rd_size_mb=128,创建128MB的RAMDISK;

4.验证设备:执行ls -l /dev/myramdisk,可以看到设备节点,执行fdisk -l /dev/myramdisk可以看到设备容量;

5.使用设备:可以格式化、挂载设备:

sudo mkfs.ext4 /dev/myramdisk
sudo mkdir /mnt/ramdisk
sudo mount /dev/myramdisk /mnt/ramdisk

5.卸载驱动:先卸载挂载点,再卸载模块:

sudo umount /mnt/ramdisk
sudo rmmod myramdisk

6.5 驱动开发工程实践与避坑指南

6.5.1 核心避坑指南

1.中断上下文的致命限制:

  • 中断顶半部、queue_rq函数、complete回调函数、软中断上下文,绝对不能执行任何睡眠、阻塞、耗时操作,比如mutex锁、kmalloc(GFP_KERNEL)、msleep等,否则会导致内核死锁、panic;
  • 必须使用自旋锁保护共享资源,使用GFP_ATOMIC标志分配原子内存,不能使用可能睡眠的API。

2.内存屏障与DMA一致性:

  • 硬件DMA操作必须使用DMA映射API(dma_map_single()/dma_unmap_single()),不能直接使用内核虚拟地址,否则会导致cache不一致,数据损坏;
  • 多核场景下,必须使用内存屏障(mb()/wmb()/rmb())保证硬件寄存器读写、DMA缓冲区的内存一致性,避免竞态导致的IO错误。

3.请求处理的竞态问题:

  • 不能直接修改struct request的核心字段,必须使用内核提供的标准API,否则会破坏blk-mq的内部状态,导致IO错乱、内核panic;
  • 必须保证一个request请求只被完成一次,重复调用blk_mq_complete_request()会导致内核崩溃。

4.资源释放的顺序问题:

  • 驱动卸载时,必须严格按照del_gendisk → blk_cleanup_queue → blk_mq_free_tag_set → unregister_blkdev的顺序释放资源,否则会导致正在处理的IO请求访问已释放的内存,触发内核Oops;
  • del_gendisk()会阻止新的IO请求进入,必须等待所有正在处理的IO完成后,才能释放后续资源。

5.IO超时与错误处理:

  • 必须实现timeout超时回调,处理IO请求超时的场景,否则硬件卡死会导致IO请求永远挂起,系统负载飙升;
  • 必须处理硬件IO错误,不能直接忽略,否则会导致数据损坏而不自知,必须实现错误统计、重试、降级机制。
6.5.2 生产环境驱动开发最佳实践
  1. 遵循内核开发规范:严格遵循Linux内核的编码规范,使用内核提供的标准API,不使用废弃的接口,保证驱动的兼容性和可维护性;
  2. 完善的错误处理:所有API调用都必须检查返回值,处理所有可能的错误场景,保证驱动的鲁棒性,不会因为单个错误导致内核panic;
  3. 可观测性设计:通过sysfs/debugfs接口暴露驱动的统计信息、硬件状态、错误计数,方便运维监控、问题排查;
  4. 性能优化:针对硬件特性优化队列深度、硬件队列数量、中断亲和性,实现零拷贝、DMA聚合,最大化硬件性能;
  5. 兼容性测试:在不同的内核版本、不同的硬件平台、不同的业务场景下做充分的测试,保证驱动的稳定性和兼容性;
  6. 安全设计:严格校验用户态传入的ioctl参数,防止恶意输入导致的缓冲区溢出、权限提升,保证驱动的安全性。
6.5.3 驱动调试技巧
  1. 内核日志调试:使用pr_debug()/dev_dbg()打印调试日志,通过dmesg/journalctl查看,开启内核动态调试;
  2. ftrace跟踪:使用ftrace跟踪驱动的函数执行、IO请求流程,定位性能瓶颈、错误路径;
  3. BUG_ON/WARN_ON:在关键位置使用WARN_ON检查异常状态,使用BUG_ON捕获致命错误,快速定位问题;
  4. lockdep锁检测:开启内核的lockdep锁检测功能,排查死锁、锁竞态问题;
  5. kasan内存检测:开启内核的KASAN内存检测功能,排查内存越界、泄漏、释放后使用等问题。
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐