﻿# IO相关指标原理
## 文档概述
本文整合全部沟通流程，区分进程状态定义、内核统计点位、指标统计的时间区间、pidstat数值输出逻辑，通过时序图标注出每一项指标对应的计时区段，厘清`%iowait`、`iodelay`、`cswch/s`、`%wait`四项核心指标的统计范围，解决如下历史疑问：
1. iodelay为累计绝对值，pidstat采样输出原始总值而非区间增量的底层逻辑
2. D态不可中断睡眠、S态可中断睡眠各自对应的统计指标，为什么网络IO不会产生两项磁盘类IO指标
3. 两种经典场景：CPU存在就绪进程 / CPU无就绪进程，iowait与iodelay出现数值分化的根源
4. 自愿上下文切换、非自愿上下文切换的计时区间划分

## 一、前置基础定义（梳理全部进程调度状态）
### 1. R TASK_RUNNING 运行/就绪态
进程分为两种形态：正在CPU上执行代码、处于运行队列等待CPU调度。
- 占用CPU的时段统计到usr用户CPU时间、sys内核CPU时间；
- 已经就绪却无法分配CPU时间片，排队耗时统计至`%wait`、非自愿上下文切换`nvcswch/s`；
- **该状态不会触发iodelay、%iowait统计**。

### 2. S TASK_INTERRUPTIBLE 可中断睡眠
触发场景：阻塞式Socket网络读写、互斥锁等待、epoll事件、管道、用户输入。
1. 进程主动让出CPU，进入睡眠，可以被信号唤醒；
2. 内核**不会标记in_iowait**，delayacct计时器不会启动，全程不统计iodelay；
3. CPU空闲时段直接划归`%idle`，永远不会计入%iowait；
4. 进程从运行态转入睡眠的动作，统计为**自愿上下文切换cswch/s**，统计区间为进程主动放弃CPU直至被内核唤醒的整段睡眠时间。

### 3. D TASK_UNINTERRUPTIBLE 不可中断睡眠
触发场景：页缓存缺失产生磁盘读、fsync/O_DIRECT同步落盘、Swap内存置换。
1. 进程陷入内核原子IO流程，kill信号无法打断阻塞；内核给进程打上`in_iowait`标记；
2. delayacct框架开启计时器，**从进入D态一刻开始计时，退出D态停止计时，这段时长累加进进程全局iodelay计数器（单位jiffies时钟节拍）**；
3. 只有CPU找不到任意就绪R进程时，CPU的空闲周期才会计入%iowait。

### 4. 核心两项IO指标底层定义，标注各自统计时域
#### （1）%iowait（CPU核心维度，vmstat/mpstat/top）
统计时域：**CPU处于空闲周期，且系统内部存在携带in_iowait标记的D态进程的这段CPU空闲时间**
必要条件必须同时成立：
1. CPU运行队列不存在就绪态R进程，CPU进入空闲；
2. 系统中至少一个进程阻塞在块设备IO（磁盘、Swap）。
只要CPU存在可调度的R进程，无论多少进程卡在D态，该段CPU时间全部计入usr/sys，%iowait数值为0。

#### （2）iodelay（进程维度，pidstat -d，内核delayacct框架）
统计时域：**单个进程每次进入D态开始，直到IO硬件返回、进程被唤醒退出D态的全部阻塞时长**
关键数值特性
1. 内核PCB中`blkio_delay_total`是进程启动之后**永久累加的绝对累计值**，进程不重启计数器不会清零；
2. 执行`pidstat -d N`周期性采样时，pidstat内部会计算两次采样点之间的增量阻塞节拍，但是终端展示字段直接打印内核原始累计总值，因此持续发生D态阻塞，每一行输出数值持续走高；进程不再触发磁盘阻塞，输出数值维持持平；
3. 统计逻辑和CPU繁忙程度完全解耦，无论CPU正在调度其他业务进程还是CPU空闲，只要进程处在D态，计时不会暂停。

### 5. 两项调度切换指标统计时域
1. cswch/s 自愿上下文切换：进程主动放弃CPU进入S态睡眠的瞬间产生一次统计，统计背后对应的时间区间为本次资源阻塞的全部时长；
2. nvcswch/s 非自愿上下文切换 + pidstat %wait：进程已经处于R就绪态，CPU时间片耗尽或者CPU资源被抢占，进程滞留在调度队列的排队耗时，和磁盘IO无关联。

