mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1569 字
5 分钟
块层与 I/O 栈
2025-06-19

strace 追踪一次 write() 调用,从系统调用返回到数据真正落在磁盘上,中间还隔着页缓存、块层、I/O 调度器、设备驱动四层。SATA HDD 时代,单队列就够用;NVMe SSD 一秒能处理百万级 I/O 请求,单队列反而成了瓶颈——这就是 blk-mq 多队列架构诞生的原因。

Linux 块层与 I/O 栈是系统调用与磁盘之间的完整通路。理解这条通路,才能理解 I/O 延迟的构成和优化的空间。

一、Linux I/O 栈全景#

1.1 I/O 栈的层次结构#

graph TB subgraph 用户空间["用户空间"] APP["应用程序<br/>read()/write()"] LIBAIO["libaio<br/>io_submit()"] IOURING["io_uring<br/>SQ/CQ 环"] end subgraph 内核空间["内核空间"] VFS["VFS 层<br/>虚拟文件系统"] FS["文件系统<br/>ext4/XFS/Btrfs"] PAGECACHE["页缓存<br/>Page Cache"] BLOCK["块层<br/>blk-mq"] IOSCHED["I/O 调度器<br/>mq-deadline/bfq/none"] DRIVER["设备驱动<br/>NVMe/AHCI"] end subgraph 硬件["硬件"] SSD["NVMe SSD"] HDD["SATA HDD"] end APP --> VFS --> FS --> PAGECACHE --> BLOCK --> IOSCHED --> DRIVER LIBAIO --> BLOCK IOURING --> BLOCK DRIVER --> SSD DRIVER --> HDD style 用户空间 fill:#e8f5e9,stroke:#2e7d32 style 内核空间 fill:#e3f2fd,stroke:#1565c0 style 硬件 fill:#fff3e0,stroke:#e65100

1.2 I/O 请求的完整路径#

一个 write() 请求从应用到磁盘的完整路径:

步骤层次操作延迟贡献
1用户空间write(fd, buf, size) 系统调用~1 μs
2VFS虚拟文件系统查找文件~1 μs
3文件系统Extent 查找、块分配~5 μs
4页缓存写入 Page Cache,标记脏页~1 μs
5返回write() 返回(数据在内存中)
6块层脏页回写:构造 bio 请求~5 μs
7I/O 调度合并、排序请求~1 μs
8设备驱动提交到设备队列~1 μs
9硬件DMA 传输到磁盘10 μs–10 ms
Note

默认情况下,write() 系统调用只将数据写入 Page Cache 就返回了,并不会等待数据真正落盘。要保证数据持久化,必须调用 fsync() 或使用 O_DIRECT + O_SYNC

1.3 关键数据结构#

// bio: 块 I/O 请求的核心数据结构
struct bio {
struct gendisk *bi_disk; // 目标磁盘
unsigned int bi_opf; // 操作标志(READ/WRITE/SYNC)
struct bvec_iter bi_iter; // 迭代器
unsigned short bi_vcnt; // bio_vec 数量
struct bio_vec *bi_io_vec; // 数据向量数组
bio_end_io_t *bi_end_io; // 完成回调
void *bi_private; // 私有数据
};
// bio_vec: 描述一个内存段
struct bio_vec {
struct page *bv_page; // 内存页
unsigned int bv_len; // 长度
unsigned int bv_offset; // 页内偏移
};
// request: 经 I/O 调度器处理后的请求
struct request {
struct request_queue *q; // 所属队列
struct bio *bio; // 关联的 bio
sector_t __sector; // 起始扇区
unsigned int __data_len; // 数据长度
struct list_head queuelist; // 队列链表
};

二、blk-mq 多队列架构#

2.1 传统单队列的瓶颈#

Linux 3.x 之前使用单队列(Legacy Block Layer):

graph LR subgraph 单队列瓶颈 CPU0["CPU 0"] --> LOCK["全局锁"] CPU1["CPU 1"] --> LOCK CPU2["CPU 2"] --> LOCK CPU3["CPU 3"] --> LOCK LOCK --> SQ["单队列<br/>request_queue"] SQ --> DRIVER_SQ["驱动处理"] end style LOCK fill:#ffcdd2,stroke:#c62828

单队列的问题:

  • 全局锁竞争:所有 CPU 共享一个队列,锁竞争严重
  • 单线程提交:I/O 调度器单线程处理,无法并行
  • 中断瓶颈:完成中断集中在单个 CPU

2.2 blk-mq 架构#

