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 请求合并

生产环境经验建议

  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
  5. 验证与基准测试

    
    # 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 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部