## 二、四大业务场景时序图【全部语法修复，可直接渲染】
### 场景一：进程A循环fsync进入D态，进程B为CPU密集型任务（CPU始终繁忙，iodelay上涨，%iowait=0）
```plantuml
@startuml 场景1 D态磁盘IO+CPU存在就绪进程
!pragma teoz true
actor "进程A[循环fsync，进入D态]" as A
actor "进程B[纯数值运算，常驻R]" as B
participant "Linux内核" as kernel
participant "delayacct IO计时器" as delay
participant "物理磁盘" as disk
participant "CPU核心" as cpu
participant "pidstat采样工具" as pidstat
A -> kernel: 调用fsync，发起同步块设备IO
kernel -> A: 设置TASK_UNINTERRUPTIBLE(D)，标记in_iowait
{start_mark} kernel -> delay: 启动计时
kernel -> cpu: 进程A让出CPU调度队列
note over cpu
运行队列存在就绪R进程B
CPU时间计入usr/sys，不满足iowait条件，%iowait=0
end note
cpu -> B: 持续分配时间片执行运算
disk --> kernel: 磁盘IO完成
{end_mark} kernel -> delay: 终止计时，时长累加至进程累计计数器 
{start_mark} <-> {end_mark}: iodelay统计窗口\n不关心CPU是否繁忙空闲
kernel -> A: 切换为R运行态
pidstat -> delay: 读取内核累计绝对值并打印
note over pidstat
pidstat内部可计算区间增量，但终端输出原始累加总值
end note
loop 循环执行fsync
A -> kernel: 重复发起同步磁盘IO
end loop
@enduml
```

**场景一分析**：
- **iodelay 统计窗口**：`{start_mark}` → `{end_mark}`，即进程 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两项指标同步上涨）
```plantuml
@startuml 场景2 D态磁盘IO+CPU完全空闲
!pragma teoz true
actor "进程A[唯一业务进程 fsync]" as A
participant "Linux内核" as kernel
participant "delayacct IO计时器" as delay
participant "物理磁盘" as disk
participant "CPU核心" as cpu
participant "pidstat采样工具" as pidstat
A -> kernel: 调用fsync请求同步落盘
kernel -> A: 标记TASK_UNINTERRUPTIBLE(D)、in_iowait
{start_mark} kernel -> delay: 开启iodelay计时
kernel -> cpu: 运行队列无R就绪进程，CPU进入空闲
note over cpu
CPU空闲 && 存在in_iowait进程
空闲时间统计进CPU指标%iowait
end note
disk --> kernel: IO请求处理完毕
{end_mark} kernel -> delay: 计时结束，时长累加至全局计数器 
{start_mark} <-> {end_mark}: 计入进程iodelay及CPU iowait
kernel -> A: 进程切换为R态
note over cpu
进程A回到R态，in_iowait标记清除
CPU空闲周期结束，%iowait停止累加
end note
pidstat -> delay: 读取累计绝对值，数值上涨
loop 循环fsync调用
A -> kernel: 重复发起同步IO
end loop
@enduml
```

**场景二分析**：
- **iodelay 统计窗口**：`{start_mark}` → `{end_mark}`，进程 A 从进入 D 态到 IO 完成退出 D 态的整段时长，delayacct 持续累加。
- **%iowait 统计窗口**：`{start_mark}` → `{end_mark}`，与 iodelay 窗口**几乎完全重合**。原因：CPU 运行队列为空，进程 A 进入 D 态后 CPU 立即空闲，且系统存在 `in_iowait` 标记——两个条件同时满足，整段 CPU 空闲时间全部计入 %iowait。
- **关键结论**：这是 iodelay 与 %iowait **同步上涨**的唯一场景——系统负载极低、仅有一个（或少数几个）进程卡在磁盘 IO 上。此时 %iowait 高确实意味着磁盘慢，排查路径直接有效。

### 场景三：进程阻塞TCP网络IO，进入S可中断睡眠（iodelay、%iowait全程无变化，仅自愿上下文切换cswch/s上涨）
```plantuml
@startuml 场景3 S态网络Socket阻塞 Redis read
!pragma teoz true
actor "业务进程" as app
participant "Linux内核" as kernel
participant "TCP接收缓冲区" as tcp_buf
participant "物理网卡" as nic
participant "CPU核心" as cpu
app -> kernel: 阻塞read读取Redis套接字
kernel -> tcp_buf: 查询缓冲区，无可用报文
kernel -> app: 设置TASK_INTERRUPTIBLE(S)，不打in_iowait标记
kernel -> cpu: {start_mark} 进程主动让出CPU，产生自愿上下文切换cswch
nic --> kernel: 收到TCP数据包，触发硬件中断
kernel -> app: 唤醒进程至R就绪态 {end_mark}
@enduml
```

