IO相关指标原理
下一篇 disk-io 下沉到设备维度(
iostat -x各列含义)。
文档概述
本文整合全部沟通流程,区分进程状态定义、内核统计点位、指标统计的时间区间、pidstat数值输出逻辑,通过时序图标注出每一项指标对应的计时区段,厘清%iowait、iodelay、cswch/s、%wait四项核心指标的统计范围,解决如下历史疑问:
- iodelay为累计绝对值,pidstat采样输出原始总值而非区间增量的底层逻辑
- D态不可中断睡眠、S态可中断睡眠各自对应的统计指标,为什么网络IO不会产生两项磁盘类IO指标
- 两种经典场景:CPU存在就绪进程 / CPU无就绪进程,iowait与iodelay出现数值分化的根源
- 自愿上下文切换、非自愿上下文切换的计时区间划分
一、前置基础定义(梳理全部进程调度状态)
1. R TASK_RUNNING 运行/就绪态
进程分为两种形态:正在CPU上执行代码、处于运行队列等待CPU调度。
- 占用CPU的时段统计到usr用户CPU时间、sys内核CPU时间;
- 已经就绪却无法分配CPU时间片,排队耗时统计至
%wait、非自愿上下文切换nvcswch/s; - 该状态不会触发iodelay、%iowait统计。
2. S TASK_INTERRUPTIBLE 可中断睡眠
触发场景:阻塞式Socket网络读写、互斥锁等待、epoll事件、管道、用户输入。
- 进程主动让出CPU,进入睡眠,可以被信号唤醒;
- 内核不会标记in_iowait,delayacct计时器不会启动,全程不统计iodelay;
- CPU空闲时段直接划归
%idle,永远不会计入%iowait; - 进程从运行态转入睡眠的动作,统计为自愿上下文切换cswch/s,统计区间为进程主动放弃CPU直至被内核唤醒的整段睡眠时间。
3. D TASK_UNINTERRUPTIBLE 不可中断睡眠
触发场景:页缓存缺失产生磁盘读、fsync/O_DIRECT同步落盘、Swap内存置换。
- 进程陷入内核原子IO流程,kill信号无法打断阻塞;内核给进程打上
in_iowait标记; - delayacct框架开启计时器,从进入D态一刻开始计时,退出D态停止计时,这段时长累加进进程全局iodelay计数器(单位jiffies时钟节拍);
- 只有CPU找不到任意就绪R进程时,CPU的空闲周期才会计入%iowait。
4. 核心两项IO指标底层定义,标注各自统计时域
(1)%iowait(CPU核心维度,vmstat/mpstat/top)
统计时域:CPU处于空闲周期,且系统内部存在携带in_iowait标记的D态进程的这段CPU空闲时间 必要条件必须同时成立:
- CPU运行队列不存在就绪态R进程,CPU进入空闲;
- 系统中至少一个进程阻塞在块设备IO(磁盘、Swap)。 只要CPU存在可调度的R进程,无论多少进程卡在D态,该段CPU时间全部计入usr/sys,%iowait数值为0。
(2)iodelay(进程维度,pidstat -d,内核delayacct框架)
统计时域:单个进程每次进入D态开始,直到IO硬件返回、进程被唤醒退出D态的全部阻塞时长 关键数值特性
- 内核PCB中
blkio_delay_total是进程启动之后永久累加的绝对累计值,进程不重启计数器不会清零; - 执行
pidstat -d N周期性采样时,pidstat内部会计算两次采样点之间的增量阻塞节拍,但是终端展示字段直接打印内核原始累计总值,因此持续发生D态阻塞,每一行输出数值持续走高;进程不再触发磁盘阻塞,输出数值维持持平; - 统计逻辑和CPU繁忙程度完全解耦,无论CPU正在调度其他业务进程还是CPU空闲,只要进程处在D态,计时不会暂停。
5. 两项调度切换指标统计时域
- cswch/s 自愿上下文切换:进程主动放弃CPU进入S态睡眠的瞬间产生一次统计,统计背后对应的时间区间为本次资源阻塞的全部时长;
- nvcswch/s 非自愿上下文切换 + pidstat %wait:进程已经处于R就绪态,CPU时间片耗尽或者CPU资源被抢占,进程滞留在调度队列的排队耗时,和磁盘IO无关联。
二、四大业务场景时序图
场景一:进程A循环fsync进入D态,进程B为CPU密集型任务(CPU始终繁忙,iodelay上涨,%iowait=0)

