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 调度器算法与选型

现代调度器对比

调度器适用场景延迟控制带宽公平复杂度
noneSSD/NVMe极简无O(1)
mq-deadline混合负载读写截止时间FIFO + deadlineO(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 请求合并

生产环境经验建议

  1. 高吞吐场景:dirty_ratio=40, dirty_background_ratio=10, scheduler=none, readahead=4096
  2. 低延迟数据库:O_DIRECT + io_uring + scheduler=none + queue_depth=256~1024
  3. 混合读写:mq-deadline + read_expire=200, write_expire=2000
  4. 虚拟化环境:blk-mq + io_uring + poll_queues=4

验证与基准测试

# 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 个层次的数据处理与转换。记住这些关键心智模型:

  • Page Cache 是 I/O 性能的第一道杠杆:命中缓存的读延迟低至 100ns 级,缺页读取则为 ms 级
  • VFS 提供抽象,filesystem 提供语义,block layer 提供调度
  • blk-mq + io_uring + NVMe 是现代高性能存储的三位一体
  • 监控 %util、await、r_await/w_await 是诊断存储瓶颈的第一步

深入理解这整个链路,是数据库引擎开发、存储系统优化和云原生基础设施调优的必备功底。

---

参考资料:Linux 内核 6.x 源码 (fs/, block/, drivers/nvme/, mm/)、Jens Axboe io_uring 白皮书、NVMe 2.0 规范

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿
网站二维码

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部