ESC
输入关键词搜索文章标题和内容

块设备驱动开发:从gendisk到blk-mq的实战指南

本文由 linuxROS 整理发布,首发于 linuxros.cn,转载请注明出处。

块设备驱动开发:从gendisk到blk-mq的实战指南

块设备和字符设备是Linux驱动的两大阵营,字符设备大家写得多了,块设备却很少有人碰。原因很简单——块设备驱动门槛高,gendisk、request_queue、bio、blk-mq一堆概念,加上6.8内核里传统请求队列已经基本被blk-mq取代,网上老文章的代码跑不通,新手更懵。本文基于Linux 6.8内核,从块设备和字符设备的区别讲起,把核心数据结构过一遍,再用完整RAM磁盘驱动把blk-mq框架串起来,最后给调试方法和常见坑点。

一、块设备vs字符设备

维度 块设备 字符设备
数据访问 按扇区(512B)寻址 按字节流访问
缓冲机制 经过页缓存(page cache) 不经过页缓存
随机访问 支持,可跳到任意扇区 一般不支持
最小寻址单位 扇区(512字节) 字节
典型设备 硬盘、SSD、U盘、RAM磁盘 串口、按键、LED

块设备的核心特征就是按扇区寻址。一个扇区(512字节),不管你个字节还是4096个字节,底层都是按扇区对齐操作。另一个关键区别是页缓存——块设备的读写默认经过页缓存,同一个数据块多次读取时直接从内存返回,不用反复访问硬件,字符设备没有这层缓存,每次read/write都直达驱动。

二、核心数据结构

块设备驱动打交道最多的五个结构体:gendisk、block_device_operations、request_queue、bio、bio_vec。

gendisk:磁盘设备表示

// linux/genhd.h(简化)
struct gendisk {
    int major;                       // 主设备号
    int first_minor;                 // 起始次设备号
    int minors;                      // 次设备号数量(分区数+1。    const struct block_device_operations *fops;  // 操作函数集    struct request_queue *queue;     // 请求队列
    char disk_name[DISK_NAME_LEN];  // 设备名,如sda
    unsigned int flags;              // 标志位,如GENHD_FL_NO_PART
    // ... 更多字段
};
  • major:主设备号,向内核申请或动态分支- first_minor/minors:次设备号范围,minors至少为1(整个盘),要支持分区就多分配几。- fops:操作函数集,类似字符设备的file_operations,但简单得多。- queue:请求队列,6.8中必须是blk-mq队列

block_device_operations:块设备操作集

