Linux I/O Stack 深度实战:从系统调用到磁盘硬件的完整数据链路
引言
当你执行 cat /etc/hosts 或数据库执行一次 fsync() 时,整个 Linux 内核中最复杂的子系统之一——I/O Stack——便开始了一场精密的"接力赛"。数据从用户空间发起,穿越 VFS 抽象层、文件系统、Page Cache、通用块层、I/O 调度队列,最终在磁盘硬件上留下磁信号或电荷痕迹。
本文将以源码级视角,逐层拆解 Linux I/O 子系统的完整架构,帮你建立从应用层 API 到物理介质的"端到端透视能力",并结合 iostat、blktrace、bcc 等工具进行实战诊断与性能调优。
第一部分:I/O Stack 全景架构
数据流全景图
┌─────────────────────────────────────────────────────────────────┐
│ 用户空间 (User Space) │
│ 应用程序 → libc read/write/pread/fsync (syscall) │
└───────────────────────────────┬─────────────────────────────────┘
│ 系统调用 (int 0x80 / syscall)
┌───────────────────────────────▼─────────────────────────────────┐
│ 内核空间 (Kernel Space) │
│ │
│ ┌───────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ VFS │──▶│ Page Cache │──▶│ 文件系统 (ext4/ │ │
│ │ (sys_ │ │ (address_ │ │ xfs/btrfs) │ │
│ │ read/ │ │ space_ops) │ │ ext4_file_read/ │ │
│ │ write) │ └──────────────┘ │ xfs_file_write │ │
│ └─────┬─────┘ │ └──────────┬───────────┘ │
│ │ │ 命中/未命中 │ │
│ │ ▼ ▼ │
│ ┌─────┴──────────────────────────────────────────────────┐ │
│ │ 通用块层 (Block Layer) │ │
│ │ bio 合并、请求合并、plug/unplug │ │
│ │ submit_bio → generic_make_request │ │
│ └──────────────────────┬──────────────────────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ I/O 调度器 (mq-deadline / bfq / kyber) │ │
│ │ 请求排序、合并、优先级分配 │ │
│ └──────────────────────┬───────────────────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 设备驱动层 (nvme / scsi / virtio-blk) │ │
│ │ 提交硬件队列、DMA 映射、中断处理 │ │
│ └──────────────────────┬───────────────────────────────┘ │
└─────────────────────────┼───────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ 硬件层 (Hardware) │
│ NVMe SSD / SCSI HDD / virtio-blk / Network Block Device │
└─────────────────────────────────────────────────────────────────┘
关键数据结构关系
// 文件描述符 → inode → block device 的完整关联
struct file {
struct path f_path; // 包含 dentry + vfsmount
struct inode *f_inode; // 对应的 inode
const struct file_operations *f_op; // 文件操作表
struct address_space *f_mapping; // Page Cache 映射
};
struct inode {
struct super_block *i_sb; // 超级块(文件系统实例)
struct address_space i_data; // 文件的 Page Cache
struct block_device *i_bdev; // 关联的块设备(仅对 block inode)
};
第二部分:VFS 抽象层——系统调用的入口
sys_read 的执行路径
// fs/read_write.c
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
struct fd f = fdget_pos(fd);
loff_t pos = file_pos_read(f.file);
ret = vfs_read(f.file, buf, count, &pos);
// ...
}
// vfs_read → file->f_op->read_iter → new_sync_read → call_read_iter
// call_read_iter 调用文件系统的具体实现,如 ext4_file_read_iter
文件描述符与内核结构的映射关系
用户空间的 int fd 是进程文件描述符表的下标:
// 进程 → 文件描述符表 → 文件对象
current->files->fdt[fd] // 指向 struct file
每个 struct file 携带了"打开文件的所有上下文":当前偏移量、操作表指针、缓存映射指针。同一个 inode 被打开多次会产生多个不同 offset 的 file 对象,但共享同一个 Page Cache。
第三部分:Page Cache——I/O 性能的核心
地址空间操作表
Page Cache 的本质是一对 (inode, offset) 到 struct page 的映射。每个 inode 通过 i_data (address_space) 管理自己的缓存页:
struct address_space_operations {
int (*readpage)(struct file *, struct page *);
int (*readpages)(struct file *, struct list_head *, unsigned);
int (*writepage)(struct page *, struct writeback_control *);
int (*writepages)(struct address_space *, struct writeback_control *);
int (*write_begin)(struct file *, struct address_space *,
loff_t, unsigned, unsigned, struct page **, void **);
int (*write_end)(struct file *, struct address_space *,
loff_t, unsigned, unsigned, struct page *, void *);
// ...
};
读路径:缺页 → 读磁盘 → 填缓存
read() → vfs_read() → new_sync_read() → ext4_file_read_iter()
→ generic_file_read_iter() → filemap_read()
→ page_cache_sync_readahead() / page_cache_async_readahead()
→ filemap_get_page() // 查找 Page Cache 中是否存在
├── 命中 → copy_to_user() → 完成
└── 未命中 → page_cache_alloc() → readpage() → 发起 bio
写路径:脏页与回写策略
// 用户空间 write() 时的 Page Cache 写入流程
write() → vfs_write() → new_sync_write() → ext4_file_write_iter()
→ generic_perform_write() → a_ops->write_begin()
→ copy_from_user() → a_ops->write_end()
→ balance_dirty_pages_ratelimited() // 脏页比例控制
当脏页比例超过 /proc/sys/vm/dirty_ratio 或 /proc/sys/vm/dirty_background_ratio 时,writeback 线程(wb_workfn)会启动异步回写:
// mm/page-writeback.c
struct wb_writeback_args {
unsigned long nr_pages;
// ...
};
// 回写入口
void wb_workfn(struct work_struct *work)
{
// 遍历 bdi_writeback 链表,回写超期脏页
wb_do_writeback(wb);
}
直接 I/O (O_DIRECT) 的绕过逻辑
使用 O_DIRECT 标志打开文件时:
// 直接 I/O 直接在用户空间和块设备之间传输,不经过 Page Cache
ext4_file_direct_IO() → blockdev_direct_IO()
→ submit_bh() // 直接构造 bio 提交给块层
第四部分:文件系统到块设备的接口
从文件系统到 bio 的转换
文件系统不感知块设备的扇区细节,它通过"逻辑块号"与块层交互:
// ext4 读取时构造 bio 的典型路径
ext4_ext_map_blocks() → 将文件偏移映射到物理块号
ext4_read_bio = bio_alloc() → 设置 bi_bdev, bi_iter.bi_sector
bio->bi_end_io = ext4_end_bio() → submit_bio()
bio 结构——通用块层的核心数据结构
// include/linux/blk_types.h
struct bio {
struct bio *bi_next; // 请求链表
struct block_device *bi_bdev; // 目标块设备
unsigned int bi_opf; // 操作类型 + 标志
struct bvec_iter bi_iter; // 缓冲区向量迭代器(磁盘扇区范围)
struct bio_vec *bi_io_vec; // 实际数据缓冲区数组
bio_end_io_t *bi_end_io; // 完成回调
};
第五部分:通用块层 (Block Layer)
请求合并与 Plug 机制
通用块层在提交 I/O 之前会尝试"相邻请求合并",减少磁盘寻道:
// 合并策略
enum blk_plug_state {
BLK_PLUG_STATE_NOT_PLUGGED = 0,
BLK_PLUG_STATE_PLUGGED,
};
// 用户空间连续写入时,blk_start_plug() → 累积请求
// 写入完成后 blk_finish_plug() → 批量提交
Plug 机制允许将短时间内累积的"相邻块号"请求合并为一个大 bio,减少机械硬盘的寻道开销。对于 SSD 场景,blk-mq 架构下 Plug 的收益相对较小。
blk-mq 多队列架构(现代标准)
自 Linux 4.0 起引入 blk-mq(Multi-Queue Block Layer),核心思想是为每个 CPU 和每个硬件队列建立独立的提交队列,消除单一请求队列的锁竞争:
// 每个硬件队列对应一个 blk_mq_hw_ctx
struct blk_mq_hw_ctx {
struct request_queue *queue;
unsigned int hctx_id; // 硬件队列编号
struct blk_mq_ctx *ctxs; // CPU → 软件队列的映射
struct sbitmap ctx_map; // CPU 分配位图
struct request *dispatch_busy; // 正在处理的请求链表
// ...
};
关键架构优势:
- 软件队列 (submission queue):每个 CPU 一个,无锁提交
- 硬件调度队列 (hardware dispatch queue):映射到 SSD 的并行 submission queue
- msi-X 中断亲和性:每个硬件队列独占一个中断向量,绑定到对应 CPU,避免全局中断风暴
第六部分:I/O 调度器算法与选型
现代调度器对比
| 调度器 | 适用场景 | 延迟控制 | 带宽公平 | 复杂度 |
|---|---|---|---|---|
| none | SSD/NVMe | 极简 | 无 | O(1) |
| mq-deadline | 混合负载 | 读写截止时间 | FIFO + deadline | O(log n) |
| bfq | 桌面/实时 | 低延迟感知 | 权重分配 | O(log n) |
| kyber | 快速 SSD | 自适应 | 无 | O(1) |
mq-deadline 实现原理
// 按扇区号排序的两个红黑树 + 按时间排序的 fifo 队列
struct deadline_data {
struct rb_root_sort fifo_tree[2]; // 读/写按 LBA 排序的 RB-tree
struct list_head fifo_list[2]; // 按时间排序的 FIFO 列表
unsigned int front_merges; // 前端合并计数
unsigned int last_sector; // 上次写入的扇区位置
// ...
};
核心参数:
read_expire:读请求最长等待时间(默认 500ms)write_expire:写请求最长等待时间(默认 5000ms)writes_starved:写请求饥饿时优先处理写的次数(默认 2)fifo_batch:每次出队的 Batch 大小(默认 16)
BFQ 预算分配模型
BFQ (Budget Fair Queuing) 为每个进程分配"预算"(budget = 每次调度分配的扇区数),实现了带宽的公平分配:
// BFQ 的核心调度逻辑
__bfq_dispatch_request() → 从当前活跃 bfqp 取出一个请求
bfq_update_dispatch budget() → 扣除已消耗预算。
// 若预算耗尽,切换到下一个进程队列
第七部分:NVMe 驱动深度剖析
NVMe 队列模型
NVMe 规格要求支持多达 64K 个 I/O Completion Queue (CQ) / Submission_queue 对,每个队列最多 64K 个条目。
// drivers/nvme/host/pci.c
struct nvme_dev {
struct nvme_queue __iomem **dbs; // 寄存器尾部门铃
struct nvme_queue *queues; // SQ/CQ 对数组
unsigned online_queues; // 在线队列数
// ...
};
struct nvme_queue {
struct nvme_command *sq_cmds; // 提交队列(DMA 内存)
struct nvme_completion *cqes; // 完成队列(DMA 内存)
volatile u32 __iomem *sq_db; // 提交门铃寄存器
volatile u32 __iomem *cq_db; // 完成门铃寄存器
// ...
};
提交一个 NVMe 命令的完整流程
blk_mq_dispatch_rq_list()
→ nvme_queue_rq()
→ nvme_setup_cmd()
→ nvme_submit_cmd()
→ memcpy(nvmeq->sq_cmds + tail, cmd, ...) // 写 SQ 条目
→ writel(nvmeq->sq_tail, nvmeq->sq_db) // 敲 SQ 尾部门铃(MMIO)
↓ DMA 传输(PCIe TLPs)
NVMe 控制器发现新命令 → DMA 读取 SQ 条目
↓
控制器执行读/写操作
→ DMA 写入 CQ 条目
→ MSI-X 中断(写 ICRO)
↓
NVMe 中断处理程序:nvme_irq()
→ nvme_pci_complete_batch()
→ blk_mq_complete_request() → bio->bi_end_io() → I/O 完成
中断合并 (Coalescing) 与 Poll Mode
// NVMe 模块参数
static int io_queue_depth = 1024; // 每个 I/O 队列深度
static int poll_queues; // Poll 模式队列数
// Poll 模式:CPU 轮询 CQ 完成队列,消除中断延迟
// 适用于低延迟 NVMe SSD(延迟 < 10μs 级)
第八部分:io_uring——异步 I/O 的革命
环形队列设计
io_uring 由 Jens Axboe 于 2019 年提交,核心创新是用两个无锁环形队列 (ring buffer) 替代传统 AIO 的系统调用:
struct io_uring_sq {
unsigned *head; // 内核更新(指示可消费位置)
unsigned *tail; // 应用更新(指示新提交位置)
unsigned *ring_mask; // 用于快速取模
struct io_uring_sqe *sqes; // 实际 SQE 表(DMA 共享内存)
};
struct io_uring_cq {
unsigned *head; // 应用更新
unsigned *tail; // 内核更新
struct io_uring_cqe *cqes; // 完成队列条目
};
io_uring 的性能优势
传统方式(read/write):
应用 → syscall → VFS → 块层 → syscal 返回 → 完成
每次 I/O = 2 次上下文切换
io_uring 模式:
iosubmit_sq_ring() → 批量提交(无 syscall)
内核消费 SQE → 写入 CQE → 批量收割(可选:IORING_ENTER)
每秒可达数百万 IOPS(仅受限于硬件)
实战配置:
# 创建 io_uring 实例
io_uring_queue_init(QUEUE_DEPTH, &ring, 0);
// 使用 liburing
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, fd, buf, len, offset);
io_uring_submit(&ring);
io_uring_wait_cqe(&ring, &cqe);
第九部分:性能监控与诊断工具集
iostat 深入解读
$ iostat -xz 1
Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s %util await r_await w_await
nvme0n1 1800.5 2350.2 92045 145800 256.3 803.4 98.2 0.26 0.18 0.32
关键指标含义:
- %util:设备利用率(受队列深度和并行能力影响,100% 不一定饱和)
- await:I/O 平均等待时间(排队 + 处理),> 10ms 需关注
- r_await / w_await:分别观察读写延迟
- rrqm/wrqm:合并计数(相邻扇区请求被合并)
blktrace 全链路追踪
# 捕获 raw I/O 事件
blktrace -d /dev/nvme0n1 -o - | blkparse -i -
# 输出示例:
# 8,0 1 915 0.002345 12345 Q WS 10485760 / 10485760 [mysqld]
# 8,0 1 916 0.002400 12345 G WS 10485760 / 10485760 [mysqld]
# 8,0 1 917 0.002500 12345 C WS 0 [0]
# 时间戳解析:Q(入队) → G(get request) → C(完成)
# I/O 完成耗时 = 0.002500 - 0.002345 = 155μs
eBPF/bcc 实战脚本
# 统计延迟分布
biolatency-bcc 1
# 输出:
# usecs : count distribution
# 0 -> 1 : 23 | |
# 2 -> 3 : 112 |** |
# 4 -> 7 : 542 |********** |
# 8 -> 15 : 1023 |******************* |
# ...
ftrace 函数追踪
# 追踪 block 子系统函数
echo 1 > /sys/kernel/debug/tracing/events/block/enable
echo "dev == $(stat -c '%t:%T' /dev/nvme0n1)" > /sys/kernel/debug/tracing/events/block/filter
cat /sys/kernel/debug/tracing/trace
第十部分:调优参数与最佳实践
关键 sysctl 与运行时参数
# Page Cache 回写策略
sysctl -w vm.dirty_ratio=40 # 同步阻塞写入的脏页比例上限
sysctl -w vm.dirty_background_ratio=10 # 后台回写开始的阈值
sysctl -w vm.dirty_expire_centisecs=3000 # 脏页过期时间(30s)
sysctl -w vm.dirty_writeback_centisecs=500 # 回写线程唤醒间隔(5s)
# 块设备预读大小
blockdev --setra 4096 /dev/nvme0n1 # 预读 4096*512B = 2MB
# NVMe 队列参数
echo 1024 > /sys/block/nvme0n1/queue/nr_requests # 请求队列深度
echo 2 > /sys/block/nvme0n1/queue/nr_requests # (注意: 单位为 OS page 数为 512B 的倍数)
# I/O 调度器(SSD 推荐 none/kyber,HDD 推荐 bfq)
echo none > /sys/block/nvme0n1/queue/scheduler
# 最大合并扇区数(相邻扇区合并)
echo 1024 > /sys/block/nvme0n1/queue/max_sectors_kb # 最大 512KB 请求合并
生产环境经验建议
- 高吞吐场景:
dirty_ratio=40, dirty_background_ratio=10, scheduler=none, readahead=4096 - 低延迟数据库:
O_DIRECT + io_uring + scheduler=none + queue_depth=256~1024 - 混合读写:
mq-deadline + read_expire=200, write_expire=2000 - 虚拟化环境:
blk-mq + io_uring + poll_queues=4 - Page Cache 是 I/O 性能的第一道杠杆:命中缓存的读延迟低至 100ns 级,缺页读取则为 ms 级
- VFS 提供抽象,filesystem 提供语义,block layer 提供调度
- blk-mq + io_uring + NVMe 是现代高性能存储的三位一体
- 监控 %util、await、r_await/w_await 是诊断存储瓶颈的第一步
验证与基准测试
# fio 经典测试模板
fio --name=randread --ioengine=io_uring --direct=1 --bs=4k \
--iodepth=128 --size=1G --numjobs=4 --runtime=60 \
--group_reporting --filename=/dev/nvme0n1
# 预期: 4k 随机读 QD128 可达 300K+ IOPS(消费级 NVMe)
总结
Linux I/O Stack 从系统调用的第一行代码到硬件的第一个电信号,经历了至少 8 个层次的数据处理与转换。记住这些关键心智模型:
深入理解这整个链路,是数据库引擎开发、存储系统优化和云原生基础设施调优的必备功底。
参考资料:Linux 内核 6.x 源码 (fs/, block/, drivers/nvme/, mm/)、Jens Axboe io_uring 白皮书、NVMe 2.0 规范

发表评论 取消回复