基于 Linux Kernel 源码深度分析 路径:
block/|include/linux/blk_types.h|include/linux/blkdev.h|include/linux/blk-mq.h
- 块设备层概述与整体架构
- 核心数据结构:bio 与 bio_vec
- request 结构体详解
- bio 生命周期:从提交到完成
- bio 合并机制
- blk-mq 多队列框架
- blk-mq 请求分发路径
- 标签(tag)分配机制
- plug/unplug 批量提交
- I/O 调度器框架
- mq-deadline 调度器
- BFQ 调度器
- Kyber 与 none 调度器
- NVMe 多队列驱动
- NVMe-oF(NVMe over Fabrics)
- 块设备层架构:gendisk 与 block_device
- 分区与 bd_mapping 页缓存集成
- Direct I/O 与 Buffered I/O
- I/O 统计与 blkcg 限速
- 错误处理与重试机制
- block/ 目录主要文件一览
- 性能调优指南
Linux 存储 I/O 栈是一个层次化的软件架构,块设备层处于中间枢纽位置,将上层文件系统的 I/O 请求转化为对底层硬件驱动的命令序列。
+-----------------------------------------------------------+
| 用户空间 (User Space) |
| read() / write() / io_uring / direct I/O |
+-----------------------------------------------------------+
|
+-----------------------------------------------------------+
| VFS (Virtual File System) |
| struct file_operations -> address_space_operations |
+-----------------------------------------------------------+
|
+----------------------------+------------------------------+
| Page Cache / Writeback | Direct I/O (绕过 Page Cache)|
+----------------------------+------------------------------+
|
+-----------------------------------------------------------+
| 块设备层 (Block Layer) <-- 本文重点 |
| |
| submit_bio() --> blk-mq --> I/O Scheduler |
| bio / request / request_queue / elevator |
| |
+-----------------------------------------------------------+
|
+-----------------------------------------------------------+
| 设备驱动层 (Device Driver) |
| NVMe / SCSI / virtio-blk / loop |
+-----------------------------------------------------------+
|
+-----------------------------------------------------------+
| 物理硬件 (Hardware) |
| SSD / HDD / NVMe SSD / Network Block Device |
+-----------------------------------------------------------+
- I/O 抽象:为上层提供统一的
bio接口,屏蔽底层硬件差异。 - 请求排队:通过
request_queue管理待处理的 I/O 请求。 - 合并优化:将相邻扇区的多个 I/O 合并为一个大请求,减少调度开销。
- I/O 调度:通过 elevator(调度器)对请求重排序,提升吞吐量或降低延迟。
- 多队列支持:blk-mq 框架支持现代 NVMe 等多队列设备,消除单队列锁竞争。
- 流量控制:blkcg(块设备 cgroup)实现资源隔离与带宽限速。
include/linux/blk_types.h:30 定义了基本常量:
#define SECTOR_SHIFT 9
#define SECTOR_SIZE (1 << SECTOR_SHIFT) /* 512 字节 */所有块 I/O 以扇区(512 字节)为基本单位。sector_t 类型表示扇区偏移量或大小。
bio 是块 I/O 层的基本传输单元,定义在 include/linux/blk_types.h:210:
struct bio {
struct bio *bi_next; /* 请求队列链表 */
struct block_device *bi_bdev; /* 目标块设备 */
blk_opf_t bi_opf; /* 操作类型 + 标志位 */
unsigned short bi_flags; /* BIO_* 标志 */
unsigned short bi_ioprio; /* I/O 优先级 */
enum rw_hint bi_write_hint; /* 写入提示 */
u8 bi_write_stream; /* 写流标识 */
blk_status_t bi_status; /* 完成状态 */
u8 bi_bvec_gap_bit; /* 段间隙位,DMA 优化用 */
atomic_t __bi_remaining; /* 引用计数 */
/* scatter-gather 列表 */
struct bio_vec *bi_io_vec; /* bvec 数组指针 */
struct bvec_iter bi_iter; /* 当前迭代位置 */
union {
blk_qc_t bi_cookie; /* 轮询 bio 使用 */
unsigned int __bi_nr_segments; /* zone 写插件 */
};
bio_end_io_t *bi_end_io; /* 完成回调 */
void *bi_private; /* 提交者私有数据 */
#ifdef CONFIG_BLK_CGROUP
struct blkcg_gq *bi_blkg; /* blkcg 组关联 */
u64 issue_time_ns; /* 发起时间戳 */
#endif
unsigned short bi_vcnt; /* bio_vec 数量 */
unsigned short bi_max_vecs; /* 已分配的 bvec 数 */
atomic_t __bi_cnt; /* pin 引用计数 */
struct bio_set *bi_pool; /* 内存池 */
};bi_opf 是一个 32 位字段(blk_opf_t),低 8 位编码操作类型(req_op),高 24 位编码标志位(req_flag_bits)。
操作类型(include/linux/blk_types.h:347):
| 操作码 | 值 | 含义 |
|---|---|---|
REQ_OP_READ |
0 | 从设备读取扇区 |
REQ_OP_WRITE |
1 | 向设备写入扇区 |
REQ_OP_FLUSH |
2 | 刷新易失性写缓存 |
REQ_OP_DISCARD |
3 | 丢弃扇区(TRIM) |
REQ_OP_SECURE_ERASE |
5 | 安全擦除 |
REQ_OP_ZONE_APPEND |
7 | 追加写到当前区写指针 |
REQ_OP_WRITE_ZEROES |
9 | 写零填充 |
最低位标识数据传输方向:置 1 表示写入设备,清 0 表示从设备读取。
常见标志位(include/linux/blk_types.h:382):
REQ_SYNC /* 同步请求 */
REQ_META /* 元数据 I/O */
REQ_FUA /* Forced Unit Access(强制落盘) */
REQ_PREFLUSH /* 请求前先刷缓存 */
REQ_RAHEAD /* 预读,可随时失败 */
REQ_NOWAIT /* 不阻塞,设备忙则返回 */
REQ_POLLED /* 调用方轮询完成 */
REQ_ATOMIC /* 原子写操作 *//* include/linux/blk_types.h:301 */
enum {
BIO_PAGE_PINNED, /* 页面已锁定,完成时释放 */
BIO_CLONED, /* 克隆的 bio,不拥有数据 */
BIO_QUIET, /* 抑制错误消息 */
BIO_CHAIN, /* 链式 bio */
BIO_REFFED, /* bi_cnt 已增加 */
BIO_BPS_THROTTLED, /* 已经过带宽限速 */
BIO_TRACE_COMPLETION, /* bio_endio() 时追踪完成 */
BIO_CGROUP_ACCT, /* 已计入 cgroup */
BIO_QOS_THROTTLED, /* 已过 rq_qos 限速路径 */
BIO_REMAPPED, /* 已重映射(DM/MD 使用)*/
BIO_ZONE_WRITE_PLUGGING, /* zone 写插件处理 */
};bio_vec 定义在 include/linux/bvec.h:28,描述一段连续的物理内存地址范围:
struct bio_vec {
struct page *bv_page; /* 起始页帧 */
unsigned int bv_len; /* 字节长度 */
unsigned int bv_offset; /* 相对 bv_page 起始的偏移 */
};一个 bio 包含一个 bio_vec 数组(bi_io_vec),代表 scatter-gather DMA 列表。每个 bio_vec 描述了一段连续的内存区域,多个 bio_vec 组成完整的 I/O 缓冲区。
bio
+------------------+
| bi_io_vec -----> | bio_vec[0]: page=A, offset=0, len=4096
| bi_vcnt = 3 | bio_vec[1]: page=B, offset=512, len=2048
| bi_iter.bi_idx=0 | bio_vec[2]: page=C, offset=0, len=1024
+------------------+
bvec_iter 追踪 bio 当前的迭代状态(include/linux/bvec.h:77):
struct bvec_iter {
sector_t bi_sector; /* 当前设备扇区偏移 */
unsigned int bi_size; /* 剩余字节数 */
unsigned int bi_idx; /* 当前 bio_vec 索引 */
unsigned int bi_bvec_done; /* 当前 bvec 中已处理字节数 */
};遍历 bio 的标准宏:
/* 遍历 bio 的每个段 */
bio_for_each_segment(bvl, bio, iter) { ... }
/* 遍历 request 的每个段 */
rq_for_each_segment(bvl, rq, iter) { ... }bio_set 是 bio 的内存池管理结构。bio_alloc() 会从 bio_set 的 mempool 中分配 bio,确保在内存压力下仍能分配(通过预留池):
struct bio *bio_alloc(struct block_device *bdev,
unsigned short nr_vecs,
blk_opf_t opf,
gfp_t gfp_mask);内联 bvec 优化:当 nr_vecs 较小时,bio_vec 数组直接追加在 bio 结构体后面(include/linux/blk_types.h:293):
static inline struct bio_vec *bio_inline_vecs(struct bio *bio)
{
return (struct bio_vec *)(bio + 1);
}这种内联布局减少了一次额外的内存分配,提升了 cache 局部性。
request 是调度器操作的基本单元,定义在 include/linux/blk-mq.h:105:
struct request {
struct request_queue *q; /* 所属请求队列 */
struct blk_mq_ctx *mq_ctx; /* 关联的软件队列 */
struct blk_mq_hw_ctx *mq_hctx; /* 关联的硬件队列 */
blk_opf_t cmd_flags; /* 操作类型 + 公共标志 */
req_flags_t rq_flags; /* 请求内部标志 */
int tag; /* 硬件队列标签(dispatch 后分配)*/
int internal_tag; /* 调度器标签(入队时分配)*/
unsigned int timeout; /* 请求超时时间 */
unsigned int __data_len; /* 总数据长度 */
sector_t __sector; /* 扇区游标 */
struct bio *bio; /* bio 链表头 */
struct bio *biotail; /* bio 链表尾 */
union {
struct list_head queuelist; /* 在调度器队列中 */
struct request *rq_next; /* plug 列表链 */
};
struct block_device *part; /* 目标分区 */
u64 start_time_ns; /* 请求分配时间 */
u64 io_start_time_ns; /* 提交到设备的时间 */
unsigned short nr_phys_segments; /* DMA 物理段数量 */
unsigned char phys_gap_bit; /* 段间隙位 */
enum mq_rq_state state; /* MQ_RQ_IDLE/IN_FLIGHT/COMPLETE */
atomic_t ref; /* 引用计数 */
unsigned long deadline; /* 截止时间(调度器用)*/
union {
struct hlist_node hash; /* 合并哈希表节点 */
struct llist_node ipi_list; /* softirq 完成队列 */
};
union {
struct rb_node rb_node; /* 调度器排序树节点 */
struct bio_vec special_vec; /* RQF_SPECIAL_PAYLOAD 时使用 */
};
struct {
struct io_cq *icq; /* I/O context 队列 */
void *priv[2]; /* 调度器私有数据(最多 2 个指针)*/
} elv;
struct {
unsigned int seq;
rq_end_io_fn *saved_end_io;
} flush; /* flush 操作序列 */
u64 fifo_time; /* FIFO 到期时间 */
rq_end_io_fn *end_io; /* 完成回调 */
void *end_io_data;
};/* include/linux/blk-mq.h:34 */
enum rqf_flags {
__RQF_STARTED, /* 驱动已开始处理 */
__RQF_FLUSH_SEQ, /* flush 序列请求 */
__RQF_MIXED_MERGE, /* 不同类型请求合并 */
__RQF_DONTPREP, /* 不调用 prep */
__RQF_SCHED_TAGS, /* 使用调度器标签 */
__RQF_USE_SCHED, /* 使用 I/O 调度器 */
__RQF_FAILED, /* 驱动内部错误 */
__RQF_IO_STAT, /* 记录 I/O 统计 */
__RQF_PM, /* 运行时电源管理请求 */
__RQF_HASHED, /* 在调度器合并哈希中 */
__RQF_STATS, /* 追踪 I/O 完成时间 */
__RQF_ZONE_WRITE_PLUGGING, /* 需通知 zone 写插件 */
__RQF_TIMED_OUT, /* 已超时 */
};分配时: MQ_RQ_IDLE
|
| blk_mq_start_request()
v
MQ_RQ_IN_FLIGHT
|
| blk_mq_end_request() / blk_mq_set_request_complete()
v
MQ_RQ_COMPLETE
|
| 释放回标签池
v
(free)
一个 request 可包含多个合并的 bio,通过 bio->bi_next 链接。bio 链上的所有 bio 必须满足:
- 属于同一个块设备
- 操作类型相同
- 扇区地址连续(BACK_MERGE 或 FRONT_MERGE)
request
+---------+
| bio ---+---> bio[0] --bi_next--> bio[1] --bi_next--> bio[2] --> NULL
| biotail -+--------------------------------------------^
+---------+
应用/文件系统
|
| bio_alloc() + bio_add_page()
v
submit_bio(bio) [block/bio.c]
|
| generic_make_request() [已废弃] 或
v
submit_bio_noacct(bio) [block/blk-core.c]
|
| 检查递归提交(bi_opf & REQ_NOWAIT)
v
blk_mq_submit_bio(bio) [block/blk-mq.c]
|
+---> 尝试 bio 合并(plug merge / sched merge)
|
+---> 分配 request(blk_mq_get_new_requests)
|
+---> 加入 plug 列表 或 直接提交
v
blk_mq_run_hw_queue(hctx, async)
|
v
blk_mq_dispatch_rq_list()
|
v
ops->queue_rq(hctx, &bd) [驱动层]
block/blk-mq.c:3141 是 blk-mq 路径上提交 bio 的入口函数:
void blk_mq_submit_bio(struct bio *bio)
{
struct request_queue *q = bdev_get_queue(bio->bi_bdev);
struct blk_plug *plug = current->plug;
// ...
// 1. 尝试合并
// 2. 分配 request
// 3. 加入 plug 或直接分发
}硬件中断 / 轮询
|
v
驱动调用 blk_mq_end_request(rq, error)
|
v
blk_update_request(rq, error, nr_bytes)
|
v
bio_endio(bio) [遍历 bio 链]
|
v
bio->bi_end_io(bio) [调用完成回调]
|
v
文件系统 / 应用层完成处理
块层支持三种 bio 合并方式:
ELEVATOR_BACK_MERGE (后向合并)
existing_rq: [sector 100 ... 200]
new_bio: [sector 200 ... 250]
结果: [sector 100 .................. 250]
ELEVATOR_FRONT_MERGE (前向合并)
new_bio: [sector 50 ... 100]
existing_rq: [sector 100 ... 200]
结果: [sector 50 ........................ 200]
ELEVATOR_DISCARD_MERGE (discard 合并)
合并多个 discard 范围
block/blk-merge.c:1085 是 plug 路径上的合并尝试函数:
bool blk_attempt_plug_merge(struct request_queue *q, struct bio *bio,
unsigned int nr_segs)
{
struct blk_plug *plug = current->plug;
struct request *rq;
if (!plug || rq_list_empty(&plug->mq_list))
return false;
/* 优先检查最近一个请求(尾部) */
rq = plug->mq_list.tail;
if (rq->q == q)
return blk_attempt_bio_merge(q, rq, bio, nr_segs, false) ==
BIO_MERGE_OK;
else if (!plug->multiple_queues)
return false;
/* 多队列情况:遍历 plug 列表 */
rq_list_for_each(&plug->mq_list, rq) {
if (rq->q != q)
continue;
if (blk_attempt_bio_merge(q, rq, bio, nr_segs, false) ==
BIO_MERGE_OK)
return true;
break;
}
return false;
}关键设计:plug 合并在无锁路径下进行(per-task plug 列表),完全避免了队列锁竞争。
blk_mq_attempt_bio_merge() [block/blk-mq.c:3034]
|
+---> blk_attempt_plug_merge() [plug 列表合并,无锁]
|
+---> blk_mq_sched_bio_merge() [调度器合并,使用哈希]
|
v
elv_merge() [遍历 elevator 合并逻辑]
|
+---> elv_rqhash_find() [通过扇区号快速查找]
+---> ops->request_merge()
合并必须满足的硬件限制(struct queue_limits,include/linux/blkdev.h:370):
struct queue_limits {
unsigned int max_sectors; /* 最大扇区数 */
unsigned int max_segment_size; /* 单段最大字节数 */
unsigned short max_segments; /* 最大段数(BLK_MAX_SEGMENTS=128)*/
unsigned long seg_boundary_mask; /* 段边界限制 */
unsigned int dma_alignment; /* DMA 对齐要求 */
// ...
};REQ_NOMERGE_FLAGS = REQ_NOMERGE | REQ_PREFLUSH | REQ_FUA:带有这些标志的请求不能被合并。
blk-mq(Multi-Queue Block I/O Queueing Mechanism)是 Linux 3.13 引入的多队列块 I/O 框架,专为现代 NVMe SSD、高速网络存储等支持多队列的设备设计。
CPU 0 CPU 1 CPU 2 CPU 3
| | | |
+-------+--------+--------+--------+
| 软件队列(Software Queues) |
| blk_mq_ctx (per-CPU) |
+---+--------+--------+-------------+
| | |
+------+ +-----+ +----+
| HW Q0| | HW Q1| | HW Q2| <- 硬件队列 blk_mq_hw_ctx
+------+ +------+ +------+
| | |
+---v----------v----------v---------+
| NVMe 控制器 |
| SQ0 SQ1 SQ2 (提交队列) |
| CQ0 CQ1 CQ2 (完成队列) |
+-----------------------------------+
软件队列(Software Queue)定义在 block/blk-mq.h:19,每个 CPU 对应一个:
struct blk_mq_ctx {
struct {
spinlock_t lock;
struct list_head rq_lists[HCTX_MAX_TYPES]; /* 每种请求类型一个列表 */
} ____cacheline_aligned_in_smp;
unsigned int cpu; /* 所属 CPU */
unsigned short index_hw[HCTX_MAX_TYPES]; /* 硬件队列索引 */
struct blk_mq_hw_ctx *hctxs[HCTX_MAX_TYPES]; /* 关联的硬件队列 */
struct request_queue *queue; /* 所属请求队列 */
struct blk_mq_ctxs *ctxs;
struct kobject kobj;
} ____cacheline_aligned_in_smp;获取当前 CPU 的软件队列(block/blk-mq.h:155):
static inline struct blk_mq_ctx *blk_mq_get_ctx(struct request_queue *q)
{
return __blk_mq_get_ctx(q, raw_smp_processor_id());
}硬件队列(Hardware Queue)定义在 include/linux/blk-mq.h:322,对应设备的一个 MSI-X 中断/队列对:
struct blk_mq_hw_ctx {
struct {
spinlock_t lock;
struct list_head dispatch; /* 待派发但暂时无法提交的请求 */
unsigned long state; /* BLK_MQ_S_* 状态位 */
} ____cacheline_aligned_in_smp;
struct delayed_work run_work; /* 延迟运行工作项 */
cpumask_var_t cpumask; /* 可运行此队列的 CPU 掩码 */
int next_cpu; /* 轮询 CPU 选择 */
int next_cpu_batch;
unsigned long flags; /* BLK_MQ_F_* 标志 */
void *sched_data; /* I/O 调度器私有数据 */
struct request_queue *queue;
struct blk_flush_queue *fq; /* flush 请求队列 */
void *driver_data; /* 驱动私有数据 */
struct sbitmap ctx_map; /* 软件队列的位图(有请求时置位)*/
struct blk_mq_ctx *dispatch_from; /* 无调度器时的分发来源 */
unsigned int dispatch_busy; /* EWMA 繁忙度估计 */
unsigned short type; /* HCTX_TYPE_DEFAULT/READ/POLL */
unsigned short nr_ctx; /* 关联的软件队列数 */
struct blk_mq_ctx **ctxs; /* 软件队列数组 */
struct blk_mq_tags *tags; /* 硬件标签集 */
struct blk_mq_tags *sched_tags; /* 调度器标签集 */
unsigned int numa_node;
unsigned int queue_num; /* 队列编号 */
atomic_t nr_active; /* 活跃请求数(共享标签集时用)*/
};/* include/linux/blk-mq.h:488 */
enum hctx_type {
HCTX_TYPE_DEFAULT, /* 所有 I/O(默认)*/
HCTX_TYPE_READ, /* 只读 I/O */
HCTX_TYPE_POLL, /* 轮询 I/O(不依赖中断)*/
HCTX_MAX_TYPES,
};操作类型到队列类型的映射(block/blk-mq.h:90):
static inline enum hctx_type blk_mq_get_hctx_type(blk_opf_t opf)
{
if (opf & REQ_POLLED)
return HCTX_TYPE_POLL;
else if ((opf & REQ_OP_MASK) == REQ_OP_READ)
return HCTX_TYPE_READ;
return HCTX_TYPE_DEFAULT;
}include/linux/blk-mq.h:576 定义了 blk-mq 驱动必须实现的回调接口:
struct blk_mq_ops {
/* 核心:提交单个请求到硬件 */
blk_status_t (*queue_rq)(struct blk_mq_hw_ctx *,
const struct blk_mq_queue_data *);
/* 批量提交后的收尾(bd->last=false 时需要) */
void (*commit_rqs)(struct blk_mq_hw_ctx *);
/* 批量提交请求列表(可选,优化路径) */
void (*queue_rqs)(struct rq_list *rqlist);
/* 获取/释放设备资源预算 */
int (*get_budget)(struct request_queue *);
void (*put_budget)(struct request_queue *, int);
/* 超时处理 */
enum blk_eh_timer_return (*timeout)(struct request *);
/* I/O 轮询(POLL 队列使用)*/
int (*poll)(struct blk_mq_hw_ctx *, struct io_comp_batch *);
/* 标记请求完成 */
void (*complete)(struct request *);
/* 初始化/清理硬件队列 */
int (*init_hctx)(struct blk_mq_hw_ctx *, void *, unsigned int);
void (*exit_hctx)(struct blk_mq_hw_ctx *, unsigned int);
/* 初始化/清理请求(分配驱动私有空间)*/
int (*init_request)(struct blk_mq_tag_set *, struct request *,
unsigned int, unsigned int);
void (*exit_request)(struct blk_mq_tag_set *, struct request *,
unsigned int);
/* CPU 到队列的自定义映射 */
void (*map_queues)(struct blk_mq_tag_set *set);
};include/linux/blk-mq.h:534 定义全局标签集,在注册块设备时初始化,多个 request_queue 可共享(如 SCSI 多路径):
struct blk_mq_tag_set {
const struct blk_mq_ops *ops;
struct blk_mq_queue_map map[HCTX_MAX_TYPES]; /* CPU->HW Queue 映射 */
unsigned int nr_maps;
unsigned int nr_hw_queues; /* 硬件队列数 */
unsigned int queue_depth; /* 每个队列的标签数 */
unsigned int reserved_tags; /* 预留标签数 */
unsigned int cmd_size; /* 每个 request 附加数据大小 */
int numa_node;
unsigned int timeout;
unsigned int flags; /* BLK_MQ_F_* */
void *driver_data;
struct blk_mq_tags **tags; /* 每个 hctx 的标签结构 */
struct blk_mq_tags *shared_tags; /* 共享标签(可选)*/
};submit_bio(bio)
|
v
blk_mq_submit_bio() [block/blk-mq.c]
|
+--- 尝试 bio 合并
| blk_mq_attempt_bio_merge()
| |-- blk_attempt_plug_merge() (plug 合并)
| `-- blk_mq_sched_bio_merge() (调度器合并)
|
+--- 分配 request
| blk_mq_get_new_requests()
| `-- __blk_mq_alloc_requests()
| `-- blk_mq_get_tag() (从 sbitmap 分配标签)
|
+--- current->plug 存在?
| YES: 加入 plug->mq_list(延迟提交)
| NO: 直接进入队列
|
v
blk_mq_run_hw_queue(hctx, async)
|
v
__blk_mq_run_hw_queue()
|
v
blk_mq_sched_dispatch_requests()
|
+--- 有调度器?
| YES: elevator->ops.dispatch_request(hctx)
| NO: blk_mq_dequeue_from_ctx()
|
v
blk_mq_dispatch_rq_list()
|
v
blk_mq_dispatch_rq_list() 循环调用:
+---> blk_mq_prep_dispatch_rq()
+---> ops->queue_rq(hctx, &bd)
| 返回 BLK_STS_OK: 请求已提交
| 返回 BLK_STS_RESOURCE: 设备暂时无资源,重排队
| 返回 BLK_STS_DEV_RESOURCE: 设备繁忙
`---> bd.last=true 时调用 ops->commit_rqs()
blk_mq_run_hw_queue() 被以下情况触发:
- unplug 时:
blk_finish_plug()→blk_mq_flush_plug_list() - 请求完成时:空闲标签释放后检查等待队列
- 调度延迟:
hctx->run_workdelayed_work 到期 - requeue 时:
blk_mq_kick_requeue_list()
CPU ID --> blk_mq_ctx (per-CPU) --> blk_mq_hw_ctx
^
ctx->index_hw[type] ---------+
ctx->hctxs[type] ----------+
映射通过 blk_mq_queue_map 在驱动初始化时建立(include/linux/blk-mq.h:475):
struct blk_mq_queue_map {
unsigned int *mq_map; /* CPU ID -> HW 队列索引 */
unsigned int nr_queues;
unsigned int queue_offset;
};标签是 request 的唯一标识符,定义在 include/linux/blk-mq.h:774:
struct blk_mq_tags {
unsigned int nr_tags; /* 总标签数 */
unsigned int nr_reserved_tags; /* 预留标签数 */
unsigned int active_queues; /* 活跃队列数 */
struct sbitmap_queue bitmap_tags; /* 普通标签位图 */
struct sbitmap_queue breserved_tags; /* 预留标签位图 */
struct request **rqs; /* tag -> request 指针数组 */
struct request **static_rqs; /* 预分配的 request 数组 */
struct list_head page_list; /* 内存页列表 */
spinlock_t lock;
};标签分配基于 sbitmap_queue,这是一个支持等待的可扩展稀疏位图。其设计目标是在高并发场景下最小化原子操作的 cache line 竞争。
sbitmap_queue
+------------------+
| sb (sbitmap) | --> 位图数组,按 WORD 分散到不同 cache line
| ws (wait states) | --> 等待队列数组
| round_robin | --> 轮询分配位置
+------------------+
位图布局(假设 nr_tags=1024):
word[0]: bits 0..63
word[1]: bits 64..127
...
word[15]: bits 960..1023
blk_mq_get_tag(data) [block/blk-mq-tag.c]
|
+--- 尝试从 bitmap_tags 分配
| sbitmap_queue_get(&tags->bitmap_tags, &tag)
|
+--- 失败?
| BLK_MQ_REQ_RESERVED: 尝试 breserved_tags
| BLK_MQ_REQ_NOWAIT: 立即返回 BLK_MQ_NO_TAG
| 否则: 进入等待队列睡眠
|
v
成功: tags->rqs[tag] = rq (建立 tag -> request 映射)
blk-mq 维护两套标签:
-
hctx->tags(硬件标签):在queue_rq之前由 blk-mq 核心分配。标签数等于queue_depth,直接对应设备硬件资源(NVMe 的 SQ entry)。 -
hctx->sched_tags(调度器标签):当存在 I/O 调度器时,请求入队时分配此标签(RQF_SCHED_TAGS标志)。调度器标签数 =nr_requests(通常 = 2 × queue_depth),允许更多请求在调度器内缓冲。
有调度器时:
bio --> sched_tag 分配 --> 调度器队列 --> 派发 --> hw_tag 分配 --> 提交硬件
无调度器时:
bio --> hw_tag 分配 --> 直接提交硬件
/* include/linux/blk-mq.h:794 */
static inline struct request *blk_mq_tag_to_rq(struct blk_mq_tags *tags,
unsigned int tag)
{
if (tag < tags->nr_tags) {
prefetch(tags->rqs[tag]); /* 提前预取,减少 cache miss */
return tags->rqs[tag];
}
return NULL;
}
/* 全局唯一 tag(包含 hctx 编号)*/
u32 blk_mq_unique_tag(struct request *rq);
/* 高 16 位:hctx 编号;低 16 位:本地 tag */include/linux/blkdev.h:1172 定义了 plug 机制:
struct blk_plug {
struct rq_list mq_list; /* blk-mq 请求列表(已分配的 request)*/
struct rq_list cached_rqs; /* 缓存的 request(批量预分配)*/
u64 cur_ktime; /* 当前 ktime 快照(避免重复调用)*/
unsigned short nr_ios; /* 预期 I/O 数(批量分配用)*/
unsigned short rq_count; /* plug 中的 request 数量 */
bool multiple_queues; /* 是否有多个队列的请求 */
bool has_elevator; /* 是否有请求使用调度器 */
struct list_head cb_list; /* unplug 回调(md 等使用)*/
};/* 典型使用模式(文件系统中)*/
struct blk_plug plug;
blk_start_plug(&plug); /* 设置 current->plug = &plug */
/* 提交多个 bio,它们会积累在 plug->mq_list */
submit_bio(bio1);
submit_bio(bio2);
submit_bio(bio3);
blk_finish_plug(&plug); /* 清除 current->plug,批量提交 */
|
v
blk_mq_flush_plug_list(&plug, false)
|
v
对每个 hctx 调用 blk_mq_sched_insert_requests() 或 blk_mq_run_hw_queue()blk_start_plug_nr_ios(plug, nr_ios) 可以提示 plug 将要提交的 I/O 数量,使标签分配可以批量进行:
/* block/blk-mq.c:3065 */
if (plug) {
data.nr_tags = plug->nr_ios; /* 一次分配多个标签 */
plug->nr_ios = 1;
data.cached_rqs = &plug->cached_rqs;
}批量分配通过 blk_mq_get_tags() 实现,使用单次 sbitmap 操作分配多个连续标签,显著减少原子操作开销。
1. 显式调用 blk_finish_plug()
2. 进程调度:schedule() -> blk_flush_plug(plug, true)
3. 系统调用返回:检查 TIF_NOTIFY_RESUME
4. blk_plug_invalidate_ts() 使时间戳失效
I/O 调度器框架(elevator)提供了可插拔的 I/O 请求排序和合并策略。定义在 block/elevator.h。
request_queue
|
v
elevator_queue (运行时实例)
+-> type (elevator_type: 调度器类型)
+-> elevator_data (调度器私有数据)
+-> et (elevator_tags: 调度器标签)
+-> hash[64] (合并哈希表)
block/elevator.h:97 定义调度器类型:
struct elevator_type {
struct kmem_cache *icq_cache; /* I/O context 缓存 */
struct elevator_mq_ops ops; /* 操作接口集 */
size_t icq_size; /* io_cq 大小 */
size_t icq_align; /* io_cq 对齐 */
const struct elv_fs_entry *elevator_attrs; /* sysfs 属性 */
const char *elevator_name; /* 调度器名称 */
const char *elevator_alias; /* 别名 */
struct module *elevator_owner;
char icq_cache_name[ELV_NAME_MAX + 6];
struct list_head list; /* 全局注册链表 */
};block/elevator.h:57 定义 blk-mq 调度器的操作集:
struct elevator_mq_ops {
/* 初始化/销毁 */
int (*init_sched)(struct request_queue *, struct elevator_queue *);
void (*exit_sched)(struct elevator_queue *);
int (*init_hctx)(struct blk_mq_hw_ctx *, unsigned int);
void (*exit_hctx)(struct blk_mq_hw_ctx *, unsigned int);
/* 合并相关 */
bool (*allow_merge)(struct request_queue *, struct request *, struct bio *);
bool (*bio_merge)(struct request_queue *, struct bio *, unsigned int);
int (*request_merge)(struct request_queue *q, struct request **, struct bio *);
void (*request_merged)(struct request_queue *, struct request *, enum elv_merge);
void (*requests_merged)(struct request_queue *, struct request *, struct request *);
/* 深度限制(限制并发度)*/
void (*limit_depth)(blk_opf_t, struct blk_mq_alloc_data *);
/* 请求生命周期 */
void (*prepare_request)(struct request *);
void (*finish_request)(struct request *);
/* 入队与分发 */
void (*insert_requests)(struct blk_mq_hw_ctx *hctx,
struct list_head *list, blk_insert_t flags);
struct request *(*dispatch_request)(struct blk_mq_hw_ctx *);
bool (*has_work)(struct blk_mq_hw_ctx *);
/* 完成与重排队 */
void (*completed_request)(struct request *, u64);
void (*requeue_request)(struct request *);
/* 邻居请求查找(merge 用)*/
struct request *(*former_request)(struct request_queue *, struct request *);
struct request *(*next_request)(struct request_queue *, struct request *);
/* I/O Context 初始化/清理 */
void (*init_icq)(struct io_cq *);
void (*exit_icq)(struct io_cq *);
};block/elevator.h:146:
struct elevator_queue {
struct elevator_type *type; /* 调度器类型 */
struct elevator_tags *et; /* 调度器标签 */
void *elevator_data; /* 调度器私有数据 */
struct kobject kobj; /* sysfs 节点 */
struct mutex sysfs_lock;
unsigned long flags;
DECLARE_HASHTABLE(hash, ELV_HASH_BITS); /* 合并哈希(64桶)*/
};合并哈希表通过 elv_rqhash_find(q, sector) 可以在 O(1) 时间内找到以目标扇区结尾的请求,用于 BACK_MERGE 合并。
/* 注册调度器 */
int elv_register(struct elevator_type *e);
/* 通过 sysfs 切换调度器 */
/* echo mq-deadline > /sys/block/sda/queue/scheduler */
ssize_t elv_iosched_store(struct gendisk *disk, const char *page, size_t count);
/* 切换需要先冻结队列 */
/* blk_mq_freeze_queue() -> 切换 -> blk_mq_unfreeze_queue() */mq-deadline 是传统 deadline 调度器的 blk-mq 适配版(block/mq-deadline.c),核心目标:
- 防止饥饿:每个请求都有截止时间,超时后强制派发
- 读写分离:读请求优先(延迟敏感),写请求可以稍延迟(吞吐优先)
- 扇区排序:在截止时间允许范围内,按扇区顺序派发,减少磁头移动
block/mq-deadline.c:41 定义核心数据结构:
/* 优先级分类 */
enum dd_prio {
DD_RT_PRIO = 0, /* 实时优先级 (IOPRIO_CLASS_RT) */
DD_BE_PRIO = 1, /* 最佳效率 (IOPRIO_CLASS_BE) */
DD_IDLE_PRIO = 2, /* 空闲 (IOPRIO_CLASS_IDLE) */
};
/* 每优先级、每方向的数据 */
struct dd_per_prio {
struct rb_root sort_list[DD_DIR_COUNT]; /* 按扇区排序的红黑树 */
struct list_head fifo_list[DD_DIR_COUNT]; /* 按到期时间排序的 FIFO */
sector_t latest_pos[DD_DIR_COUNT]; /* 最近派发位置 */
struct io_stats_per_prio stats;
};
/* 调度器主数据 */
struct deadline_data {
struct list_head dispatch; /* 立即派发队列 */
struct dd_per_prio per_prio[DD_PRIO_COUNT]; /* 3个优先级 */
enum dd_data_dir last_dir; /* 最近派发方向 */
unsigned int batching; /* 批处理计数 */
unsigned int starved; /* 写饥饿计数 */
/* 可调参数 */
int fifo_expire[DD_DIR_COUNT]; /* 默认: 读=HZ/2, 写=5*HZ */
int fifo_batch; /* 批量处理数: 默认 16 */
int writes_starved; /* 最大读饥饿写次数: 默认 2 */
int front_merges; /* 是否允许前向合并 */
int prio_aging_expire; /* 低优先级老化时间: 10*HZ */
spinlock_t lock;
};截止时间参数(block/mq-deadline.c:30):
static const int read_expire = HZ / 2; /* 读请求最大等待: 500ms */
static const int write_expire = 5 * HZ; /* 写请求最大等待: 5s */
static const int prio_aging_expire = 10 * HZ; /* 低优先级老化: 10s */
static const int writes_starved = 2; /* 读可饥饿写的最大次数 */
static const int fifo_batch = 16; /* 批量处理大小 */每个优先级维护两套排序结构:
sort_list[READ] (rb_tree, 按扇区排序)
+-- sector 100
+-- sector 200
+-- sector 300
fifo_list[READ] (链表, 按到期时间排序)
+-- [100, deadline=T+100ms]
+-- [200, deadline=T+200ms]
+-- [300, deadline=T+50ms] <-- 先到期的先派发
sort_list[WRITE] (rb_tree)
fifo_list[WRITE] (链表)
dd_dispatch_request(hctx)
|
+-- 检查 dispatch 列表(未派发的请求)
|
+-- 优先级循环(RT > BE > IDLE)
| |
| +-- 检查 FIFO 是否有到期请求?
| | 是: 强制派发(deadline 强制)
| |
| +-- 批量计数未超 fifo_batch?
| | 是: 从 sort_list 顺序派发
| |
| +-- 切换方向(读/写平衡)
| writes_starved 超限: 强制切到写
|
v
派发请求(elv_dispatch_sort 或直接出队)
每次派发读请求: starved++
每次派发写请求: starved = 0
当 starved >= writes_starved(默认2)时:
强制派发写请求,防止写饥饿
┌─────────────────────────────────┐
│ deadline_data │
│ │
│ per_prio[RT] │
│ sort_list[R]: rb_tree─────────┼──> R[s=100] R[s=200] ...
│ fifo_list[R]: FIFO────────────┼──> R[dl=10ms] R[dl=50ms] ...
│ sort_list[W]: rb_tree─────────┼──> W[s=50] W[s=150] ...
│ fifo_list[W]: FIFO────────────┼──> W[dl=1s] W[dl=3s] ...
│ │
│ per_prio[BE] (类似结构) │
│ per_prio[IDLE](类似结构) │
│ │
│ dispatch: list_head─────────────┼──> 立即派发的请求
└─────────────────────────────────┘
│
v
blk_mq_dispatch_rq_list()
│
v
ops->queue_rq()
BFQ(Budget Fair Queueing)是一个比例共享 I/O 调度器,定义在 block/bfq-iosched.c。
核心思想(block/bfq-iosched.c:22):
BFQ 向进程分配预算(以扇区数度量),而不是时间片。设备不是在给定时间片内服务于进程,而是直到进程耗尽分配的预算。这一变化使 BFQ 能够按预期分配设备吞吐量,不受吞吐量波动或设备内部队列的影响。
BFQ 使用 B-WF2Q+(Budgeted Worst-case Fair Weighted Fair Queueing)内部调度器:
- 每个进程/队列有权重
w_i - 虚拟时间(virtual time)
V(t)以全局速率推进 - 每个队列的虚拟截止时间
vd = V + budget / w - 服务树(service tree)按虚拟截止时间排序
服务树(augmented red-black tree):
[vd=100]
/ \
[vd=50] [vd=150]
/ \
[vd=30] [vd=70]
每次派发 vd 最小的队列(等效于 EDF)
BFQ 识别两类时间敏感应用并赋予特权(block/bfq-iosched.c:40):
-
交互式应用:周期性进行短时 I/O 的进程(如桌面程序)。检测方式:队列只在有限时间内持续非空,然后变空。
-
软实时应用:需要固定速率 I/O 的进程(如视频播放器)。通过
bfq_bfqq_softrt_next_start函数检测。
对这两类应用,BFQ 临时提升其权重(weight-raising),确保低延迟。
每个进程的每种 I/O 类型对应一个 bfq_queue,包含:
bfq_queue
+-- sort_list (rb_tree: 按扇区排序)
+-- fifo (链表: FIFO 顺序)
+-- weight (I/O 权重)
+-- budget (当前预算:扇区数)
+-- vdisktime (虚拟磁盘时间)
+-- service_tree -> 全局服务树
+-- bfqd -> 调度器全局数据
BFQ 通过 bfq_group 结构支持完整的层级调度:
cgroup hierarchy:
/sys/fs/cgroup/io/
├── io.weight = 100 (根组默认权重)
├── app_group/
│ ├── io.weight = 200 (高优先级组)
│ └── process A, B
└── bg_group/
├── io.weight = 50 (低优先级组)
└── process C, D
BFQ 使用 H-WF2Q+ 在 cgroup 层级上递归应用公平调度。
Kyber 是为快速存储设备(NVMe SSD)设计的轻量级调度器,基于令牌桶原理:
核心设计:
每种 I/O 类型(READ / SYNC_WRITE / OTHER)维护独立的令牌桶
令牌桶参数:
- target_lat: 延迟目标(READ: 2ms, WRITE: 10ms)
- tokens: 当前可用令牌数
- tokens_per_s: 令牌生成速率(动态调整)
派发逻辑:
FOR each hctx:
IF tokens[type] > 0:
派发该类型请求
tokens[type]--
ELSE:
限流(排队等待令牌)
动态调整:
测量实际延迟 vs target_lat
延迟 < 目标: 增加令牌(放宽限流)
延迟 > 目标: 减少令牌(加强限流)
适用场景:NVMe 等低延迟设备。在这类设备上,deadline 的读写分离反而增加不必要的开销,Kyber 的轻量级令牌机制更合适。
令牌桶 ASCII 图:
READ 令牌桶 [■■■■■□□□□□] 50/100 tokens <- 延迟目标 2ms
WRITE 令牌桶 [■■■■■■■□□□] 70/100 tokens <- 延迟目标 10ms
OTHER 令牌桶 [■■□□□□□□□□] 20/100 tokens
每次派发消耗1个令牌
每个测量周期根据延迟反馈补充令牌
none(也叫 "noop")调度器不做任何重排序:
insert_requests: 直接追加到 dispatch 队列
dispatch_request: FIFO 顺序取出
bio_merge: 只做基本的 FIFO 合并
适用场景:
- NVMe 等设备本身有硬件队列和内部调度,软件调度无益
- 虚拟化环境(hypervisor 层已做调度)
- 基准测试(排除调度器影响)
在 blk-mq 中,当只有一个硬件队列或硬件队列共享时,BLK_MQ_F_NO_SCHED_BY_DEFAULT 标志会默认选择 none:
/* include/linux/blk-mq.h:709 */
BLK_MQ_F_NO_SCHED_BY_DEFAULT = 1 << 6,
/* 单队列或共享队列时,默认不使用调度器 */| 特性 | mq-deadline | BFQ | Kyber | none |
|---|---|---|---|---|
| 适用设备 | HDD/SATA SSD | 所有 | NVMe SSD | NVMe/虚拟化 |
| 防饥饿 | 截止时间 | 权重+预算 | 令牌桶 | 无 |
| 延迟保证 | 硬截止 | 软实时 | 延迟目标 | 无 |
| 吞吐优化 | 扇区排序 | 顺序提升 | 低 | 设备自身 |
| cgroup 支持 | 有限 | 完整层级 | 基本 | 无 |
| CPU 开销 | 低 | 中 | 极低 | 极低 |
NVMe 协议原生支持多队列(最多 65535 个队列),每个队列对独立工作:提交队列(Submission Queue,SQ)+ 完成队列(Completion Queue,CQ)。这与 blk-mq 框架完全对齐:
blk-mq HW Queue <--> NVMe I/O Queue (SQ/CQ pair)
drivers/nvme/host/pci.c:294 定义 NVMe PCI 设备:
struct nvme_dev {
struct nvme_queue *queues; /* 所有队列数组(0=admin)*/
struct blk_mq_tag_set tagset; /* I/O 标签集 */
struct blk_mq_tag_set admin_tagset; /* Admin 标签集 */
u32 __iomem *dbs; /* 门铃寄存器基址 */
struct device *dev;
unsigned online_queues; /* 在线队列数 */
unsigned max_qid; /* 最大队列 ID */
unsigned io_queues[HCTX_MAX_TYPES]; /* 每类型队列数 */
unsigned int num_vecs; /* MSI-X 向量数 */
u32 q_depth; /* 队列深度 */
int io_sqes; /* SQ Entry Size(log2)*/
u32 db_stride; /* 门铃寄存器步长 */
void __iomem *bar; /* BAR 映射 */
bool cmb_use_sqes; /* 使用 CMB(控制器内存缓冲)*/
struct nvme_ctrl ctrl; /* 通用控制器数据 */
/* shadow doorbell 支持(减少 MMIO 写操作)*/
__le32 *dbbuf_dbs;
__le32 *dbbuf_eis;
unsigned int nr_write_queues; /* 写专用队列数 */
unsigned int nr_poll_queues; /* 轮询队列数 */
};drivers/nvme/host/pci.c:365:
struct nvme_queue {
struct nvme_dev *dev;
spinlock_t sq_lock; /* 提交队列自旋锁 */
void *sq_cmds; /* SQ 命令数组(DMA 内存)*/
spinlock_t cq_poll_lock; /* 轮询队列的 CQ 锁 */
struct nvme_completion *cqes; /* CQ 完成条目数组(DMA 内存)*/
dma_addr_t sq_dma_addr; /* SQ 的 DMA 地址 */
dma_addr_t cq_dma_addr; /* CQ 的 DMA 地址 */
u32 __iomem *q_db; /* 门铃寄存器指针 */
u32 q_depth; /* 队列深度 */
u16 cq_vector; /* MSI-X 中断向量号 */
u16 sq_tail; /* SQ 尾指针(Host 维护)*/
u16 last_sq_tail; /* 上次写入门铃的 sq_tail */
u16 cq_head; /* CQ 头指针(Host 维护)*/
u16 qid; /* 队列 ID(0=admin)*/
u8 cq_phase; /* CQ phase bit */
u8 sqes; /* SQ Entry Size(log2)*/
unsigned long flags; /* NVMEQ_ENABLED 等标志 */
__le32 *dbbuf_sq_db; /* shadow SQ doorbell */
__le32 *dbbuf_cq_db; /* shadow CQ doorbell */
};Queue ID = 0: Admin Queue (管理队列)
- 专用于控制命令:Identify、Create I/O Queue、Set Features 等
- 深度通常较小(64 或 128)
- 使用独立的 blk_mq_tag_set (admin_tagset)
Queue ID = 1..N: I/O Queue (数据队列)
- 用于读写数据命令
- 深度由 io_queue_depth 参数控制(默认 1024,最大 4095)
- 映射到 blk-mq 的 blk_mq_hw_ctx
Host NVMe 控制器
| |
blk_mq_submit_bio | |
| | |
v | |
nvme_queue_rq() | |
| | |
v | 写 SQE(64字节命令)到 sq_cmds[sq_tail]
nvme_submit_cmd() | |
| | sq_tail = (sq_tail+1) % q_depth|
| | |
v | 写门铃寄存器(MMIO写) |
writel(sq_tail, |----> q_db (SQ Tail Doorbell) -->|
q_db) | |
| | 控制器处理命令
| | 执行 DMA 传输
| |
| (中断或轮询) |
|<-- MSI-X 中断 <--- 写 CQE ------+
| |
nvme_irq() | |
nvme_process_cq() | |
| | 读取 cqes[cq_head](phase bit)|
| | cq_head = (cq_head+1) % q_depth|
v | 写 CQ Head 门铃 |
blk_mq_end_request()|----> cq_db (CQ Head Doorbell) ->|
| |
phase bit 机制:CQ 不需要额外的写操作清零,而是通过 phase bit(0/1 交替)判断 CQE 是否有效:
/* phase=1 表示当前轮次的完成条目有效 */
if ((cqe->status & 1) == nvme_queue->cq_phase)
/* 这是一个新的完成条目 */在高 IOPS 场景下,频繁的 MMIO 写(写门铃寄存器)开销显著。NVMe 1.3 引入了 Controller Memory Buffer(CMB)技术,允许将门铃寄存器映射到控制器内存中,通过 DMA 写而非 MMIO 写更新门铃:
/* drivers/nvme/host/pci.c */
#define SQ_SIZE(q) ((q)->q_depth << (q)->sqes)
#define CQ_SIZE(q) ((q)->q_depth * sizeof(struct nvme_completion))
/* shadow doorbell: 先更新内存中的 shadow 值,
* 只在真正需要时才写 MMIO 门铃 */
if (*nvmeq->dbbuf_sq_db != sq_tail)
writel(sq_tail, nvmeq->q_db);/sys/module/nvme/parameters/
io_queue_depth = 1024 # 每队列深度
write_queues = 0 # 0=读写共用同一队列集
poll_queues = 0 # 轮询队列数(需 BLK_FEAT_POLL)
blk-mq 队列分配(以 8 核系统 + NVMe 8队列为例):
HCTX_TYPE_DEFAULT: 8个HW队列(写/其他)
HCTX_TYPE_READ: 8个HW队列(读,若 write_queues>0)
HCTX_TYPE_POLL: 2个HW队列(轮询,若 poll_queues>0)
NVMe-oF 将 NVMe 协议扩展到网络 fabric(如 RDMA、TCP、FC),使远程 NVMe 设备如同本地 NVMe 一样使用。
Host 端:
blk-mq HW Queue
|
nvme_tcp / nvme_rdma / nvme_fc (传输层驱动)
|
网络 Fabric (TCP/RDMA/FC)
|
Target 端: nvmet_tcp / nvmet_rdma
|
nvmet (NVMe Target 核心)
|
后端存储(本地 NVMe / 文件)
| 传输 | 模块 | 延迟 | 适用场景 |
|---|---|---|---|
| NVMe/TCP | nvme-tcp | ~100μs+ | 通用以太网 |
| NVMe/RDMA | nvme-rdma | ~10μs+ | InfiniBand/RoCE |
| NVMe/FC | nvme-fc | ~10μs+ | 光纤通道 SAN |
| NVMe/Loop | nvme-loop | 极低 | 本地测试 |
- 连接管理:NVMe-oF 需要建立 fabric 连接(队列对),类似 TCP 连接
- 传输开销:数据通过网络传输,延迟远高于本地 PCIe
- 队列映射:每个 fabric 连接对应一个 I/O 队列
- Capsule:NVMe-oF 将命令和数据封装在 capsule 中传输
nvme_tcp_alloc_io_queues() <- 连接 target 的 I/O 队列
|
v
blk_mq_alloc_tag_set(&ctrl->tagset)
|
v
nvme_alloc_io_tag_set(ctrl, &ctrl->tagset, ops, nr_maps, cmd_size)
|
v
blk_mq_alloc_disk() / nvme_alloc_ns()
include/linux/blkdev.h:144 定义通用磁盘结构:
struct gendisk {
int major; /* 主设备号 */
int first_minor; /* 起始次设备号 */
int minors; /* 次设备号数量(分区数+1)*/
char disk_name[DISK_NAME_LEN]; /* 设备名(如 nvme0n1)*/
struct xarray part_tbl; /* 分区表(XArray 替代旧数组)*/
struct block_device *part0; /* 整盘的 block_device */
const struct block_device_operations *fops; /* 设备操作集 */
struct request_queue *queue; /* 请求队列 */
void *private_data;
struct bio_set bio_split; /* bio 分割用的内存池 */
int flags; /* GENHD_FL_* */
unsigned long state; /* GD_DEAD / GD_READ_ONLY 等 */
struct mutex open_mutex;
unsigned open_partitions;
/* zoned 设备支持 */
unsigned int nr_zones;
unsigned int zone_capacity;
// ...
int node_id; /* NUMA 节点 */
u64 diskseq; /* 磁盘序列号(每次 add/del 递增)*/
blk_mode_t open_mode;
};磁盘标志(include/linux/blkdev.h:87):
enum {
GENHD_FL_REMOVABLE = 1 << 0, /* 可移除媒体 */
GENHD_FL_HIDDEN = 1 << 1, /* 隐藏(多路径底层设备)*/
GENHD_FL_NO_PART = 1 << 2, /* 不扫描分区 */
};磁盘状态位(state 字段):
#define GD_NEED_PART_SCAN 0 /* 需要扫描分区 */
#define GD_READ_ONLY 1 /* 只读 */
#define GD_DEAD 2 /* 设备已失效 */
#define GD_NATIVE_CAPACITY 3 /* 使用原始容量 */
#define GD_ADDED 4 /* 已注册到系统 */
#define GD_SUPPRESS_PART_SCAN 5 /* 抑制分区扫描 */
#define GD_OWNS_QUEUE 6 /* disk 拥有 queue */include/linux/blk_types.h:41 定义块设备(整盘或分区):
struct block_device {
sector_t bd_start_sect; /* 分区起始扇区(整盘=0)*/
sector_t bd_nr_sectors; /* 扇区数量 */
struct gendisk *bd_disk; /* 所属 gendisk */
struct request_queue *bd_queue; /* 请求队列(= disk->queue)*/
struct disk_stats __percpu *bd_stats; /* per-CPU I/O 统计 */
unsigned long bd_stamp;
atomic_t __bd_flags; /* 分区号 + BD_* 标志 */
dev_t bd_dev; /* 设备号 (major:minor) */
struct address_space *bd_mapping; /* 页缓存 */
atomic_t bd_openers; /* 打开计数 */
spinlock_t bd_size_lock;
void *bd_claiming; /* 独占声明者 */
void *bd_holder; /* 持有者(文件系统超级块)*/
int bd_holders; /* 持有计数 */
atomic_t bd_fsfreeze_count; /* 冻结请求计数 */
struct mutex bd_fsfreeze_mutex;
struct partition_meta_info *bd_meta_info; /* GPT 分区元数据 */
struct device bd_device; /* 内嵌 device(用于 sysfs)*/
} __randomize_layout;bd_flags(include/linux/blk_types.h:48):
#define BD_PARTNO 255 /* 低8位:分区号 */
#define BD_READ_ONLY (1u<<8) /* 只读策略 */
#define BD_WRITE_HOLDER (1u<<9) /* 有写持有者 */
#define BD_HAS_SUBMIT_BIO (1u<<10) /* fops->submit_bio 存在 */
#define BD_RO_WARNED (1u<<11) /* 已警告只读 */gendisk (磁盘)
|
+-- part0 ---------> block_device (整盘, partno=0)
| bd_start_sect=0
| bd_nr_sectors=全部
|
+-- part_tbl[1] --> block_device (第1分区, partno=1)
| bd_start_sect=2048
| bd_nr_sectors=1024000
|
+-- part_tbl[2] --> block_device (第2分区, partno=2)
bd_start_sect=1026048
bd_nr_sectors=8192000
include/linux/blkdev.h:478 定义请求队列:
struct request_queue {
void *queuedata; /* 驱动私有数据 */
struct elevator_queue *elevator; /* I/O 调度器 */
const struct blk_mq_ops *mq_ops; /* blk-mq 操作集 */
struct blk_mq_ctx __percpu *queue_ctx; /* per-CPU 软件队列 */
unsigned long queue_flags;
unsigned int queue_depth; /* 总队列深度 */
unsigned int nr_hw_queues; /* 硬件队列数 */
struct blk_mq_hw_ctx * __rcu *queue_hw_ctx; /* 硬件队列数组 */
struct request *last_merge; /* 最近合并的请求(热路径优化)*/
spinlock_t queue_lock;
struct gendisk *disk;
struct queue_limits limits; /* 硬件限制 */
struct blk_mq_tags *sched_shared_tags; /* 共享调度器标签 */
struct blk_flush_queue *fq; /* flush 队列 */
struct rq_qos *rq_qos; /* QoS 链表 */
struct throtl_data *td; /* 节流数据(blk-throttle)*/
struct blk_mq_tag_set *tag_set; /* 标签集 */
spinlock_t requeue_lock;
struct list_head requeue_list; /* 重排队列表 */
struct delayed_work requeue_work; /* 重排队延迟工作项 */
};queue_flags 中重要的标志(include/linux/blkdev.h:653):
QUEUE_FLAG_DYING /* 队列正在销毁 */
QUEUE_FLAG_NOMERGES /* 禁用合并 */
QUEUE_FLAG_NOXMERGES /* 禁用扩展合并 */
QUEUE_FLAG_QUIESCED /* 队列已静止(不派发)*/
QUEUE_FLAG_SQ_SCHED /* 单队列风格派发 */旧内核使用 disk_part_tbl(数组+RCU),新内核使用 xarray part_tbl(include/linux/blkdev.h:158):
struct gendisk {
struct xarray part_tbl; /* key: partno, value: block_device* */
struct block_device *part0; /* 整盘(partno=0)*/
// ...
};访问分区:
/* 通过分区号访问 */
struct block_device *bdev = xa_load(&disk->part_tbl, partno);
/* 遍历所有分区 */
xa_for_each(&disk->part_tbl, idx, bdev) {
// 处理每个分区
}block_device.bd_mapping(include/linux/blk_types.h:58)指向该块设备的 address_space,用于实现块设备级别的页缓存。
block_device
|
+-- bd_mapping --> address_space (块设备页缓存)
|
+-- i_pages (XArray: 块设备的页缓存)
+-- a_ops (address_space_operations)
|
+-- readpage: block_read_full_folio
+-- writepage: block_write_full_folio
作用:
- 缓冲 I/O(Buffered I/O):文件系统通过
mapping实现数据缓存,减少实际磁盘访问。 - 元数据缓存:读取分区表、超级块等元数据时通过
bd_mapping缓存。 - 回写机制:脏页通过
writeback机制异步写回磁盘。
在旧版内核中,block_device 通过 bd_inode 关联到一个特殊的块设备 inode。新版内核将 address_space 直接嵌入或通过 bd_mapping 指针管理,简化了结构层次。
write(fd, buf, len)
|
v
vfs_write()
|
v
generic_file_write_iter() [文件系统通用写路径]
|
v
pagecache_write_begin() [获取页缓存页]
|
v
copy_from_user_enhanced() [从用户空间复制数据到页缓存]
|
v
pagecache_write_end() [标记页为脏]
|
v
balance_dirty_pages_ratelimited() [检查脏页是否超限]
|
(后台) writeback_inodes_sb_nr()
|
v
write_cache_pages()
|
v
submit_bio(bio) [最终提交到块设备层]
Direct I/O 绕过页缓存,直接将用户缓冲区 DMA 到设备:
write(fd, buf, len) with O_DIRECT 或 io_uring with IORING_OP_WRITE
|
v
generic_file_write_iter()
|
| kiocb->ki_flags & IOCB_DIRECT
v
iomap_dio_rw() / direct_IO() [文件系统 direct I/O 实现]
|
v
bio_iov_iter_get_pages() [将用户页面 pin 住(不复制数据)]
|
v
submit_bio(bio) [直接提交 bio,bi_io_vec 指向用户页]
|
v
设备 DMA 直接读写用户内存
O_DIRECT 的限制:
- 缓冲区地址必须满足设备对齐要求(通常 512 字节)
- 传输长度必须是逻辑块大小的整数倍
- 文件偏移也必须对齐
- 读写需要等待完成(或使用 io_uring 异步)
io_uring(Linux 5.1+)提供了真正的异步提交和完成机制,通过共享内存的 SQ/CQ 环形队列与内核通信:
用户空间 内核空间
| |
| io_uring_enter() |
| |
SQE ring ──提交──────────────> io_uring_submit()
| | |
| | v
| | blk-mq / 直接 I/O 路径
| | |
| | v
CQE ring <──完成────────────── io_uring_complete()
| |
| (轮询或 eventfd 通知) |
块设备的 I/O 统计通过 disk_stats 结构(per-CPU)维护:
/* include/linux/part_stat.h */
#define part_stat_lock() preempt_disable()
#define part_stat_unlock() preempt_enable()
/* 读取统计(any context)*/
#define part_stat_read(part, field) \
({ \
typeof_field(struct disk_stats, field) res = 0; \
unsigned int cpu; \
for_each_possible_cpu(cpu) \
res += per_cpu_ptr(part->bd_stats, cpu)->field; \
res; \
})
/* 修改统计(需在 preempt_disable 区间内)*/
#define __part_stat_add(part, field, addnd) \
(part_stat_get(part, field) += (addnd))iostat 中的字段来源:
| iostat 字段 | 内核统计字段 | 描述 |
|---|---|---|
| r/s | ios[STAT_READ] |
读操作数/秒 |
| w/s | ios[STAT_WRITE] |
写操作数/秒 |
| rkB/s | sectors[STAT_READ] |
读扇区数/秒 |
| wkB/s | sectors[STAT_WRITE] |
写扇区数/秒 |
| r_await | nsecs[STAT_READ] / ios |
平均读延迟 |
| w_await | nsecs[STAT_WRITE] / ios |
平均写延迟 |
| %util | in_flight[0] / 采样时间 |
设备利用率 |
blkcg 是块设备层的 cgroup 控制器,实现资源隔离与限速。
cgroup v2 接口:
/sys/fs/cgroup/<group>/
io.weight - I/O 权重(1-10000,默认100)
io.max - 带宽/IOPS 限制
io.stat - I/O 统计
io.pressure - I/O 压力
io.max 格式:
echo "8:0 rbps=10485760 wbps=20971520" > io.max
# 设置 /dev/sda (8:0) 的读带宽限制为 10MB/s,写为 20MB/s
echo "8:0 riops=1000 wiops=2000" > io.max
# 设置读 1000 IOPS,写 2000 IOPS
blk-throttle 是实现 io.max 的核心模块(block/blk-throttle.c):
bio 提交路径:
submit_bio()
|
v
rq_qos_throttle() [rq_qos 框架钩子]
|
v
tg_throttle_down() [blk-throttle 节流]
|
+-- 读取当前组的 bps/iops 限制
+-- 计算是否超限
+-- 超限:io_schedule() 等待令牌补充
+-- 未超限:更新计数,继续
令牌补充通过定时器(throtl_pending_timer_fn)周期性执行。
当使用 BFQ 调度器时,io.weight 直接映射到 BFQ 的队列权重:
io.weight=100 -> bfq_group.weight=100
io.weight=200 -> bfq_group.weight=200 (获得2倍 I/O 份额)
使用 mq-deadline 或 none 时,io.weight 通过 bfq 的 cgroup 权重处理层实现(如果配置了的话)。
rq_qos(Request Quality of Service)是 blk-mq 的 QoS 框架,允许链式挂载多个 QoS 策略:
request_queue.rq_qos (链表):
+-- blk-throttle (throttl_data) <- io.max 实现
+-- wbt (rq_wb) <- 写回节流
+-- iocost (ioc_data) <- 基于代价的 I/O 控制
每个 rq_qos 实现以下回调:
struct rq_qos_ops {
void (*throttle)(struct rq_qos *, struct bio *);
void (*track)(struct rq_qos *, struct request *, struct bio *);
void (*merge)(struct rq_qos *, struct request *, struct bio *);
void (*issue)(struct rq_qos *, struct request *);
void (*requeue)(struct rq_qos *, struct request *);
void (*done)(struct rq_qos *, struct request *);
void (*done_bio)(struct rq_qos *, struct bio *);
void (*cleanup)(struct rq_qos *, struct bio *);
void (*queue_depth_changed)(struct rq_qos *);
void (*exit)(struct rq_qos *);
};include/linux/blk_types.h:96 定义块层错误码:
typedef u8 __bitwise blk_status_t;
#define BLK_STS_OK 0 /* 成功 */
#define BLK_STS_NOTSUPP 1 /* 操作不支持 */
#define BLK_STS_TIMEOUT 2 /* 超时 */
#define BLK_STS_NOSPC 3 /* 无空间 */
#define BLK_STS_TRANSPORT 4 /* 传输层错误(路径相关)*/
#define BLK_STS_TARGET 5 /* 目标设备错误 */
#define BLK_STS_RESV_CONFLICT 6 /* 预留冲突 */
#define BLK_STS_MEDIUM 7 /* 介质错误(坏块)*/
#define BLK_STS_PROTECTION 8 /* 数据完整性保护失败 */
#define BLK_STS_RESOURCE 9 /* 内存等系统资源不足 */
#define BLK_STS_IOERR 10 /* 通用 I/O 错误 */
#define BLK_STS_DM_REQUEUE 11 /* DM 重排队 */
#define BLK_STS_AGAIN 12 /* 非阻塞请求会阻塞 */
#define BLK_STS_DEV_RESOURCE 13 /* 设备资源不足(有飞行中 I/O 时会释放)*/
#define BLK_STS_ZONE_OPEN_RESOURCE 14 /* Zone 打开资源超限 */
#define BLK_STS_ZONE_ACTIVE_RESOURCE 15 /* Zone 活跃资源超限 */
#define BLK_STS_OFFLINE 16 /* 设备离线 */
#define BLK_STS_DURATION_LIMIT 17 /* 命令时长超限 */
#define BLK_STS_INVAL 19 /* 无效大小或对齐 */include/linux/blk_types.h:185:
static inline bool blk_path_error(blk_status_t error)
{
switch (error) {
case BLK_STS_NOTSUPP:
case BLK_STS_NOSPC:
case BLK_STS_TARGET:
case BLK_STS_RESV_CONFLICT:
case BLK_STS_MEDIUM:
case BLK_STS_PROTECTION:
return false; /* 不可重试(路径切换无用)*/
}
return true; /* 其他错误可通过故障转移路径重试 */
}这个函数被多路径(DM multipath / NVMe multipath)使用,判断是否需要切换路径重试。
每个请求都有超时定时器,由 request_queue.timeout 定时器周期检查:
blk_mq_timeout_work() [超时工作项]
|
v
blk_mq_check_expired() [对每个 in-flight 请求检查]
|
+-- rq->deadline > jiffies? 继续等待
+-- 超时:调用 ops->timeout(rq)
|
返回 BLK_EH_DONE:
blk_mq_force_complete_rq() 强制完成
返回 BLK_EH_RESET_TIMER:
blk_add_timer(rq) 重置定时器
SCSI 错误恢复层(EH,Error Handler)是一个独立线程,负责处理复杂的错误场景:
SCSI 命令完成(错误)
|
v
scsi_io_completion()
|
+-- 可重试错误(如 UNIT_ATTENTION)
| v
| scsi_queue_insert() -> 重新插入队列
|
+-- 需要 EH 的错误
| v
| scsi_eh_scmd_add() -> EH 线程
| |
| v
| scsi_error_handler()
| +-- TUR (Test Unit Ready)
| +-- REQUEST SENSE
| +-- 设备/总线/主机 复位
| +-- 成功: scsi_eh_flush_done_q() 重新提交
| +-- 失败: blk_mq_fail_request()
|
+-- 不可恢复错误
v
blk_mq_end_request(rq, BLK_STS_IOERR)
include/linux/blk-mq.h:926 是请求重排队的主要接口:
void blk_mq_requeue_request(struct request *rq, bool kick_requeue_list);使用场景:
- 资源临时不足:
queue_rq返回BLK_STS_RESOURCE或BLK_STS_DEV_RESOURCE时,blk-mq 自动调用此函数 - 驱动显式重排队:多路径切换时,失败路径的请求需要重排队到成功路径
- DM 重映射:Device Mapper 目标设备在重映射时重排队
重排队流程:
blk_mq_requeue_request(rq, kick)
|
v
blk_mq_add_to_requeue_list(rq) [加入 q->requeue_list]
|
+-- kick=true:
| blk_mq_kick_requeue_list(q) [立即调度 requeue_work]
|
+-- kick=false:
等待下次调度
|
v
blk_mq_requeue_work() [工作项执行]
|
v
blk_mq_dispatch_rq_list() 或 elevator 重新入队
queue_rq() 返回值
|
+-- BLK_STS_OK:
| 请求已接受,等待完成回调
|
+-- BLK_STS_RESOURCE:
| 系统资源不足(内存等)
| -> blk_mq_requeue_request(rq, true)
| -> 停止队列直到资源释放
|
+-- BLK_STS_DEV_RESOURCE:
| 设备资源不足(飞行中 I/O 完成后会释放)
| -> blk_mq_stop_hw_queue(hctx)
| -> 完成回调中调用 blk_mq_start_hw_queue()
|
+-- BLK_STS_IOERR / 其他:
请求失败
-> blk_mq_end_request(rq, error)
-> bio->bi_end_io(bio) 通知上层
blk_noretry_request() 宏(include/linux/blkdev.h:697)检查请求是否有 FAILFAST 标志:
#define blk_noretry_request(rq) \
((rq)->cmd_flags & (REQ_FAILFAST_DEV |
REQ_FAILFAST_TRANSPORT |
REQ_FAILFAST_DRIVER))REQ_FAILFAST_DEV:不重试设备错误REQ_FAILFAST_TRANSPORT:不重试传输错误REQ_FAILFAST_DRIVER:不重试驱动错误
设置了任一 FAILFAST 标志的请求,在遇到对应错误时立即失败,不进行重试。
block/
├── blk-core.c # 块层核心:submit_bio、queue 管理
├── blk-mq.c # blk-mq 多队列核心实现
├── blk-mq-tag.c # 标签分配/释放(sbitmap)
├── blk-mq-sched.c # blk-mq 调度器接口层
├── blk-merge.c # bio/request 合并逻辑
├── blk-flush.c # flush/fua 命令处理
├── blk-throttle.c # blk-throttle(io.max 实现)
├── blk-cgroup.c # blkcg cgroup 控制器
├── blk-iocost.c # iocost I/O 代价控制器
├── blk-wbt.c # writeback throttle(写回节流)
├── blk-zoned.c # Zoned Block Device 支持
├── blk-integrity.c # T10 DIF 数据完整性
├── blk-stat.c # I/O 统计框架
├── elevator.c # I/O 调度器框架核心
├── elevator.h # elevator 内部头文件
├── mq-deadline.c # mq-deadline 调度器
├── bfq-iosched.c # BFQ 调度器主文件
├── bfq-wf2q.c # B-WF2Q+ 调度算法
├── bfq-cgroup.c # BFQ cgroup 支持
├── kyber-iosched.c # Kyber 调度器
├── partitions/ # 分区表解析(GPT/MBR/...)
│ ├── efi.c # GPT/EFI 分区解析
│ └── msdos.c # MBR 分区解析
└── ...
# 查看当前调度器
cat /sys/block/nvme0n1/queue/scheduler
# 输出: [none] mq-deadline kyber
# 切换调度器
echo "mq-deadline" > /sys/block/sda/queue/scheduler
# 推荐配置:
# NVMe SSD: none 或 kyber(低延迟,高吞吐)
# SATA SSD: mq-deadline(防止写饥饿)
# HDD: mq-deadline(扇区排序,减少磁头移动)
# 虚拟机磁盘: none(hypervisor 层已做调度)
# 数据库服务器(混合 I/O): BFQ 或 mq-deadline# 查看队列深度
cat /sys/block/nvme0n1/queue/nr_requests
# 增大队列深度(适合高 IOPS 场景)
echo 256 > /sys/block/nvme0n1/queue/nr_requests
# 对于 HDD,较小的队列深度可以减少延迟
echo 16 > /sys/block/sda/queue/nr_requests# 查看合并配置
cat /sys/block/sda/queue/nomerges
# 0: 允许所有合并
# 1: 只允许简单合并
# 2: 不允许合并
# 禁用合并(适合随机 I/O 为主的 NVMe)
echo 2 > /sys/block/nvme0n1/queue/nomerges# 创建 cgroup
mkdir /sys/fs/cgroup/io/myapp
# 设置带宽限制(8:0 = sda)
echo "8:0 rbps=104857600 wbps=52428800" > /sys/fs/cgroup/io/myapp/io.max
# 设置 IOPS 限制
echo "8:0 riops=1000 wiops=500" > /sys/fs/cgroup/io/myapp/io.max
# 将进程加入 cgroup
echo $PID > /sys/fs/cgroup/io/myapp/cgroup.procs/sys/block/<dev>/queue/
├── scheduler # I/O 调度器
├── nr_requests # 队列深度
├── read_ahead_kb # 预读大小(KB)
├── nomerges # 合并控制
├── rotational # 旋转设备标志(影响调度决策)
├── rq_affinity # 请求完成 CPU 亲和性
│ # 0: 不限制; 1: 在提交 CPU 完成; 2: 在提交 CPU 组完成
├── max_sectors_kb # 单请求最大扇区数
├── discard_max_bytes # discard 最大字节数
└── wbt_lat_usec # writeback throttle 延迟目标(微秒)
# 减小读超时(适合延迟敏感型读)
echo 250 > /sys/block/sda/queue/iosched/read_expire # 250ms
# 增大写超时(允许写更多等待,提升读吞吐)
echo 10000 > /sys/block/sda/queue/iosched/write_expire # 10s
# 调整批处理大小
echo 8 > /sys/block/sda/queue/iosched/fifo_batch# 查看 NVMe 队列数
ls /sys/class/nvme/nvme0/
# 设置轮询队列(用于低延迟 I/O)
modprobe nvme poll_queues=2
# 使用 io_uring + 轮询获得最低延迟
# io_uring_setup() with IORING_SETUP_IOPOLL# 查看 I/O 统计
iostat -x 1
# 查看 blk-mq 统计(debugfs)
cat /sys/kernel/debug/block/nvme0n1/hctx0/run
# 查看合并效率
cat /proc/diskstats
# 字段 9: 合并的读请求数; 字段 11: 合并的写请求数
# 查看 I/O 调度器队列
cat /sys/kernel/debug/block/sda/sched/| 函数 | 文件 | 功能 |
|---|---|---|
bio_alloc() |
block/bio.c |
分配 bio |
bio_add_page() |
block/bio.c |
添加页面到 bio |
submit_bio() |
block/blk-core.c |
提交 bio |
bio_endio() |
block/bio.c |
完成 bio,调用 bi_end_io |
bio_split() |
block/bio.c |
分割超限的 bio |
| 函数 | 文件 | 功能 |
|---|---|---|
blk_mq_alloc_request() |
block/blk-mq.c |
分配 request |
blk_mq_start_request() |
block/blk-mq.c |
标记开始处理 |
blk_mq_end_request() |
block/blk-mq.c |
完成请求 |
blk_mq_requeue_request() |
block/blk-mq.c |
重排队 |
blk_update_request() |
block/blk-mq.c |
更新请求进度 |
| 函数 | 文件 | 功能 |
|---|---|---|
blk_mq_run_hw_queue() |
block/blk-mq.c |
运行硬件队列 |
blk_mq_stop_hw_queue() |
block/blk-mq.c |
停止硬件队列 |
blk_mq_freeze_queue() |
block/blk-mq.c |
冻结队列(切换调度器用) |
blk_start_plug() |
block/blk-core.c |
开始 plug |
blk_finish_plug() |
block/blk-core.c |
结束 plug,触发提交 |
应用进程
| write()/io_uring
v
VFS Layer
| vfs_write() -> generic_file_write_iter()
v
Page Cache (Buffered I/O) 或 Direct I/O
| 标记脏页 | pin 用户页
| writeback daemon |
v v
bio_alloc() + bio_add_page() bio_alloc() + bio_iov_iter_get_pages()
| |
+-------------------+--------------------+
|
v
submit_bio(bio)
|
v
blk_mq_submit_bio() [block/blk-mq.c]
|
+----------+----------+
| |
尝试 plug 合并 分配 request (tag)
blk_attempt_plug_merge blk_mq_get_tag()
| |
+----------+----------+
|
current->plug?
/ \
YES NO
| |
加入 plug 直接 run_hw_queue
mq_list |
| |
blk_finish_plug() |
| |
+-+-------------+
|
v
blk_mq_dispatch_rq_list()
|
I/O 调度器存在?
/ \
YES NO
| |
elevator blk_mq_dequeue_from_ctx()
dispatch_request() |
| |
+-------+--------+
|
v
ops->queue_rq(hctx, &bd) [驱动层]
|
(NVMe) nvme_queue_rq()
|
写 SQE + 敲门铃
|
(NVMe 控制器执行 DMA)
|
MSI-X 中断 / io_uring 轮询
|
v
nvme_process_cq()
|
blk_mq_end_request(rq, status)
|
bio_endio(bio)
|
bi_end_io(bio) [回调上层:文件系统/应用]
|
v
read()/write() 返回
由 Claude Code 分析生成