// linux/blkdev.h(简化)
struct block_device_operations {
    int (*open)(struct gendisk *disk, blk_mode_t mode);
    void (*release)(struct gendisk *disk);
    int (*ioctl)(struct block_device *bdev, blk_mode_t mode,
                 unsigned cmd, unsigned long arg);
    int (*getgeo)(struct block_device *bdev, struct hd_geometry *geo);
    // 6.8中owner字段已移除,模块引用计数由内核自动管理};

注意6.8的变化:open的第二个参数从fmode_t改成了blk_mode_t,owner字段被移除,块设备的读写操作不在fops里——数据传输由请求队列处理,fops只管open/release/ioctl这些控制操作集

request_queue:请求队列

// linux/blk-mq.h(简化)
struct request_queue {
    struct blk_mq_tag_set *tag_set;  // blk-mq标签。    struct request *rq;              // 当前请求
    // ... 大量内部字段,驱动一般不直接操作
};

6.8内核中,传统request_queue已经完全被blk-mq取代。驱动不再直接操作request_queue,而是通过blk_mq_tag_set和blk_mq_ops来管理队列。

bio:块I/O请求

// linux/blk_types.h(简化)
struct bio {
    struct block_device *bi_bdev;    // 目标块设。    struct bvec_iter bi_iter;        // 迭代器,含bi_sector
    unsigned short bi_vcnt;          // bio_vec数量
    unsigned short bi_idx;           // 当前bio_vec索引
    struct bio_vec *bi_io_vec;       // bio_vec数组
    unsigned int bi_opf;             // 操作标志(读/。flush等)
    // ... 更多字段
};

struct bvec_iter {
    sector_t bi_sector;     // 起始扇区。    unsigned int bi_size;   // 剩余字节。    unsigned int bi_idx;    // 当前bio_vec索引
    unsigned int bi_bvec_done;  // 当前bio_vec内已处理字节。};

bio是块层发给驱动的I/O请求。一个bio描述一次连续的I/O操作,起始扇区在bi_iter.bi_sector,数据存放在bi_io_vec数组里。bio和request的关系:bio是块层提交的原始请求,request是blk-mq调度后包装的结构,一个request可能合并了多个bio。

bio_vec:段描述

// linux/bvec.h
struct bio_vec {
    struct page *bv_page;    // 数据所在的内存。    unsigned int bv_len;     // 数据长度(字节)
    unsigned int bv_offset;  // 页内偏移
};

bio_vec描述bio中的一个段。一个bio可以包含多个bio_vec(即多段),每段指向一个内存页的不同区域。块设备驱动遍历bio_vec来实际搬运数据。

三、块设备驱动开发流程

flowchart TB A(["开销"]) --> B["alloc_disk()分配gendisk"] B --> C["设置gendisk字段<br/>major/minors/fops/disk_name"] C --> D["blk_mq_alloc_tag_set()<br/>初始化blk-mq标签"] D --> E["blk_mq_init_queue()<br/>创建请求队列"] E --> F["设置队列参数<br/>物理块大小、队列深度"] F --> G["设置磁盘容量<br/>set_capacity()"] G --> H["add_disk()注册磁盘"] H --> I{"用户空间可见?"} I -->|"是"| J(["块设备可用"]) I -->|"否"| K["检查major号和fops"] K --> C style A fill:#E8F5E9 style D fill:#E3F2FD style E fill:#E3F2FD style I fill:#FFF8E1 style J fill:#E8F5E9 style K fill:#FFEBEE

6.8内核中推荐使用blk-mq框架,流程比传统方式多一步blk_mq_alloc_tag_set,但性能和扩展性更好。关键顺序:先分配gendisk,再初始化blk-mq,最后add_disk。add_disk之后设备就对用户空间可见了,这之前的顺序不能乱。

四、简单块设备驱动示例

下面是一个基于Linux 6.8的RAM磁盘驱动,用内存模拟磁盘,支持读写,使用blk-mq框架。

// my_blkdev.c - Linux 6.8 RAM磁盘驱动
#include <linux/module.h>
#include <linux/blk-mq.h>
#include <linux/blkdev.h>
#include <linux/genhd.h>
#include <linux/hdreg.h>
#include <linux/vmalloc.h>

#define MY_DISK_NAME    "myram"
#define MY_DISK_MINORS  1
#define MY_DISK_SIZE    (4 * 1024 * 1024)  // 4MB
#define SECTOR_SIZE     512
#define NUM_SECTORS     (MY_DISK_SIZE / SECTOR_SIZE)

// 驱动私有数据
struct my_blkdev {
    struct gendisk          *disk;      // 磁盘设备
    struct blk_mq_tag_set   tag_set;    // blk-mq标签。    void                    *data;      // 模拟磁盘的内存    spinlock_t              lock;       // 保护数据访问
};

static struct my_blkdev *my_dev;

// ---- blk-mq操作回调 ----

// 处理单个请求
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 my_blkdev *dev = rq->q->queuedata;
    struct bio_vec bvec;
    struct req_iterator iter;
    sector_t sector = blk_rq_pos(rq);
    loff_t offset = sector << SECTOR_SHIFT;
    loff_t remaining = blk_rq_bytes(rq);

    // 标记请求开始处。    blk_mq_start_request(rq);

    // 检查越。    if (offset + remaining > MY_DISK_SIZE) {
        blk_mq_end_request(rq, BLK_STS_IOERR);
        return BLK_STS_IOERR;
    }