I/O 模型提交方式完成通知系统调用次数适用场景
同步 read/write阻塞调用函数返回每次 I/O 1 次简单场景,低并发
libaioio_submit()io_getevents()提交+收割各 1 次数据库(Direct I/O)
io_uring写入 SQE 环读取 CQE 环批量提交 0 次高并发、低延迟
Tip

io_uring 的”零系统调用”模式需要内核 5.1+ 和 IORING_SETUP_SQPOLL 标志。数据库场景下,io_uring 相比 libaio 可提升 15-30% 的 I/O 吞吐量。

blk-mq(Block Multi-Queue)引入多队列架构,解决单队列瓶颈:

graph TB subgraph blk_mq["blk-mq 多队列架构"] subgraph 软件队列["软件队列(每 CPU)"] SQ0["SW Queue 0<br/>CPU 0"] SQ1["SW Queue 1<br/>CPU 1"] SQ2["SW Queue 2<br/>CPU 2"] SQ3["SW Queue 3<br/>CPU 3"] end subgraph 硬件队列["硬件队列(每设备)"] HQ0["HW Queue 0"] HQ1["HW Queue 1"] HQ2["HW Queue 2"] HQ3["HW Queue 3"] end SQ0 --> HQ0 SQ1 --> HQ1 SQ2 --> HQ2 SQ3 --> HQ3 end style 软件队列 fill:#e3f2fd,stroke:#1565c0 style 硬件队列 fill:#c8e6c9,stroke:#2e7d32
组件说明数量
软件队列(Software Queue)每 CPU 一个,无锁操作= CPU 数量
硬件队列(Hardware Queue)对应设备的提交队列= 设备队列数
标记集(Tag Set)管理请求标签分配1/设备
I/O 调度在软件队列上执行每 HW 队列一个
# 查看 blk-mq 队列信息
cat /sys/block/nvme0n1/queue/nr_requests
cat /sys/block/nvme0n1/mq/*/cpu_list
ls /sys/block/nvme0n1/mq/
# 输出示例(4 核 CPU + NVMe SSD):
# 0/ 1/ 2/ 3/
# 表示 4 个硬件队列

2.3 blk-mq 请求流程#

// blk-mq 请求处理流程(简化)
void blk_mq_submit_bio(struct bio *bio) {
// 1. 获取当前 CPU 对应的软件队列
struct blk_mq_ctx *ctx = blk_mq_get_ctx(bio);
// 2. 获取请求标签(预分配的 request 结构)
struct request *rq = blk_mq_alloc_request(ctx, bio->bi_opf);
// 3. 将 bio 附加到 request
blk_mq_bio_to_request(rq, bio);
// 4. I/O 调度(如果启用)
if (has_scheduler(q)) {
blk_mq_sched_insert_request(rq);
}
// 5. 提交到硬件队列
blk_mq_run_hw_queue(hctx, true);
}
// 完成回调
void blk_mq_complete_request(struct request *rq) {
// 1. 调用 bio 的完成回调
rq->bio->bi_end_io(rq->bio);
// 2. 释放请求标签
blk_mq_free_request(rq);
}

三、I/O 调度器#

3.1 I/O 调度器的作用#

I/O 调度器在块层和设备驱动之间,负责合并、排序和调度 I/O 请求:

功能说明目的
合并(Merge)将相邻的请求合并为一个减少请求数量
排序(Sort)按扇区号排序请求减少寻道(HDD)
调度(Schedule)决定请求的执行顺序公平性、延迟保证

3.2 常见 I/O 调度器#

调度器适用设备特点配置
none(noop)NVMe SSD不做排序,直接提交最快,SSD 不需要排序
mq-deadlineSATA SSD / HDD保证请求延迟上限默认,平衡性能与延迟
bfq桌面 HDD公平带宽分配交互式场景
kyberNVMe SSD基于令牌的调度低延迟场景
# 查看当前 I/O 调度器
cat /sys/block/nvme0n1/queue/scheduler
# [none] mq-deadline kyber bfq
# 切换 I/O 调度器
echo mq-deadline > /sys/block/nvme0n1/queue/scheduler
# 数据库推荐:
# NVMe SSD → none(最快,SSD 不需要排序)
# SATA SSD → mq-deadline(平衡延迟)
# HDD → bfq 或 mq-deadline(减少寻道)

3.3 mq-deadline 调度器#

mq-deadline 是最常用的调度器,保证每个请求在超时前被处理:

graph LR subgraph deadline["mq-deadline 调度器"] REQ_IN["新请求"] --> SORT["排序队列<br/>按扇区号排序"] REQ_IN --> FIFO_R["读 FIFO<br/>按到达时间"] REQ_IN --> FIFO_W["写 FIFO<br/>按到达时间"] SORT --> DISPATCH["派发"] FIFO_R -->|"读超时<br/>500ms"| DISPATCH FIFO_W -->|"写超时<br/>5s"| DISPATCH end style DISPATCH fill:#c8e6c9,stroke:#2e7d32 style FIFO_R fill:#e3f2fd,stroke:#1565c0 style FIFO_W fill:#fff9c4,stroke:#f9a825
参数默认值说明
read_expire500 ms读请求超时时间
write_expire5000 ms写请求超时时间
writes_starved2连续派发读请求的最大次数
front_merges1是否允许前向合并

四、io_uring 异步 I/O#

4.1 I/O 模型演进#

graph LR SYNC["同步 I/O<br/>read()/write()"] --> AIO["libaio<br/>io_submit()"] AIO --> URING["io_uring<br/>共享环形缓冲区"] style SYNC fill:#ffcdd2,stroke:#c62828 style AIO fill:#fff9c4,stroke:#f9a825 style URING fill:#c8e6c9,stroke:#2e7d32
I/O 模型系统调用次数是否阻塞缓冲区对齐适用场景
同步 I/O每次操作 1 次无要求简单场景
libaio提交+完成 2 次必须 4KB 对齐O_DIRECT
io_uring批量提交 0–1 次无要求高性能场景

4.2 io_uring 架构#

// io_uring 核心数据结构
struct io_uring {
// 提交队列(SQ):应用→内核
struct io_uring_sq {
uint32_t *khead; // 内核写,应用读
uint32_t *ktail; // 应用写,内核读
uint32_t *kring_mask;
struct io_uring_sqe *sqes; // 提交队列条目
};
// 完成队列(CQ):内核→应用
struct io_uring_cq {
uint32_t *khead; // 应用写,内核读
uint32_t *ktail; // 内核写,应用读
uint32_t *kring_mask;
struct io_uring_cqe *cqes; // 完成队列条目
};
};
// 提交队列条目(SQE)
struct io_uring_sqe {
uint8_t opcode; // 操作码(read/write/fsync/...)
uint32_t fd; // 文件描述符
uint64_t off; // 偏移量
uint64_t addr; // 数据缓冲区地址
uint32_t len; // 数据长度
uint64_t user_data; // 用户数据(完成时返回)
};

4.3 io_uring 的优势#

# io_uring vs libaio vs 同步 I/O 性能对比
import subprocess
# 同步 I/O
# 每次操作需要 2 次系统调用(write + fsync)
# 4KB 写入延迟:~20μs(NVMe)
# libaio
# 批量提交,但需要 O_DIRECT 和缓冲区对齐
# 4KB 写入延迟:~12μs(NVMe)
# io_uring
# 零系统调用提交(SQ 环满时才调用 io_uring_enter)
# 4KB 写入延迟:~8μs(NVMe)
# 批量提交 1000 个请求:~200μs(0.2μs/请求)
Important

io_uring 是 Linux 5.1+ 引入的高性能异步 I/O 接口,通过共享内存环形缓冲区避免了系统调用开销。RocksDB 7.x+ 已支持 io_uring,在 NVMe SSD 上可提升 10–30% 的 IOPS。

五、Direct I/O 与 Buffered I/O#

5.1 两种 I/O 模式对比#

特性Buffered I/ODirect I/O
Page Cache经过绕过
数据对齐无要求4KB 对齐
写入路径write→Page Cache→后台回写write→直接到磁盘
读取路径先查 Page Cache→未命中读磁盘直接读磁盘
适用场景通用数据库(自行管理缓存)

5.2 数据库为什么用 Direct I/O#

// 数据库使用 Direct I/O 的原因
// 1. 避免 Double Caching
// 数据库有自己的 Buffer Pool,如果用 Buffered I/O,
// 同一数据在 Buffer Pool 和 Page Cache 中各存一份,浪费内存
// 2. 控制刷盘时机
// 数据库需要精确控制 WAL 先于数据页落盘(fsync 语义)
// Buffered I/O 的后台回写时机不可控
// 3. 避免预读干扰
// 操作系统的预读策略不适合数据库的随机访问模式
// MySQL InnoDB 配置
// innodb_flush_method = O_DIRECT
// 绕过 Page Cache,使用 O_DIRECT 写入数据文件和 WAL
// PostgreSQL 配置
// PostgreSQL 默认使用 Buffered I/O
// 但 WAL 可以配置:wal_sync_method = open_datasync
# MySQL Direct I/O 配置
[mysqld]
innodb_flush_method = O_DIRECT
# O_DIRECT — 数据文件和 WAL 都用 Direct I/O
# O_DSYNC — WAL 用 O_SYNC 打开
# fdatasync — 默认,用 fdatasync() 刷盘
# PostgreSQL WAL 配置
wal_sync_method = open_datasync # Linux 默认
# fsync — 使用 fsync()
# fdatasync — 使用 fdatasync()
# open_sync — O_SYNC 打开
# open_datasync — O_DSYNC 打开

六、I/O 栈性能调优#

6.1 关键调优参数#

# 虚拟内存调优
# 脏页比例(开始后台回写)
echo 10 > /proc/sys/vm/dirty_background_ratio
# 脏页比例(开始同步回写)
echo 20 > /proc/sys/vm/dirty_ratio
# 脏页过期时间(centisecs)
echo 3000 > /proc/sys/vm/dirty_writeback_centisecs
# 块层调优
# 队列深度
echo 256 > /sys/block/nvme0n1/queue/nr_requests
# 预读大小
echo 0 > /sys/block/nvme0n1/queue/read_ahead_kb
# NVMe 调优
# 最大硬件队列数
cat /sys/block/nvme0n1/queue/nr_hw_queues
# 每队列深度
cat /sys/block/nvme0n1/queue/queue_depth

6.2 数据库 I/O 调优矩阵#

参数MySQL (InnoDB)PostgreSQLRocksDB
I/O 模式O_DIRECTBufferedO_DIRECT
I/O 调度器none (NVMe)none (NVMe)none (NVMe)
队列深度128–256128–256128–1024
脏页比例10/205/1010/20
预读关闭默认关闭
io_uring实验性不支持支持(7.x+)

七、实战:I/O 栈观察与调试#

7.1 I/O 栈跟踪#

# 使用 ftrace 跟踪 I/O 路径
trace-cmd record -e block:block_rq_issue -e block:block_rq_complete \
-p function_graph -g blk_mq_submit_bio sleep 10
# 使用 bcc 观察 I/O 延迟
# 按进程统计 I/O 延迟
/usr/share/bcc/tools/biolatency -p $(pidof mysqld) 1 5
# 按进程统计 I/O 大小
/usr/share/bcc/tools/biosnoop -p $(pidof mysqld)
# 观察块层统计
cat /sys/block/nvme0n1/stat
# Field 1: read I/Os
# Field 5: write I/Os
# Field 9: in-flight I/Os

7.2 I/O 栈性能分析#

# 使用 perf 分析 I/O 热点
perf record -e block:block_rq_issue -a sleep 10
perf report
# 使用 iostat 观察 I/O 统计
iostat -x 1
# await: 平均 I/O 延迟(ms)
# %util: 设备利用率
# svctm: 平均服务时间(ms)
# avgqu-sz: 平均队列深度
# 使用 iotop 观察进程 I/O
iotop -oP

7.3 blk-mq 队列观察#

# 查看 blk-mq 统计
for q in /sys/block/nvme0n1/mq/*/; do
echo "=== Queue $(basename $q) ==="
cat "$q/cpu_list"
cat "$q/tags"
done
# 查看 I/O 调度器统计
cat /sys/block/nvme0n1/mq/0/scheduler
Warning

块层的多队列架构(blk-mq)是现代存储性能的基础。如果你的内核还在使用单队列(legacy block layer),升级到 blk-mq 可以显著提升 NVMe SSD 的 IOPS。大多数现代发行版(内核 5.x+)已默认启用 blk-mq。

八、总结#

主题核心要点关键词
I/O 栈全景从系统调用到磁盘 9 个步骤,延迟从 1μs 到 10msI/O 栈, 延迟分布
blk-mq多队列架构消除单队列锁竞争,是 NVMe SSD 标配多队列, 无锁
I/O 调度器NVMe SSD 用 none,SATA SSD 用 mq-deadline,HDD 用 bfq调度策略, 设备匹配
io_uring零系统调用的异步 I/O,比 libaio 快 30%+异步 I/O, 零拷贝
Direct I/O数据库绕过 Page Cache,自行管理缓存和刷盘O_DIRECT, 自管理

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

块层与 I/O 栈
https://blog.souloss.com/posts/storage/storage-block-layer-and-io-stack/
作者
Souloss
发布于
2025-06-19
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时