﻿# pidstat —— 按进程/线程精准拆分 CPU、IO、上下文切换

## 这个工具是做什么的

`pidstat` 是 `sysstat` 套件的一员，性能排查标配。相比 `top`，它的优势是**按固定间隔持续采样、把 CPU 精确拆成用户态/内核态/IO 等待，并能下钻到线程**，输出是纯文本行，天然适合记录和脚本解析。

## 可以回答什么问题

| 问题 | 怎么用 |
|------|--------|
| 某个进程的 CPU 主要耗在用户态还是内核态？ | `pidstat -u -p <PID> 1` 看 `%usr` vs `%system` |
| 多线程程序中哪个线程在烧 CPU？ | `pidstat -ut -p <PID> 1` 展开到各个 TID |
| 进程在频繁做 IO 操作吗？ | `pidstat -d -p <PID> 1` 看 `kB_wr/s`、`iodelay` |
| 上下文切换多不多？是等资源还是被抢占？ | `pidstat -w -p <PID> 1` 区分 `cswch/s`（自愿）和 `nvcswch/s`（非自愿） |
| 有没有发生主缺页（触发磁盘 IO）？ | `pidstat -r -p <PID> 1` 看 `majflt/s` |
| 线程在不同核心间迁移了吗？ | `pidstat -u -p <PID> 1` 看 `CPU` 列是否频繁跳变 |

## 数据来源

- **来源文件**：CPU 拆分取自 `/proc/<pid>/stat`，线程级取自 `/proc/<pid>/task/<tid>/stat`，磁盘读写取自 `/proc/<pid>/io`，上下文切换与内存取自 `/proc/<pid>/status`。
- **采集方式**：按你指定的间隔（如 `1` 秒）把这些文件读两次，用累计值**做差**再除以间隔，得到速率（如 `kB_wr/s`、`cswch/s`）和百分比（`%usr`/`%system`）。
- **由此决定的特性**：不给间隔只读一次时，输出的是**开机以来的平均值**，没有实时意义——所以排查时务必带上间隔参数。

```bash
# 安装
yum install sysstat        # CentOS/RHEL
apt install sysstat        # Debian/Ubuntu
pidstat -V                 # 查看版本（不同版本列名略有差异）
```

## 一、命令结构

```bash
pidstat [选项] [间隔] [次数]
```

- 不给间隔：只输出一次自开机以来的**平均值**（参考意义小）。
- `pidstat 1`：每 1 秒输出一次增量，一直刷。
- `pidstat 1 5`：每秒一次，共 5 次后退出。
- `-p <PID>`：只看指定进程；`-p ALL` 看全部（含空闲）；`-p SELF` 看 pidstat 自己。
- 支持小数间隔：`pidstat -u -p <PID> 0.4`（每 400ms，观察更细腻）。

## 二、CPU：区分用户态 / 内核态（`-u`）

```bash
pidstat -u -p $(pgrep cpu_demo) 1
```

真实输出（CentOS 7 / sysstat 10.1.5，跑 cpu_demo 场景2）：

```bash
Linux 3.10.0-1160.el7.x86_64 (VM-0-17-centos)  2026年07月04日  _x86_64_  (4 CPU)
00时04分11秒   UID     PID    %usr %system  %guest    %CPU   CPU  Command
00时04分12秒  1001    4043   34.00   66.00    0.00  100.00     1  cpu_demo
00时04分13秒  1001    4043   31.00   68.00    0.00   99.00     2  cpu_demo
00时04分14秒  1001    4043   34.00   66.00    0.00  100.00     2  cpu_demo
```

**这段怎么读**：`%system` 稳定 66% 左右（内核态占大头）、`%usr` 34%、`%CPU` 接近 100% —— 典型的**场景2（频繁 open/write/close 系统调用）**特征，内核态远高于用户态。`CPU` 列在 1/2/3 之间跳，说明该线程被调度器在不同核间迁移。

| 字段 | 含义 | 判读 |
|------|------|------|
| `%usr` | 用户态 CPU | 业务计算、循环、加解密、序列化 → cpu_demo 场景1 拉满 |
| `%system` | 内核态 CPU | read/write/open、锁竞争、malloc、缺页 → cpu_demo 场景2 偏高（如上例 66%）|
| `%guest` | 运行虚拟机 vCPU 的时间 | 宿主机上跑 VM 才有，普通程序为 0 |
| `%wait` | 进程在**运行队列里等 CPU** 的时间 | **仅新版有此列**（见下方版本说明）。高 = CPU 不够/被抢占激烈；注意这不是 iowait |
| `%CPU` | 该进程 CPU 总占用 | 多核可 >100%（按核累加）|
| `CPU` | 最后运行在哪个逻辑核 | 频繁跳核（如上例 1→2→3）说明线程在核间迁移，可能影响 cache 命中 |