场景一分析:
- iodelay 统计窗口:
t1→t2,即进程 A 进入 D 态到磁盘 IO 完成退出 D 态的整段阻塞时长。无论 CPU 此时在干什么,delayacct 计时器持续累加,不受 CPU 繁忙程度影响。 - %iowait 统计窗口:不存在。虽然系统中存在携带
in_iowait标记的 D 态进程 A,但 CPU 运行队列中始终有就绪进程 B 待调度,CPU 不会进入空闲周期——因此 %iowait 条件不成立,数值为 0。 - 关键结论:iodelay 与 %iowait 在 CPU 繁忙场景下完全解耦——iodelay 照涨不误,%iowait 恒为 0。这是线上最隐蔽的场景:磁盘已经慢了(iodelay 高),但 %iowait 看起来一切正常。
场景二:系统仅保留进程A,不存在其他就绪进程(iodelay、%iowait两项指标同步上涨)

场景二分析:
- iodelay 统计窗口:
t1→t2,进程 A 从进入 D 态到 IO 完成退出 D 态的整段时长,delayacct 持续累加。 - %iowait 统计窗口:
t1→t2,与 iodelay 窗口几乎完全重合。原因:CPU 运行队列为空,进程 A 进入 D 态后 CPU 立即空闲,且系统存在in_iowait标记——两个条件同时满足,整段 CPU 空闲时间全部计入 %iowait。 - 关键结论:这是 iodelay 与 %iowait 同步上涨的唯一场景——系统负载极低、仅有一个(或少数几个)进程卡在磁盘 IO 上。此时 %iowait 高确实意味着磁盘慢,排查路径直接有效。
场景三:进程阻塞TCP网络IO,进入S可中断睡眠(iodelay、%iowait全程无变化,仅自愿上下文切换cswch/s上涨)

场景三分析:
- cswch/s 统计窗口:
t1→t2,进程从让出 CPU 进入 S 态睡眠,到网卡收到数据、内核唤醒进程回到 R 就绪态——这整段时长就是一次自愿上下文切换的"阻塞区间"。 - iodelay 统计窗口:不存在。进程进入的是 S 态(可中断睡眠),内核不会打
in_iowait标记,delayacct 计时器不启动。无论阻塞多久,iodelay 数值不变。 - %iowait 统计窗口:不存在。即使 CPU 在这期间空闲,因为系统内没有携带
in_iowait标记的进程,CPU 空闲时间计入%idle,不会计入 %iowait。 - 关键结论:网络 IO 阻塞只涨 cswch/s,不动 iodelay 和 %iowait。这是区分"磁盘慢"还是"网络/锁等待"的关键判别点——看到 cswch/s 高但 iodelay/%iowait 不变,排查方向应转向网络或应用层锁。
场景四:普通write写入PageCache,未调用fsync强制刷盘,全程R运行态,所有IO指标均不产生统计