    // 遍历请求中的每个bio。    rq_for_each_segment(bvec, rq, iter) {
        void *buf = kmap_local_page(bvec.bv_page);
        unsigned int len = bvec.bv_len;
        loff_t pos = offset + bvec.bv_offset;

        if (req_op(rq) == REQ_OP_READ) {
            // 读:从内存拷贝到bio
            memcpy(buf + bvec.bv_offset,
                   dev->data + pos, len);
        } else if (req_op(rq) == REQ_OP_WRITE) {
            // 写:从bio拷贝到内存            memcpy(dev->data + pos,
                   buf + bvec.bv_offset, len);
        }

        kunmap_local(buf);
        offset += len;
        remaining -= len;
    }

    blk_mq_end_request(rq, BLK_STS_OK);
    return BLK_STS_OK;
}

// 初始化硬件上下文
static int my_init_hctx(struct blk_mq_hw_ctx *hctx, void *data,
                         unsigned int index)
{
    return 0;
}

// blk-mq操作集static const struct blk_mq_ops my_mq_ops = {
    .queue_rq   = my_queue_rq,
    .init_hctx  = my_init_hctx,
};

// ---- block_device_operations ----

static int my_blk_open(struct gendisk *disk, blk_mode_t mode)
{
    struct my_blkdev *dev = disk->private_data;

    if (!dev)
        return -ENODEV;
    return 0;
}

