一、块设备子系统全景图
Linux 块设备子系统是内核 I/O 栈中最核心的中间层,它将上层(文件系统、Page Cache、io_uring)的逻辑请求翻译成设备控制器可执行的命令。一个典型的 write() 调用从 VFS 到 NVMe SSD 的旅程中,块层承担了请求合并、排队整形、调度分发、错误重试的枢纽角色。
架构上分为四个层次:
| 层次 | 核心组件 | 职责 |
|---|---|---|
| 通用块层 | bio、request、request_queue | 构造/合并/提交 I/O 请求 |
| I/O 调度层 | elevator(mq-deadline/bfq/kyber/none) | 排序/合并/带宽公平性 |
| 多队列框架 | blk-mq(mapqueue/hwqueue) | 多核 NUMA 亲和、硬件队列分散拥塞 |
| 设备驱动层 | nvme、virtio-blk、scsi、ramdisk | 启动/完成/错误处理 I/O |
从 Linux 5.0 开始,blk-mq(Multi-Queue Block Layer)成为默认架构。它通过 Hardware Dispatch Queue 与 Software Submission Queue 的分离映射,彻底解决了单队列锁竞争问题,让 NVMe 等多核设备可以线速运行。
二、bio 结构体:I/O 请求的基本单元
块层中最核心的数据结构是 struct bio(include/linux/blk_types.h),代表一次不完整的块 I/O 操作。一个 bio 由多个 struct bio_vec 组成的段(segment)数组构成,每个段对应一个内存页内连续的缓冲区。
struct block_device *bi_bdev; // 目标块设备 blk_opf_t bi_opf; // op + flags (READ/WRITE/FLUSH/DISCARD) unsigned short bi_ioprio; // I/O 优先级 (best-effort/realtime/idle) blk_status_t bi_status; // 完成状态 atomic_t __bi_remaining; // 引用计数 (多段拆分追踪) struct bio_vec *bi_io_vec; // 段数组 struct bio_set *bi_pool; // 内存池来源 unsigned short bi_vcnt; // 当前段数 unsigned short bi_max_vecs; // 段数组容量 struct bio_vec bi_inline_vecs[]; // BIO_INLINE_VECS 内联优化 void (*bi_end_io)(struct bio *); // 完成回调 void *bi_private; // 私有完成上下文 bio_io_advance_fn *bi_iotype; // I/O 统计分类 };
bio_vec 段结构
unsigned int bv_len; // 段长度 (字节) unsigned int bv_offset; // 页内偏移 };
一个典型的文件系统 write() 调用会经过:Page Cache → buffer_head/iov_iter → bio_alloc() → bio_add_page() → submit_bio()。每次 submit_bio() 调用都可能触发合并逻辑后再进入调度器。
三、从 bio 到 request:Elevator 合并魔术
bio 进入块层后,经过 blk_queue_enter → bio_split(如果需要)→ bio_attempt_back/front_merge → blk_mq_bio_to_request 路径。其中关键的合并策略如下:
| 合并类型 | 触发条件 | 合并后效果 |
|---|---|---|
| Back merge | 新 bio 尾扇区 = 已有 request 头扇区 | 新 bio 接到 request 尾部 |
| Front merge | 新 bio 头扇区 = 已有 request 尾扇区 | 新 bio 接到 request 头部 |
| Plug merge | Plug 批处理窗口内相邻请求 | 批量 coalescing 降低 request 数 |
| Cross-sector | 跨 bio_vec 边界连续扇区 | 仅在同 request 内操作 |
批处理插入:Plug & Unplug
Linux 使用 blk-plug 机制(struct blk_plug)实现 I/O 批量蓄流。进程提交 I/O 时先攒在 plug->mq_list,到 unplug 时再一次性刷入调度器,大幅减少锁争用和碎片化请求。
void blk_finish_plug(struct blk_plug *plug) { blk_mq_flush_plug_list(plug, false); kfree(plug); }
四、blk-mq 多队列框架:现代存储的性能基石
传统单队列块层(blk-sq)将所有 CPU 的请求塞进一把锁保护的队列,NVMe SSD 随机 4K 读可达百万 IOPS,但单队列锁争用导致多核扩展性极差。blk-mq 通过两个核心抽象解决这一问题:
4.1 队列拓扑映射
- Software Submission Queue (SQ):per-CPU 或 NUMA-node 本地提交队列。多个 CPU 可同时 push 请求而无需跨核锁。
- Hardware Dispatch Queue (HWQ):对应设备端实际硬件 submission queue(如 NVMe 的 SQ0..SQ[n-1])。驱动从 HWQ 选取请求填充设备 Doorbell。
- map:标签集(tag set)中的hw_queue 索引映射,决定 HWQ 到 SQ 的亲和关系。
关键数据结构:
unsigned int *map; // hw_queue 映射表 [map_count] unsigned int nr_maps; // 映射类型数 (HCTX_TYPE_DEFAULT 等) unsigned int nr_hw_queues; // 硬件队列总数 unsigned int queue_depth; // 每队列深度限制 unsigned int cmd_size; // 私有命令体扩展大小 int numa_node; // NUMA 节点偏好 void *driver_data; // 驱动层私有数据 };
4.2 驱动回调操作集
blk-mq 驱动通过 struct blk_mq_ops 与核心层交互:
.commit_rqs = my_commit_rqs, // 批量提交到设备 .get_budget = my_get_budget, // 限流预算获取 .put_budget = my_put_budget, // 预算归还 .timeout = my_timeout, // 超时处理 (用于 error recovery) .poll = my_poll, // 轮询完成模式 (高 IOPS 场景) .map_queue = blk_mq_map_queue,// 物理映射到 hctx .init_hctx = my_init_hctx, // per-hw_queue 初始化钩子 .exit_hctx = my_exit_hctx, // 清理钩子 .init_request = my_init_request, // request 分配后定制命令体 .show_rq = my_show_rq, // /proc/调试输出 };
4.3 queue_rq 驱动核心循环
my_queue_rq() 是每个 HWQ 在中断上下文(或轮询上下文)中执行的入口。驱动从此函数取出 request 并翻译成设备命令。
break; default: return BLK_STS_IOERR; } return BLK_STS_OK; // 异步完成,稍后 my_end_rq() 通知 }
完成后驱动调用 blk_mq_end_request(rq, status) 通知核心层回收 tag 并转发到顶层 waiter。
五、I/O 调度器详解
Linux 5.x 多队列时代提供了三种多队列 I/O 调度器,加上 none 直通模式:
| 调度器 | 设计目标 | 队列模型 | 最佳场景 |
|---|---|---|---|
| mq-deadline | 延迟读写期限 | sorted_lists + fifo_batch | 数据库、尾延迟敏感 |
| bfq | 带宽公平共享 | 预算权重树 (Budget Fair Queueing) | 交互式、混部容器 |
| kyber | 低延迟自适应 | 并发目标令牌环 | NVMe/RDMA 高速设备 |
| none | 无调度直通 | 直接 submit | NVMe 自带仲裁或 flash 直写 |
5.1 mq-deadline 内核参数
├── scheduler // 当前调度器 [mq-deadline bfq kyber none] ├── read_expire // 读请求超时 (ms), 默认 500 ├── write_expire // 写请求超时 (ms), 默认 5000 ├── writes_starved // 写饿死前最大调度读次数, 默认 2 ├── fifo_batch // 单批下发量, 默认 16 ├── front_merges // 是否允许 front_merge, 布尔 ├── nr_requests // 队列深度上限, 默认 128 └── features // 特性位图 (FUA/HEADMERGE/SG etc)
5.2 调度器切换命令
cat /sys/block/nvme0n1/queue/scheduler [mq-deadline] kyber bfq none # 切换到低延迟调度 echo none > /sys/block/nvme0n1/queue/scheduler # 数据库场景启用 mq-deadline + 调大队列深度 echo mq-deadline > /sys/block/sda/queue/scheduler echo 256 > /sys/block/sda/queue/nr_requests echo 128 > /sys/block/sda/queue/read_expire
六、完整驱动实战:RAM Disk 字符设备
下面是一个基于 blk-mq 的完整 RAM disk 驱动(简化版),展示请求处理、tag 分配、中断模拟全链路。
#include#include #include #include #define RAMDISK_SIZE (64 * 1024 * 1024) // 64 MB #define RAMDISK_NAME "myram" #define SECTORSZ 512 #define MINORS 4 static int major; static struct gendisk *disk; static struct request_queue *queue; static struct blk_mq_tag_set tag_set; static u8 *data; // vmalloc 后备内存 static DEFINE_SPINLOCK(lock); /* --- blk-mq 操作集 --- */ static blk_status_t myram_queue_rq(struct blk_hw_ctx *hctx, const struct blk_queue_ctx *bd) { struct request *rq = bd->rq; sector_t sector = blk_rq_pos(rq); unsigned long offset = sector * SECTORSZ; blk_status_t ret = BLK_STS_OK; struct bio_vec bv; struct req_iterator iter; if (offset + blk_rq_bytes(rq) > RAMDISK_SIZE) { blk_mq_end_request(rq, BLK_STS_IOERR); return BLK_STS_IOERR; } /* 遍历 request 中包含的所有 bio 段 */ rq_for_each_segment(bv, rq, iter) { void *mem = kmap_local_page(bv.bv_page); unsigned long flags; switch (req_op(rq)) { case REQ_OP_READ: spin_lock_irqsave(&lock, flags); memcpy(mem + bv.bv_offset, data + offset + iter.iter.bi_bvec_done, bv.bv_len); spin_unlock_irqrestore(&lock, flags); break; case REQ_OP_WRITE: spin_lock_irqsave(&lock, flags); memcpy(data + offset + iter.iter.bi_bvec_done, mem + bv.bv_offset, bv.bv_len); spin_unlock_irqrestore(&lock, flags); break; default: ret = BLK_STS_IOERR; goto out; } kunmap_local(mem); } out: blk_mq_end_request(rq, ret); return ret; } static const struct blk_mq_ops myram_ops = { .queue_rq = myram_queue_rq, }; /* --- 注册与卸载 --- */ static int __init myram_init(void) { int ret; major = register_blkdev(0, RAMDISK_NAME); if (major < 0 xss=removed>return major; data = vmalloc(RAMDISK_SIZE); if (!data) { ret = -ENOMEM; goto out_unreg; } disk = alloc_disk(MINORS); if (!disk) { ret = -ENOMEM; goto out_free; } memset(&tag_set, 0, sizeof(tag_set)); tag_set.ops = &myram_ops; tag_set.nr_hw_queues = 4; // 4 个硬件队列 tag_set.queue_depth = 128; // 每队列深度 128 tag_set.numa_node = NUMA_NO_NODE; tag_set.cmd_size = 0; ret = blk_mq_alloc_tag_set(&tag_set); if (ret) goto out_free_disk; queue = blk_mq_init_queue(&tag_set); if (IS_ERR(queue)) { ret = PTR_ERR(queue); goto out_tagset; } blk_queue_logical_block_sizeblk_queue_max_hw_sectorsblk_queue_flag_set// SSD 标识 queue->queuedata = (void *)disk; disk->major = major; disk->first_minor = 0; disk->minors = MINORS; disk->fops = &myram_fops; disk->private_data = data; sprintf(disk->disk_name, "%s", RAMDISK_NAME); set_capacity(disk, RAMDISK_SIZE / SECTORSZ); device_add_disk(NULL, disk, false); pr_info("myram: loaded, %u MB / %u queues ", RAMDISK_SIZE/(1024*1024), tag_set.nr_hw_queues); return 0; out_tagset: blk_mq_free_tag_set(&tag_set); out_free_disk: put_disk(disk); out_free: vfree(data); out_unreg: unregister_blkdev(major, RAMDISK_NAME); return ret; } static void __exit myram_exit(void) { del_gendisk(disk); blk_cleanup_disk(disk); // 自动 put_disk+cleanup queue blk_mq_free_tag_set(&tag_set); vfree(data); unregister_blkdev(major, RAMDISK_NAME); pr_info("myram: unloaded "); } module_init(myram_init); module_exit(myram_exit); MODULE_LICENSE("GPL");
七、blk-mq 与 io_uring 深度融合
Linux 5.x 起,io_uring 可以通过 IORING_SETUP_SQPOLL 模式与 blk-mq 协同:设备侧开启 polling(blk_mq_poll())不仅降低 IRQ 开销,还与 io_uring 的零拷贝共享内存(fixed buffer)组合进一步减少内存拷贝。
- Fixed Buffers:io_uring 预注册
io_uring_register_buffers()的 page 数组,块层直接用bio_map_user()挂载到 bio_vec。 - Fixed Files:预注册 fd(
io_uring_register_files()),每个 SQE 通过 index 指定目标块设备 fd,免去每次 fget/fput 开销。 - SQPOLL 轮询提交:内核线程轮询 SQ ring 有否新有 SQE,驱动侧调用
blk_mq_run_hw_queues()无锁触发。 - IRQ 模式 poll:NVMe 驱动注册
blk_mq_ops.poll回调,硬中断到达后改为 poll 模式批量收割 CQ entry。
八、生产级调优实战
8.1 性能调优决策树
| 症状 | 根因诊断 | 调优措施 |
|---|---|---|
| IOPS 低,CPU 空闲 | 队列深度不足 | echo 512 > /sys/block/nvme0n1/queue/nr_requests |
| 尾 P99 延迟飙升 | 写饿死 / 调度器顺序 | 切 mq-deadline + writes_starved=2, read_expire=50 |
| 多核不扩展 | 单队列争用(旧内核) | 切 blk-mq,确认 nr_hw_queues > 1 |
| 读带宽抖动 | 混合读写干扰 | 切 bfq + low_latency=1 |
| NVMe 中断打满单核 | RSS 队列数不足 | echo f > /proc/irq/XX/smp_affinity 手动分发 |
| 容器 IO 争抢 | 没有权重隔离 | blkio.weight + cgroup v2 io.max |
8.2 NVMe 多队列数与中断绑定
NVMe 设备驱动支持 io_queue_count 强制指定硬件队列数,echo N > /sys/block/nvme0n1/device/queue_count 可覆盖。中断亲和则通过 /proc/irq/ 手动绑定,每个队列一个独立 CPU。
8.3 cgroup v2 IO 隔离
// Device: rbps 读上限 wbps 写上限 riops 读IOPS wiops 写IOPS 8:0 rbps=104857600 wbps=52428800 riops=2000 wiops=1000 # 检查 /sys/fs/cgroup/container-a/io.stat // 字段: rbytes wbytes rios wios dbytes dios
8.4 性能监控三板斧
- iostat -xmt 2:查看
await、avgqu-sz、%util、r/s、w/s。await > 10ms 即需排查。 - blktrace:
blktrace -d /dev/nvme0n1 -w 10 -o trace抓取 D(下发)、I(插入)、C(完成)事件时间戳。 - bpftrace:
kprobe:blk_mq_start_request { @[comm] = count(); }实时统计进程 I/O。
九、最佳实践总结
- SSD/NVMe 用 none 或 kyber:NVMe 自带硬件仲裁和 submit queue,无需软件调度开销。
- 数据库/mq-deadline 优先:
read_expire=min(avg_lat/2, 100),write_expire放宽到 5000ms。 - 队列深度 = 延迟 × IOPS:NVMe 默认 1024 队列深度,但容器场景需按
io.max限制按预算分配。 - 中断与提交队列一一绑定:生产部署每个硬件队列配一个 CPU 核,避免跨节点转发。
- 避免 fsync 风暴:批量 flush、使用
O_DSYNC + write barriers、调整dirty_expire_centisecs=500。 - blk-mq 设施位合理配置:QUEUE_FLAG_NONROT(SSD)、QUEUE_FLAG_PCI_P2PDMA(跨 PCIe peer)。
- io_uring 直通:高吞吐存储应用优先
fixed buffer + SQPOLL + registered files三件套。
十、知识点关联图
Linux 块设备层并非孤岛——它是多条路径的交汇点。user space 的 write() 经 VFS 落地 Page Cache;direct path 跳过 cache 直接 submit_bio;io_uring SQE 直接映射 bio_vec;loop device 将块请求"倒回"文件系统;device-mapper(lvm/cache/bcache/thin)进一步截取 reshaping request。理解块层 bc 是整个 I/O 栈能力跃升的关键。
下一篇可深入 Linux AIO + uring 存储引擎实战 或 NVMe 驱动多队列 + Fabrics(TCP/RDMA),敬请期待。

发表评论 取消回复