> ⚠️ **版本差异——`%wait` 列可能没有**：`%wait` 是 sysstat 11.5.5（约 2016 年）之后才加入的。**CentOS 7 自带的 sysstat 10.1.5 输出里没有 `%wait`**（如上面真实输出），只有 `%usr/%system/%guest/%CPU`。若需要看进程等 CPU 调度的时间，得升级 sysstat，或改用 `top`(`%wa` 是整机 iowait，含义不同) / `vmstat` 的 `r` 队列间接判断。用 `pidstat -V` 查本机版本。

> ⚠️ 别混两个"wait"：pidstat 的 `%wait`（新版才有）是**等 CPU 调度**；`top`/`mpstat`/`iostat` 里的 `%iowait` 是**CPU 等 IO**。二者完全不同。

## 三、线程级下钻（`-t`）

```bash
pidstat -ut -p $(pgrep cpu_demo) 1
```

```bash
15:22:10      UID      TGID       TID    %usr %system   %CPU   CPU  Command
15:22:11     1000      8921         -    0.00    0.00  99.00     3  cpu_demo
15:22:11     1000         -      8922   99.00    0.00  99.00     3  |__cpu_demo
```

- `TGID` 是进程号，`TID` 是线程号；`TGID` 行是进程汇总，`TID` 行（`|__` 缩进）是各线程。
- cpu_demo 的负载跑在 `detach` 出来的子线程里，**只有加 `-t` 才能看到是那个 TID 在烧 CPU**，进程汇总行看不出线程分布。
- 定位到 TID 后，可用 `perf top -t <TID>` 或在 gdb 里 `thread apply` 进一步看这个线程在干嘛。

## 四、磁盘 IO（`-d`）

```bash
pidstat -d -p <PID> 1
```

| 字段 | 含义 |
|------|------|
| `kB_rd/s` | 每秒从磁盘**实际读取** KB（已扣除页缓存命中的部分）|
| `kB_wr/s` | 每秒写入磁盘 KB |
| `kB_ccwr/s` | 每秒被取消的写（如写了又被 truncate）|
| `iodelay` | 该进程的块 IO 延迟（单位 jiffies，越大越卡）|

> cpu_demo 场景2 疯狂 `ofstream` 写文件，这里 `kB_wr/s` 会明显非 0；配合 `iostat -x` 看是不是把盘写满了。

## 五、上下文切换（`-w`）

```bash
pidstat -w -p <PID> 1
```

| 字段 | 含义 | 判读 |
|------|------|------|
| `cswch/s` | 自愿上下文切换/秒 | 主动让出 CPU：等锁、等 IO、等条件变量。高 = 频繁阻塞在资源上 |
| `nvcswch/s` | 非自愿切换/秒 | 时间片耗尽被内核抢占。高 = CPU 竞争激烈、可运行线程过多 |

> 锁竞争严重的程序 `cswch/s` 会飙高；CPU 超卖、线程数远超核数时 `nvcswch/s` 高。

## 六、缺页与内存（`-r`）

```bash
pidstat -r -p <PID> 1
```

| 字段 | 含义 |
|------|------|
| `minflt/s` | 次缺页/秒（在内存中，无需读盘，如首次访问已分配页）|
| `majflt/s` | **主缺页/秒**（需从磁盘加载，如换页回来、mmap 文件首次访问）。持续非 0 = 可能在 swap 或频繁 mmap IO |
| `VSZ` | 虚拟内存 KB |
| `RSS` | 常驻物理内存 KB |
| `%MEM` | 物理内存占比 |

## 七、常用组合与技巧

```bash
pidstat -urd -p <PID> 1        # CPU + 内存 + 磁盘 一次看全
pidstat -ut 1 5                # 全系统所有线程，采 5 次（找出最耗 CPU 的线程）
pidstat -u 1 | awk '$8>50'     # 只看 %CPU>50 的行（脚本过滤）
pidstat -u -p <PID> 1 > cpu.log &   # 后台持续记录到文件
```

## 八、关键指标速查