**场景三分析**：
- **cswch/s 统计窗口**：`{start_mark}` → `{end_mark}`，进程从让出 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指标均不产生统计
```plantuml
@startuml 场景4 页缓存写入，无同步刷盘
actor "业务进程" as app
participant "Linux内核" as kernel
participant "PageCache页缓存" as pagecache
participant "CPU核心" as cpu
app -> kernel: write()写入本地文件
kernel -> pagecache: 数据写入内存缓存，立刻返回
note over app
write调用快速返回，进程持续处于R态
后台内核线程异步刷盘，不阻塞业务进程
end note
note over app, cpu
全程无 {start_mark} ... {end_mark} 标记：
进程未进入S/D睡眠态，所有IO指标统计窗口均不开启
end note
@enduml
```

**场景四分析**：
- **统计窗口**：**不存在任何 `{start_mark}` → `{end_mark}` 区间**。`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数值逻辑专项复盘
1. 内核侧：`blkio_delay_total`属于进程生命周期累计计时器，**统计每一段D态阻塞的全部时间，持续叠加，不会自动清零**，该数值为原始绝对值；
2. pidstat采样逻辑：程序在两次采样节点分别读取内核累计数值，内部可以计算两次采样之间新增的阻塞时长，但是终端输出字段直接打印第二次采样的绝对总值；
3. 现象解释：进程不断触发D态磁盘阻塞，每一次采样读出的累计数值不断走高；进程不再出现不可中断睡眠，两次采样读取到的计数器数值一致，输出结果持平；
4. 区分速率类字段：`kB_rd/s`、`kB_wr/s`这类字段会直接输出采样区间的每秒增量，负载稳定后读数基本持平，和iodelay累计值的实现逻辑有着本质区别。

## 五、/proc 文件系统指标补充（新增章节，承接后续讨论）
### 核心分类
1. **全局累计计数器（开机起累加，重启清零）**
`/proc/stat` CPU时间、`/proc/diskstats`磁盘统计；
工具两次采样差值计算使用率、%iowait。

2. **进程私有累计计数器（进程创建开始累加，进程销毁丢失）**
`/proc/$PID/stat` utime/stime/cswch/nvcswch
`blkio_delay_total(iodelay)` 数据源；
pidstat读取**原始总和**，不自动输出增量。

3. **瞬时快照（非累加，实时状态）**
`VmRSS`、打开fd数量、loadavg，数值可升可降。

> 重要结论：内核只存放原始累计tick/计数；**所有速率、百分比都是用户态工具两次采样自行计算，内核不预先计算**。

## 六、线上故障排查标准流程
1. `vmstat 1`：查看整机CPU维度的%iowait
2. `mpstat -P ALL 1`：拆分各个CPU核心，定位出现iowait的内核
3. `pidstat -d 1`：查看各个进程iodelay累计数值，定位D态磁盘阻塞进程
4. `pidstat -w 1`：观测上下文切换，区分磁盘阻塞 / 网络锁阻塞
5. `lsof -p PID`：区分本地块设备IO还是TCP套接字

## 七、十条不可推翻内核调度铁律
1. %iowait统计载体是CPU；iodelay统计载体是进程，二者主体完全隔离。
2. S态可中断睡眠无论阻塞多久，永远不产生iowait、iodelay。
3. D态进程一定会累积iodelay；仅当CPU无就绪R进程时，才会抬升%iowait。
4. iodelay为进程全局累计绝对值，pidstat展示原始读数，并非采样区间增量。
5. 自愿上下文切换 = 主动放弃CPU(S态)；非自愿切换 = 抢不到CPU(R排队)。
6. PageCache缓冲write不会阻塞用户进程，进程维持R态，IO指标无变动。
7. iowait低但业务慢，优先排查进程iodelay（典型隐蔽线上故障）。
8. `in_iowait`标记是内核判定能否计入iowait的唯一标识，仅附加在块设备IO的D进程。
9. 底层计时单位统一为jiffies（时钟节拍）。
10. 需要区间新增IO延迟，必须人工对两次pidstat iodelay数值做减法。
