Linux I/O写数据全链路拆解
本文档详细拆解 Linux 系统中"数据写入"的全过程。从应用程序发起调用,经过内核各层级的处理,最终抵达物理硬件。理解这一链路对于排查 IO 高负载、优化数据库性能、以及理解数据一致性至关重要。
主要流程分为两个大阶段:
- 前台阶段:应用发起写操作,数据写入到内存(很快)。
- 后台阶段:脏页回写,数据持久化到磁盘(较慢)。
第一阶段:从用户空间到Page Cache(前台异步写)
这个阶段通常由程序触发,不涉及磁盘硬件操作,瞬间完成。
- 用户空间:发起请求
- 动作:程序调用 write()函数。
- 内存状态 :
- 用户态 buffer:数据目前放在进程的私有虚拟内存地址中。
- CPU:指令执行,从用户态切换到内核态,触发中断进入内核。
- 文件系统:内存拷贝
- 接单:sys_write-->vfs_write-->具体的文件系统
- 内存分配 :
- 内核在 page cache 中查找对应的页
- 如果没找到,内核向伙伴系统申请分配一个内存页
- 关键动作:CPU 亲自将数据将用户态 buffer 拷贝到内核态的 page cache 的物理页中。
- 状态标记 :
- 该page cache 被标记为脏页
- inode 会记录该页的状态
- 返回 :
- 此时,write()调用结束,cpu 切换为用户态
- 程序认为"写完了",继续执行下面的代码
第二阶段:从page cache到物理磁盘
这个阶段通常由内核线程或者 fsync 触发,真正的 I/O 旅行由此开始。
-
文件系统层:寻址与打包
- 触发:回写机制启动(达到了预期内的系统脏页率,或者定时器到期)
- 动作:文件系统开始处理脏页
- 逻辑转换
- 查账:查询 inode,找到文件偏移量 offset 0 对应的磁盘逻辑块号(LBA)(例如 LBA #1000)
- 内存指派:文件系统创建个 bio 逻辑体
- bio 填充:目标磁盘位置和大小、page cache 的物理内存地址、写操作、
- 提交:将 bio 包裹扔给通用块层
-
通用块层:调度与队列
- 接单:bio 包裹进入到请求队列
- 内存操作:不涉及到数据拷贝,只操作 bio 结构体的指针
- 动作 :
- 合并:检查队列里是否有 LBA#999 的请求,如果有,就把现在的 LBA#1000拼接到他的后面,形成一个大的请求
- 调度:如果是 HDD,按照电梯算法排序,如果是 NVMe 就直接透传
- 分发:如果是多队列,根据 cpu ID 将请求放到对应的队列
-
设备驱动层:翻译与建图
- 接单:NVMe 驱动从软件中队列中提取请求
- 翻译:将通用的请求转换为 NVMe Write Command
- 关键动作(DMA Mapping)
- 硬件不懂虚拟地址,也不懂 page cache 的物理页是不连续的
- 驱动建立一个散列表(SGL:Scatter Gather Lsit)
- 内容:告诉硬件,数据在内存中的真实地址
- 这个 SGL 的地址会被填入到 NVMe 的命令当中
- 入列:将组装好的 NVMe 命令写入到内存中的提交队列
-
硬件交互:按门铃
- 动作:驱动程序执行 MMIO 写操作
- 细节:cpu 向 NVMe 控制器的 Doorbell 寄存器写入一个新的 Tail 指针
- 含义:CPU 告诉 SSD 控制器,"SQ 队列中有新任务了,开起来干活!"
-
硬件层:DMA(Direct Memory Access) 搬运与落盘
- 接单:SSD 控制器检测到 Doorbell 变化,从内存中的 SQ 队列中抓取命令。
- 解析:控制器读取命令,拿到了SGL(内存地址表)
- 数据搬运 :
- 脱离 CPU:SSD 的 DMA 引擎启动
- 总线传输:DMA 直接通过 PCIe 总线,从主机的 page cache 中把数据传输到 SSD 的内部 DRAM 缓存中。
- 落盘:SSD 控制器通过 FTL 映射算法,将 DRAM 缓存中的数据写入到 NAND Flash 颗粒
-
中断与回调:结束
- 硬件动作:数据传输完成后,SSD 控制器往内存中的完成队列写一个条目,并触发 MSI_X 中断
- CPU 响应:CPU 暂停处理当前任务,跳转到驱动注册的 ISR(中断处理程序)
- 驱动层 :
- 读取 CQ:确认命令执行成功
- 通知块层:回应完成
- 通用块层:拆解通知包,找到 原始bio,结束这个 I/O
- 文件系统 :
- 解除脏标记:将 page cache 中对应的页面的 PG_dirty 标记清除,变为 PG_clean。
- 释放资源:如果需要,将释放相关的 bio 结构体内存