Linux内核分析之设备驱动-02

41.1 块设备注册与 gendisk

块驱动的注册是"声明式"的:填一张 blk_mq_tag_set(多少硬件队列、多深的池、什么回调),core 据此建全部队列与标签池;再 blk_mq_alloc_disk 取 gendisk、add_disk 上线。本节解剖这张声明表。


41.1.1 tag_set:驱动的能力声明

// include/linux/blk-mq.h:534-566(节选)
struct blk_mq_tag_set {
    const struct blk_mq_ops *ops;       /* 35.3.2 节钩子表 */
    struct blk_mq_queue_map map[HCTX_MAX_TYPES];    /* 读/写/poll 分组映射 */
    unsigned int        nr_maps;
    unsigned int        nr_hw_queues;   /* 硬件队列数 */
    unsigned int        queue_depth;    /* 池深 (= 设备环深) */
    unsigned int        reserved_tags;  /* 给特殊路径留的标签 */
    unsigned int        cmd_size;   /* 驱动每请求私有数据大小 */
    int         numa_node;
    unsigned int        timeout;    /* 超时 (ms) */
    unsigned int        flags;      /* BLK_MQ_F_* */
    void            *driver_data;
    struct blk_mq_tags  **tags;     /* core 填充: 每硬件队列一池 */
    ...
};

cmd_size 是零分配设计的关键:驱动声明"每个 request 我要多大私有空间",core 在标签池预分配时连私有区一起布好,blk_mq_rq_to_pdu(rq) 取回——运行期零 kmalloc(35.3.1 节延迟确定性的驱动侧配套)。map[] 把 CPU 分组映射到硬件队列(NVMe 多 SQ 一一对应,map_queues 钩子或默认 NUMA 分组)。

41.1.2 gendisk 装配与上线

最小块驱动的注册序列:

 static struct blk_mq_ops my_mq_ops = {
    .queue_rq = my_queue_rq,
    .map_queues = my_map_queues,
    .init_hctx = my_init_hctx,  /* 每硬件队列私有态 */
 };

 static struct blk_mq_tag_set my_tag_set = {
    .ops = &my_mq_ops,
    .nr_hw_queues = num_possible_cpus(),    /* 或设备 SQ 数 */
    .queue_depth = 128,
    .cmd_size = sizeof(struct my_cmd),
    .flags = BLK_MQ_F_SHOULD_MERGE,
 };

 my_tag_set.ops = ...; blk_mq_alloc_tag_set(&my_tag_set);
 gendisk = blk_mq_alloc_disk(&my_tag_set, private);
 gendisk->major = my_major; /* register_blkdev 拿到的 */
 gendisk->first_minor = 0;
 gendisk->fops = &my_bdev_ops;  /* ioctl/open (blkdev.h:1648) */
 gendisk->queue->queuedata = private;
 strscpy(gendisk->disk_name, "myblk", ...);
 set_capacity(gendisk, sectors);
 add_disk(gendisk);     /* 上线: 分区扫描/uevent/sysfs */

add_disk 之后盘立即可用(分区枚举 part_tbl、uevent 39.1.3、sysfs 投影 39.3)——所以"所有状态就绪后再 add_disk"与 40.1.2 节 cdev_add 同纪律。block_device_operations(blkdev.h:1648)只管控制面:open/release(引用计数)、ioctl(分区/几何/私有命令)、check_events(介质变化)——数据面完全不在,那是 queue_rq 的领地。

41.1.3 三个参照驱动的定位

驱动 类型 学习点
loop 回环:bio→文件读写 最简 queue_rq:sync 直转发页缓存
zram 内存盘:bio→压缩页 写路径的块级变换 + sysfs 调参
NVMe pci 真硬件:多 SQ/CQ 环 tag_set 映射、DMA(43 章)、中断完成(42 章)

小结

块驱动注册是声明式的:blk_mq_tag_set 声明队列拓扑/池深/每请求私有区,core 建全部基础设施;blk_mq_alloc_disk + add_disk 取 gendisk 上线(分区/uevent/sysfs 随之而来),block_device_operations 只承载控制面。三步之外的切分/合并/调度/统计全归 core。下一节看最后一块拼图——queue_rq 如何把 request 翻译成设备命令。

41.2 块设备请求处理

queue_rq 是块驱动的数据面本体:从 request 读出"哪些扇区、哪些页、什么操作",翻译成设备命令(DMA 描述符/环表项/转发调用),并把成败报回 core。本节解剖它的骨架与完成路径。


