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 背后的"事情何时做完"的全部基础设施。