| 指标 | 缩写/英文 | 选项 | 正常值 | 异常信号 |
|------|----------|------|--------|---------|
| 用户态 CPU | `%usr` / %User | `-u` | 视业务 | 高 + 低 system → 纯计算；需 perf 定位函数 |
| 内核态 CPU | `%system` / %System | `-u` | < 10% | > 30% → 大量 syscall，用 strace 定位 |
| CPU 总占用 | `%CPU` / Total CPU | `-u` | 视业务 | 多核累加可 > 100%；注意 Irix 模式 |
| CPU 等待 | `%wait` / CPU Wait | `-u` | ≈ 0 | 高 → CPU 不够，任务在运行队列排队（新版才有） |
| 核间迁移 | `CPU` 列 | `-u` | 稳定在某核 | 频繁跳核 → 缓存变冷，考虑绑核 |
| 自愿切换 | `cswch/s` | `-w` | 视业务 | 很高 → 频繁阻塞（IO/锁/条件变量） |
| 非自愿切换 | `nvcswch/s` | `-w` | < 100/s | > 几千/s → CPU 竞争激烈、线程过多 |
| 磁盘读速率 | `kB_rd/s` | `-d` | 视业务 | 突然暴涨 + await 高 → 读盘瓶颈 |
| 磁盘写速率 | `kB_wr/s` | `-d` | 视业务 | 突然暴涨 + w_await 高 → 写盘瓶颈 |
| IO 延迟 | `iodelay` | `-d` | ≈ 0 | 持续 > 几十 tick → 进程卡在等磁盘 |
| 次缺页 | `minflt/s` | `-r` | 视业务 | 极高 → 频繁分配新页或 COW |
| 主缺页 | `majflt/s` | `-r` | 0 | 持续 > 0 → swap/频繁 mmap，触发磁盘 IO |
| 常驻内存 | RSS / Resident | `-r` | 视业务 | 单调增长 → 内存泄漏 |

## 九、常见排查方法与分析

### 9.1 stime/内核态高 → 定位系统调用

```bash
pidstat -u -p <PID> 1 → 确认 %system 高
  → strace -c -p <PID> → 看哪个 syscall 次数多/耗时长
    → 若是 openat/write/close 次数异常 → 加缓冲或复用 fd
    → 若是 futex 耗时高 → 锁竞争，用 perf lock 分析
```

### 9.2 上下文切换定位

```bash
pidstat -w -p <PID> 1 → 区分 cswch 和 nvcswch
  → cswch 高 → 任务在等什么
    → strace -p <PID> 看卡在哪个 syscall（read/futex/poll）
      → futex → 锁竞争；read → IO 阻塞
  → nvcswch 高 → CPU 被抢占
    → 线程数 > 核数 → 减少线程或加核
    → 线程数 ≈ 核数但仍高 → 检查调度策略（chrt -p）
```

### 9.3 磁盘 IO 进程定位

```bash
iostat -x 1 → 确认某块盘 await 高
  → pidstat -d 1 → 看 kB_wr/s, kB_rd/s 最高的进程
    → pidstat -d -p <PID> 1 → 确认 IO 速率稳定
      → iotop -o → 看 IO> 等待占比（需 root）
        → lsof -p <PID> → 看在操作哪些文件
```

### 9.4 内存泄漏排查

```bash
pidstat -r -p <PID> 1 → 持续观察 RSS 是否只增不减
  → 若 RSS 持续增长 → 内存泄漏嫌疑
    → cat /proc/<PID>/maps → 看哪个内存段在涨
    → 引入 valgrind / AddressSanitizer 复现
```

### 9.5 核间迁移分析

```bash
pidstat -u -p <PID> 1 | awk '{print $NF}' → 连续观察 CPU 列
  → 若 PID 每秒在不同核上 → 迁移频繁
    → taskset -cp <PID> → 查看当前亲和性
    → taskset -cp 0-3 <PID> → 绑核减少迁移
    → 但 bind 后可能失去负载均衡的好处，需实测对比
```

## 十、交叉引用

- **整机概览**：[top](/tools/cpu/top.md)（先确定哪个进程 CPU 高再看 pidstat）
- **逐核分析**：[mpstat](/tools/cpu/mpstat.md)（看负载是否均匀/软中断分布）
- **调度观测**：[scheduling-observation.md](/tools/cpu/scheduling-observation.md)（从 vmstat r/cs 到 pidstat -w 的完整链路）
- **CPU 函数级**：[perf](/tools/code/perf.md)（%usr 高时定位热点函数）
- **系统调用**：[strace](/tools/code/strace.md)（%system 高时定位 syscall）
- **磁盘 IO**：[iostat](/tools/disk/iostat.md)（设备级）→ [iotop](/tools/disk/iotop.md)（进程级 IO> 占比）
- **内存**：[free](/tools/memory/free.md)（整机）→ pidstat -r（进程级 RSS/缺页）
- **数据源**：[procfs](/tools/proc/procfs.md) §二（/proc/&lt;pid&gt;/stat、status、io）

## 十一、常见坑

- **只给 `-p` 不给间隔**：只打印一次开机以来的平均，看不到实时波动——记得加间隔如 `1`。
- **不同版本列名不同**：老版本可能没有 `%guest`/`%wait`，或 `%system` 叫别的；以本机 `pidstat -V` 和实际表头为准。
- **`%CPU` 与 top 对不上**：pidstat 默认按逻辑核累加（和 top 的 Irix 模式一致），核数多时数值大是正常的。
- **进程重启后 PID 变了**：用 `-p $(pgrep name)` 动态取，或直接 `-C <命令名>` 按名字匹配（新版支持 `-C`）。