41.2.1 queue_rq 的骨架

// include/linux/blk-mq.h:576 区(回调签名)
typedef blk_status_t (*queue_rq_fn)(struct blk_mq_hw_ctx *,
        const struct blk_mq_queue_data *);
/* bd->rq: 被派发的 request; bd->last: 队列中最后一个 */

// 遍历 request 的页向量 (include/linux/blk-mq.h:1088-1092)
#define __rq_for_each_bio(_bio, rq) ...
#define rq_for_each_segment(bvl, _rq, _iter)    ...
queue_rq 的标准骨架 (以 NVMe 为心智模型):

 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_cmd *cmd = blk_mq_rq_to_pdu(rq);

    blk_mq_start_request(rq);       /* [1] core 记"已派发" */
                        (超时计时开始, :1368)

    switch (req_op(rq)) {           /* [2] 操作类型分发 */
    case REQ_OP_READ / WRITE:
        /* 逐段填 DMA 描述符 (43.2 节 dma_map) */
        rq_for_each_segment(bvl, rq, iter) {
            addr = dma_map_...(bvl->bv_page, ...);
            填环表项;
        }
        break;
    case REQ_OP_FLUSH / DISCARD: ...    /* 特殊操作 */
    default: return BLK_STS_IOERR;
    }

    [3] 提交到硬件: 写提交环 tail + 踢门铃 (MMIO)
    return BLK_STS_OK;
 }

三条纪律:blk_mq_start_request 必须调(否则超时机制立刻误杀);返回值是状态不是完成——BLK_STS_OK 只表示"已接受进硬件",BLK_STS_DEV_RESOURCE 表示"设备忙请稍后重派"(core 挂起 hctx 等 35.3.3 节的恢复触发);硬件命令完成后必须以 blk_mq_complete_request(rq) 走完成回调(通常在中断上下文,42 章软中断化处理)——start 与 complete 的配对是请求生命周期的闭合。

41.2.2 完成路径

// block/blk-mq.c:1176
void blk_mq_end_request(struct request *rq, blk_status_t error)
// :1368
void blk_mq_start_request(struct request *rq)
完成的回流:

 设备中断 (42 章硬中断):
   读完成环 (CQ) → 拿 tag → rq = tags->rqs[tag]
   blk_mq_complete_request(rq)
     → core 视情况软中断化 (iris: 42.2) → end_io
 blk_mq_end_request(rq, status)        (:1176)
   ├─ error 记入 → bio->bi_status (36.2.2 节)
   ├─ blk_mq_free_request → 标签归还 → 等标签的
   │   提交者被唤醒 → 新请求流入
   └─ bi_end_io 回调 → 页缓存/回写/调用者收货

 批量完成: blk_mq_end_request_batch (:1197)
   io_comp_batch 聚合多请求一次回调
   — 高 IOPS 设备减少 per-req 开销 (NVMe 在用)

标签归还的连锁反应是流量控制的实相:池满时提交者在 get_request 睡眠(或 io_uring 返回 -EAGAIN),完成即释放标签唤醒补给——queue_depth(41.1.1 节)因此就是设备的在途 IO 配额。

41.2.3 三个参照的 queue_rq 对照

驱动 queue_rq 的"翻译" 完成的来源
loop 把 bio 段直接 filemap 写到后端文件(同步转发) queue_rq 内直接 complete(无硬件)
zram 解压/压缩 zram 页(内存池读写) 同步完成
NVMe 填 submission queue 命令 + DMA 映射 + 门铃 CQ 中断 → complete_request

loop/zram 展示了"queue_rq 内同步完成"的合法形态(无硬件、无中断);NVMe 展示完整异步形态——同一抽象覆盖从纯软件到真硬件的全谱。错误传播链(rq→bio→bi_status→fsync/页缓存)在 36.2.2 节闭合。


小结

queue_rq 是"request→设备命令"的翻译器:start_request 开表、按操作类型分发、rq_for_each_segment 逐段填 DMA 描述符、门铃提交、返回 BLK_STS_OK 表示"已接受";完成侧经中断 blk_mq_complete_request → blk_mq_end_request 归还标签并唤醒下游——标签池的容量就是在途 IO 的自然限流。loop/zram/NVMe 三个参照证明这套抽象从纯软件到真硬件全谱适用。第七部分至此完成设备主体;下一章进入中断与延迟机制——queue_rq 背后的"事情何时做完"的全部基础设施。