场景四分析:
- 统计窗口:不存在任何
t1→t2区间。write()写入 PageCache 是纯内存操作,数据拷贝到内核页缓存后立即返回,进程全程保持在 R 运行态,不会进入 S 或 D 睡眠态。 - iodelay:进程未进入 D 态,delayacct 计时器从未启动,数值不变。
- %iowait:进程未进入 D 态,
in_iowait标记不存在,即使 CPU 空闲也不会触发 %iowait 条件。 - cswch/s:进程未主动让出 CPU(没有进入 S 态睡眠),不会产生自愿上下文切换。
- 关键结论:PageCache 缓冲写入对业务进程完全透明——它不阻塞、不产生任何 IO 等待指标。但这不代表数据已经安全落盘,后台
kworker/flush内核线程的刷盘行为在进程维度不可见。真正需要同步持久化的场景(如数据库 WAL),必须调用fsync/fdatasync/O_DIRECT,届时才会触发场景一或场景二的 D 态阻塞。
三、汇总对照表
| 阻塞类型 | 进程状态 | 指标统计对应时间区间 | iodelay | %iowait | cswch自愿切换 | %wait非自愿切换 |
|---|---|---|---|---|---|---|
| Socket网络、锁、epoll阻塞 | S可中断睡眠 | 进程让出CPU到被事件唤醒的全部睡眠时长,统计自愿上下文切换 | 无统计区间,数值不变 | CPU空闲计入idle,数值恒为0 | 上涨 | 无变化 |
| 磁盘IO、缺页、Swap,系统存在可运行进程 | D不可中断睡眠 | 进程进入D态直至IO返回的阻塞时段计入iodelay;CPU持续执行业务,不存在iowait统计时域 | 持续累加上涨 | 统计条件不满足,数值为0 | 无变化 | 无变化 |
| 磁盘IO、缺页、Swap,系统无就绪进程 | D不可中断睡眠 | 进程阻塞时段统计iodelay;CPU空闲时段同时划入iowait统计范围 | 持续累加上涨 | 同步上涨 | 无变化 | 无变化 |
| 普通write写入页缓存,无fsync | R运行态 | 不存在任何睡眠阻塞时域 | 数值不变 | 数值不变 | 数值不变 | 数值不变 |
| 进程就绪但是抢占不到CPU时间片 | R就绪排队 | 进程滞留在运行队列的等待时段统计%wait、非自愿上下文切换 | 数值不变 | 数值不变 | 数值不变 | 上涨 |
四、pidstat iodelay数值逻辑专项复盘
- 内核侧:
blkio_delay_total属于进程生命周期累计计时器,统计每一段D态阻塞的全部时间,持续叠加,不会自动清零,该数值为原始绝对值; - pidstat采样逻辑:程序在两次采样节点分别读取内核累计数值,内部可以计算两次采样之间新增的阻塞时长,但是终端输出字段直接打印第二次采样的绝对总值;
- 现象解释:进程不断触发D态磁盘阻塞,每一次采样读出的累计数值不断走高;进程不再出现不可中断睡眠,两次采样读取到的计数器数值一致,输出结果持平;
- 区分速率类字段:
kB_rd/s、kB_wr/s这类字段会直接输出采样区间的每秒增量,负载稳定后读数基本持平,和iodelay累计值的实现逻辑有着本质区别。
五、/proc 文件系统指标补充(新增章节,承接后续讨论)
核心分类
全局累计计数器(开机起累加,重启清零)
/proc/statCPU时间、/proc/diskstats磁盘统计; 工具两次采样差值计算使用率、%iowait。进程私有累计计数器(进程创建开始累加,进程销毁丢失)
/proc/$PID/statutime/stime/cswch/nvcswchblkio_delay_total(iodelay)数据源; pidstat读取原始总和,不自动输出增量。瞬时快照(非累加,实时状态)
VmRSS、打开fd数量、loadavg,数值可升可降。
重要结论:内核只存放原始累计tick/计数;所有速率、百分比都是用户态工具两次采样自行计算,内核不预先计算。
六、线上故障排查标准流程
vmstat 1:查看整机CPU维度的%iowaitmpstat -P ALL 1:拆分各个CPU核心,定位出现iowait的内核pidstat -d 1:查看各个进程iodelay累计数值,定位D态磁盘阻塞进程pidstat -w 1:观测上下文切换,区分磁盘阻塞 / 网络锁阻塞lsof -p PID:区分本地块设备IO还是TCP套接字
七、十条不可推翻内核调度铁律
- %iowait统计载体是CPU;iodelay统计载体是进程,二者主体完全隔离。
- S态可中断睡眠无论阻塞多久,永远不产生iowait、iodelay。
- D态进程一定会累积iodelay;仅当CPU无就绪R进程时,才会抬升%iowait。
- iodelay为进程全局累计绝对值,pidstat展示原始读数,并非采样区间增量。
- 自愿上下文切换 = 主动放弃CPU(S态);非自愿切换 = 抢不到CPU(R排队)。
- PageCache缓冲write不会阻塞用户进程,进程维持R态,IO指标无变动。
- iowait低但业务慢,优先排查进程iodelay(典型隐蔽线上故障)。
in_iowait标记是内核判定能否计入iowait的唯一标识,仅附加在块设备IO的D进程。- 底层计时单位统一为jiffies(时钟节拍)。
- 需要区间新增IO延迟,必须人工对两次pidstat iodelay数值做减法。
总结
%iowait 和 iodelay 是两个主体完全不同的量:前者统计“CPU 空闲且系统存在带 in_iowait 标记的 D 态进程”的 CPU 时间(只要有就绪 R 进程就为 0),后者是 delayacct 对进程每次 D 态阻塞时长的永久累计绝对值、与 CPU 忙闲无关,pidstat 展示原始总值、要看增量须人工做差。网络 IO 只走 S 态、涨自愿切换不产生 iowait/iodelay,这是区分磁盘瓶颈与网络/锁等待的关键判据。