static void my_blk_release(struct gendisk *disk)
{
    // RAM磁盘不需要特殊释放操。}

static int my_blk_ioctl(struct block_device *bdev, blk_mode_t mode,
                         unsigned int cmd, unsigned long arg)
{
    struct my_blkdev *dev = bdev->bd_disk->private_data;

    switch (cmd) {
    case HDIO_GETGEO: {
        struct hd_geometry geo;
        geo.cylinders = NUM_SECTORS / (4 * 16);
        geo.heads = 4;
        geo.sectors = 16;
        geo.start = 0;
        if (copy_to_user((void __user *)arg, &geo, sizeof(geo)))
            return -EFAULT;
        return 0;
    }
    default:
        return -ENOTTY;
    }
}

static const struct block_device_operations my_blk_fops = {
    .open       = my_blk_open,
    .release    = my_blk_release,
    .ioctl      = my_blk_ioctl,
};

// ---- 模块初始化与退。----

static int __init my_blkdev_init(void)
{
    int ret;

    // 分配私有数据
    my_dev = kzalloc(sizeof(*my_dev), GFP_KERNEL);
    if (!my_dev)
        return -ENOMEM;

    spin_lock_init(&my_dev->lock);

    // 分配模拟磁盘的内存    my_dev->data = vzalloc(MY_DISK_SIZE);
    if (!my_dev->data) {
        ret = -ENOMEM;
        goto fail_data;
    }

    // 分配gendisk
    my_dev->disk = blk_alloc_disk(NULL, NUMA_NO_NODE);
    if (IS_ERR(my_dev->disk)) {
        ret = PTR_ERR(my_dev->disk);
        goto fail_disk;
    }

    // 设置gendisk字段
    my_dev->disk->major = 0;  // 动态分配主设备。    my_dev->disk->first_minor = 0;
    my_dev->disk->minors = MY_DISK_MINORS;
    my_dev->disk->fops = &my_blk_fops;
    my_dev->disk->private_data = my_dev;
    snprintf(my_dev->disk->disk_name, DISK_NAME_LEN, MY_DISK_NAME);

    // 初始化blk-mq标签。    my_dev->tag_set.ops = &my_mq_ops;
    my_dev->tag_set.nr_hw_queues = 1;
    my_dev->tag_set.queue_depth = 128;
    my_dev->tag_set.numa_node = NUMA_NO_NODE;
    my_dev->tag_set.cmd_size = 0;
    my_dev->tag_set.flags = BLK_MQ_F_SHOULD_MERGE;
    my_dev->tag_set.driver_data = my_dev;

    ret = blk_mq_alloc_tag_set(&my_dev->tag_set);
    if (ret)
        goto fail_tag_set;

    // 创建请求队列
    my_dev->disk->queue = blk_mq_init_queue(&my_dev->tag_set);
    if (IS_ERR(my_dev->disk->queue)) {
        ret = PTR_ERR(my_dev->disk->queue);
        goto fail_queue;
    }
    my_dev->disk->queue->queuedata = my_dev;

    // 设置队列参数
    blk_queue_logical_block_size(my_dev->disk->queue, SECTOR_SIZE);
    blk_queue_physical_block_size(my_dev->disk->queue, SECTOR_SIZE);
    blk_queue_max_hw_sectors(my_dev->disk->queue, 256);
    set_capacity(my_dev->disk, NUM_SECTORS);

    // 注册磁盘,对用户空间可见
    ret = add_disk(my_dev->disk);
    if (ret)
        goto fail_add_disk;

    pr_info("my_blkdev: RAM磁盘已注册,大小 %d MB\n",
            MY_DISK_SIZE / 1024 / 1024);
    return 0;

fail_add_disk:
    blk_cleanup_queue(my_dev->disk->queue);
fail_queue:
    blk_mq_free_tag_set(&my_dev->tag_set);
fail_tag_set:
    put_disk(my_dev->disk);
fail_disk:
    vfree(my_dev->data);
fail_data:
    kfree(my_dev);
    return ret;
}

static void __exit my_blkdev_exit(void)
{
    // 删除磁盘
    del_gendisk(my_dev->disk);

    // 清理请求队列
    blk_cleanup_queue(my_dev->disk->queue);

    // 释放标签。    blk_mq_free_tag_set(&my_dev->tag_set);

    // 释放gendisk引用
    put_disk(my_dev->disk);

    // 释放模拟磁盘内存
    vfree(my_dev->data);

    kfree(my_dev);

    pr_info("my_blkdev: RAM磁盘已卸载\n");
}

module_init(my_blkdev_init);
module_exit(my_blkdev_exit);

MODULE_AUTHOR("linuxros");
MODULE_DESCRIPTION("Linux 6.8 RAM磁盘块设备驱动);
MODULE_LICENSE("GPL");

关键点说明

blk_mq_ops是blk-mq框架的核心,6.8中必须实现.queue_rq。
- queue_rq:处理I/O请求的回调,这是数据搬运的主战场
- init_hctx:初始化硬件队列上下文,简单驱动可以空实现

queue_rq里的处理流程。
1. blk_mq_start_request(rq)标记请求开始,必须在处理前调用
2. blk_rq_pos(rq)获取起始扇区,blk_rq_bytes(rq)获取总字节数
3. rq_for_each_segment遍历请求中的每个bio。4. kmap_local_page/kunmap_local_page访问页内数据。.8推荐替代kmap_atomic。5. req_op(rq)判断读还是写
6. blk_mq_end_request结束请求,通知块层处理完成

6.8 API变化:kmap_atomic/kunmap_atomic已被kmap_local_page/kunmap_local_page取代,新API不禁止抢占,更安全。blk_alloc_disk替代了旧的alloc_disk,配合动态主设备号分配更方便。

五、bio操作API

遍历bio。

// 遍历bio中的每个段
struct bio_vec bvec;
struct bvec_iter iter;

bio_for_each_segment(bvec, bio, iter) {
    void *buf = kmap_local_page(bvec.bv_page);
    // 访问 buf + bvec.bv_offset,长bvec.bv_len
    kunmap_local(buf);
}

bio_for_each_segment是遍历bio段的标准宏,每次迭代得到一个bio_vec,包含页指针、偏移和长度。注意bv_offset——数据不一定从页的起始位置开始,尤其在页内对齐场景下。

访问段内数据

// 6.8推荐方式
void *buf = kmap_local_page(bvec.bv_page);
memcpy(dst, buf + bvec.bv_offset, bvec.bv_len);
kunmap_local(buf);

// 旧方式(6.8仍可用但不推荐)
void *buf = kmap_atomic(bvec.bv_page);
memcpy(dst, buf + bvec.bv_offset, bvec.bv_len);
kunmap_atomic(buf);

kmap_local_page比kmap_atomic更灵活:不禁止抢占,可以睡眠(虽然块设备驱动中一般不会),嵌套调用更安全。唯一限制是不能跨CPU迁移——但在块设备驱动的queue_rq回调里这不是问题。

完成bio请求

// 直接完成bio(不经过blk-mqbio_endio(bio);

// 通过blk-mq完成request
blk_mq_end_request(rq, BLK_STS_OK);    // 成功
blk_mq_end_request(rq, BLK_STS_IOERR); // IO错误
blk_mq_end_request(rq, BLK_STS_NOSPC); // 空间不足

blk-mq驱动中一般用blk_mq_end_request而不是bio_endio,因为request可能合并了多个bio,blk_mq_end_request会一次性完成所有bio。

来自 linuxros.cn · linuxROS

bio数据拷贝辅助函数

// 从bio拷贝数据到迭代器(读操作集size_t bio_copy_to_iter(struct bio *bio, struct iov_iter *iter);

// 从迭代器拷贝数据到bio(写操作集size_t bio_copy_from_iter(struct bio *bio, struct iov_iter *iter);

这两个函数在需要把bio数据和用户空间缓冲区之间拷贝时使用,内核内部处理页映射和偏移计算。不过块设备驱动中更常见的做法是直接在queue_rq里用memcpy操作,像上面示例那样。

六、请求队列与blk-mq

传统request_queue vs blk-mq

维度 传统request_queue blk-mq
队列模型 单队列,全局锁 多硬件队列,每队列独立锁
请求映射 请求→单硬件队列 请求→软件队列→硬件队列
标签管理 tag_set管理请求标签
并行 受全局锁限制 硬件队列数×队列深度
6.8状态 已废弃 唯一推荐方式

传统单队列模型在高并发场景下全局锁是瓶颈,NVMe SSD动辄几十万IOPS,单队列根本扛不住。blk-mq把队列拆成多个硬件队列,每个队列独立锁,并行度大幅提升。6.8内核中所有块设备都必须走blk-mq路径。

blk_mq_tag_set结构建

// linux/blk-mq.h(简化)
struct blk_mq_tag_set {
    const struct blk_mq_ops *ops;     // 操作回调试    unsigned int nr_hw_queues;        // 硬件队列。    unsigned int queue_depth;         // 每个队列深度
    int numa_node;                    // NUMA节点
    unsigned int cmd_size;            // 驱动私有数据大小
    unsigned int flags;               // 标志。    void *driver_data;                // 驱动私有数据
    // ... 内部字段
};

关键字段说明。
- nr_hw_queues:硬件队列数,RAM磁盘这种虚拟设备1就行,NVMe驱动通常设为CPU核心。- queue_depth:每个硬件队列能同时处理的请求数,默认128,高性能设备可以设到1024
- cmd_size:每个请求附带的驱动私有数据大小,不需要就。
- flags:BLK_MQ_F_SHOULD_MERGE允许请求合并,BLK_MQ_F_BLOCKING允许queue_rq里睡。

blk_mq_ops回调试

// linux/blk-mq.h(简化)
struct blk_mq_ops {
    blk_status_t (*queue_rq)(struct blk_mq_hw_ctx *hctx,
                             const struct blk_mq_queue_data *bd);
    int (*init_hctx)(struct blk_mq_hw_ctx *hctx, void *data,
                     unsigned int index);
    void (*exit_hctx)(struct blk_mq_hw_ctx *hctx, unsigned int index);
    int (*init_request)(struct blk_mq_tag_set *set, struct request *rq,
                        unsigned int hctx_idx, unsigned int numa_node);
    void (*cleanup_rq)(struct request *rq);
    // ... 更多可选回调};

必须实现的是queue_rq,其余按需实现。
| 回调 | 用途 | 是否必须 |
|:-----|:-----|:---------|
| queue_rq | 处理I/O请求 | 必须 |
| init_hctx | 初始化硬件队列上下文 | 推荐 |
| exit_hctx | 清理硬件队列上下文 | 可 |
| init_request | 初始化请求私有数据 | 可 |
| cleanup_rq | 清理请求 | 可 |

多硬件队列并。

flowchart TB A["用户提交bio"] --> B["块层合并/排序"] B --> C["软件队列<br/>per-CPU"] C --> D["硬件队列0<br/>blk_mq_hw_ctx"] C --> E["硬件队列1<br/>blk_mq_hw_ctx"] C --> F["硬件队列N<br/>blk_mq_hw_ctx"] D --> G["queue_rq回调"] E --> H["queue_rq回调"] F --> I["queue_rq回调"] style A fill:#E8F5E9 style B fill:#E3F2FD style C fill:#FFF8E1 style G fill:#E3F2FD style H fill:#E3F2FD style I fill:#E3F2FD

blk-mq的并行模型:每个CPU有独立的软件队列(无锁),软件队列通过映射关系把请求分发到硬件队列。硬件队列之间完全独立,各自有自旋锁,互不干扰。NVMe驱动把硬件队列和CPU一一绑定,实现真正的无锁并行。

七、调试技。

lsblk/blkid/fdisk查看块设。

# 列出所有块设备的树形视图
lsblk

# 查看设备UUID和文件系统类blkid /dev/myram

# 查看分区
fdisk -l /dev/myram

# 查看块设备详细信息cat /proc/partitions

lsblk最常用,一眼看清设备层次和分区关系。blkid能看到文件系统类型和UUID,挂载时要用。fdisk -l看分区表,新设备如果没分区会显示"Doesn't contain a valid partition table"。

/sys/block/查看队列参数

# 查看块设备队列参数ls /sys/block/myram/queue/

# 查看调度器
cat /sys/block/myram/queue/scheduler

# 查看队列深度
cat /sys/block/myram/queue/nr_requests

# 查看最大扇区数
cat /sys/block/myram/queue/max_hw_sectors_kb

# 查看逻辑块大小
cat /sys/block/myram/queue/logical_block_size

# 查看物理块大小
cat /sys/block/myram/queue/physical_block_size

sysfs里的队列参数是blk-mq驱动的窗口,能看到调度器、队列深度、块大小等配置。如果性能不正常,先看这里的参数是否合理。

blktrace:块层跟。

# 安装blktrace
# Ubuntu/Debian: apt install blktrace
# CentOS/RHEL:   yum install blktrace

# 跟踪块设备的所有I/O事件
blktrace -d /dev/myram -o - | blkparse -i -

# 只跟。。timeout 5 blktrace -d /dev/myram -o - | blkparse -i -

# 输出到文件后分析
blktrace -d /dev/myram -o myram_trace
blkparse -i myram_trace -o myram_parsed.txt

blktrace能看到I/O从提交到完成的每个阶段:

事件 含义
Q 请求排队
G 获取请求
I 插入调度队列
D 发给驱动
C 驱动完成
M 请求合并

从Q到C的时间差就是一次I/O的端到端延迟。如果D到C之间间隔长,说明驱动处理慢;如果Q到D间隔长,说明调度器在攒请求。

fio:块设备性能测试

# 安装fio
# Ubuntu/Debian: apt install fio
# CentOS/RHEL:   yum install fio

# 顺序读测试
fio --name=seqread --filename=/dev/myram \
    --rw=read --bs=4k --size=1M --numjobs=1 --time_based --runtime=10

# 随机写测试
fio --name=randwrite --filename=/dev/myram \
    --rw=randwrite --bs=4k --size=1M --numjobs=4 --time_based --runtime=10 \
    --iodepth=32

# 混合读写测试
fio --name=mixed --filename=/dev/myram \
    --rw=randrw --rwmixread=70 --bs=4k --size=1M \
    --numjobs=2 --time_based --runtime=10 --iodepth=16

fio参数说明。
| 参数 | 含义 |
|:-----|:-----|
| --rw=read/write/randread/randwrite/randrw | 读写模式 |
| --bs=4k | 块大 |
| --iodepth=32 | I/O深度,模拟并发请求数 |
| --numjobs=4 | 并发线程 |
| --runtime=10 | 测试时长(秒 |

测试RAM磁盘时注意:数据全在内存里,IOPS会非常高,不要拿这个数据跟真实SSD比。fio测真实硬件才有参考价值

八、常见问题

Q1:add_disk后看不到设备。

lsblk和/dev/下都找不到设备,最常见的原因:

  1. 主设备号冲突:用major=0让内核动态分配,避免和已有设备冲。2. *fops没设:gendisk->fops必须指向有效的block_device_operations,哪怕只实现open/release
  2. *add_disk返回值没检。.8中add_disk会返回错误码,老版本返回void,升级代码时容易忽略
  3. 容量:set_capacity没调用或参数错误,内核不会注册容量的磁盘。

Q2:bio数据处理越界。

读写操作返回BLK_STS_IOERR或数据错乱,排查方向。
1. sector偏移计算:sector << SECTOR_SHIFT才是字节偏移,别直接用sector当偏。2. bv_offset没加:kmap_local_page后要加bvec.bv_offset,不是从页起始位置读
3. 大小没对*:块设备操作必须512字节对齐,blk_queue_logical_block_size设对
4.
越界检*:offset + remaining > MY_DISK_SIZE必须检查,否则内核可能panic

Q3:blk-mq性能不如预期。

队列参数没调对,几个调整方向。
1. nr_hw_queues:虚拟设备设1就行,真实NVMe设为CPU核心。2. queue_depth:默认128可能不够,高性能SSD可以。56。12
3. BLK_MQ_F_SHOULD_MERGE:没加这个flag请求不会合并,小IO性能。4. *调度:cat /sys/block/xxx/queue/scheduler看当前调度器,NVMe用none,SATA用mq-deadline

九、总结

块设备驱动开发速查看

操作 API 注意事项
分配gendisk blk_alloc_disk(NULL, NUMA_NO_NODE) 6.8推荐,替代alloc_disk
设置操作集 disk->fops = &my_blk_fops 必须实现open/release
初始化blk-mq blk_mq_alloc_tag_set(&tag_set) 先设置ops/depth/hw_queues
创建队列 blk_mq_init_queue(&tag_set) 返回后设置queuedata
设置块大 blk_queue_logical_block_size(q, 512) 必须和硬件匹
设置容量 set_capacity(disk, num_sectors) 单位是扇区,不是字节
注册磁盘 add_disk(disk) 之后设备对用户空间可
删除磁盘 del_gendisk(disk) 卸载时调试
清理队列 blk_cleanup_queue(q) del_gendisk之后调用
释放标签 blk_mq_free_tag_set(&tag_set) cleanup_queue之后调用
释放gendisk put_disk(disk) 最后调试
遍历请求 rq_for_each_segment(bvec, rq, iter) 配合kmap_local_page
完成请求 blk_mq_end_request(rq, BLK_STS_OK) 成功OK,失败IOERR
获取扇区 blk_rq_pos(rq) 返回sector_t
获取字节 blk_rq_bytes(rq) 返回unsigned int
判断操作类型 req_op(rq) == REQ_OP_READ READ/WRITE/FLUSH

本文首发于linuxros.cn,转载请注明出处。

版权声明

作者linuxROS
协议本作品采用 CC BY-NC-SA 4.0 许可协议:署名-非商业性使用-相同方式共享
关注欢迎关注微信公众号 linuxROS,获取更多机器人 / 嵌入式 / Linux 干货
返回首页