跳转至

诊断工具、实验方法与自测

操作系统诊断不是看到一个高指标就套结论,而是把现象放到控制流和资源层次中验证。本页以 Linux 常见工具为例;命令和字段会随内核、发行版、权限和工具版本变化,使用时应先阅读本机手册。

诊断的第一原则:先描述现象

“机器很慢”信息不足。应至少记录:

  • 哪个操作慢,开始和结束点是什么?
  • 平均延迟还是尾延迟异常?
  • 单个进程、某类请求还是整机?
  • 从何时开始,与发布、负载和配置有何关系?
  • CPU、内存、I/O、网络和错误率是否同时变化?

先建立时间线,避免把结果发生后的伴随指标误当成原因。

从四种线程状态开始

诊断某线程时先分类:

  1. on-CPU:正在执行,可能计算或内核工作繁忙。
  2. runnable:可以运行但在等 CPU。
  3. sleeping:等待 I/O、锁、定时器或消息。
  4. stopped/dead:被暂停或已退出。

高延迟若来自 on-CPU,应找热点指令;来自 runnable,应看调度竞争;来自 sleeping,应找等待对象。三条路径不能混用。

低成本第一轮观测

进程与线程

ps -eo pid,ppid,stat,comm,%cpu,%mem
ps -L -p PID -o pid,tid,stat,psr,%cpu,comm

关注状态、CPU、内存和线程分布。瞬时 ps 是快照,无法代表一段时间。

整机 CPU 与运行队列

uptime
vmstat 1
mpstat -P ALL 1

运行队列高且 CPU 忙可能表示 CPU 竞争;CPU 空闲但任务延迟高,应检查睡眠和外部等待。load average 的组成依系统定义,不应简单等同 CPU 使用率。

每进程时间序列

pidstat -p PID 1
pidstat -w -p PID 1

观察 CPU、主动/非主动上下文切换。上下文切换高可能是 I/O 并发、锁竞争或过多短任务,不自动等于问题。

系统调用层

strace

strace -f -tt -T -p PID

可观察系统调用、时间戳、耗时和返回值。适合判断进程反复调用什么、阻塞在哪类调用、是否出现错误重试。

限制:

  • 跟踪会增加明显开销,尤其高频系统调用。
  • read 耗时长只说明调用路径等待,未直接证明物理磁盘慢。
  • 用户态计算和无系统调用锁竞争可能不可见。
  • 附加其他进程需要权限,并可能改变时序。

先在测试环境或短窗口使用,并限制过滤范围。

文件描述符与映射

lsof -p PID
cat /proc/PID/maps
cat /proc/PID/status

lsof 帮助找到仍打开但已删除的文件、套接字和设备。maps 显示虚拟区域,status 提供线程数、内存等摘要。/proc 是动态快照,多文件读取之间状态可能变化。

CPU 热点

perf stat -p PID sleep 10
perf record -g -p PID -- sleep 10
perf report

perf stat 给周期、指令、分支、缓存等计数,record/report 采样调用栈。需要符号、权限和合适采样频率。

一个高 CPU 进程可能:

  • 正在做有效计算。
  • 忙轮询等待事件。
  • 自旋锁竞争。
  • 反复失败和重试。
  • 执行垃圾回收或内核辅助工作。

火焰图或调用栈告诉“时间在哪里”,业务指标才能判断“这些工作是否有用”。

调度与 off-CPU

当任务大多不在 CPU 上,应观察阻塞栈、调度延迟和等待原因。可选工具包括 perf sched、内核跟踪设施和 eBPF 工具集,具体可用性取决于环境。

关键问题:

  • 线程是 runnable 但没被调度,还是 sleeping?
  • 若 sleeping,等待 I/O、futex、定时器还是子进程?
  • 唤醒到真正运行之间延迟多大?
  • 是否存在锁队列和优先级反转?

只看 on-CPU profile 会漏掉绝大多数等待时间。

内存与缺页

free -h
vmstat 1
pidstat -r -p PID 1
cat /proc/PID/smaps_rollup

关注:

  • 可用内存与缓存,而非只看 free 列。
  • 次要/主要缺页的时间序列。
  • 交换活动,而非交换空间是否曾被使用。
  • RSS、共享页和匿名页构成。

判读例子

证据 更合理的下一步
启动时大量次要缺页,随后稳定 可能是正常按需映射
持续主要缺页 + swap I/O 检查工作集和内存压力
RSS 高但无回收/缺页压力 不要仅因“内存用满”就优化
分配延迟高但总空闲不低 检查碎片、锁和 NUMA

Linux 会把空闲内存用于缓存,这通常是提高性能,不等于泄漏。

块 I/O

iostat -xz 1
pidstat -d -p PID 1

关注吞吐、请求大小、队列、完成延迟和设备利用,但字段含义依工具版本和设备模型。虚拟设备、RAID、云盘和 NVMe 多队列会让单一 %util 解释变得复杂。

高 I/O 延迟可能来自:

  • 应用提交过深队列。
  • 文件系统回写或日志提交。
  • 存储设备内部垃圾回收。
  • 虚拟化宿主争用。
  • 网络存储延迟。
  • 内存压力引发交换。

要把应用 read 延迟、块层请求延迟和设备完成延迟放到同一时间线。

页缓存命中与冷读实验

对比同一文件第一次与第二次读取可以形成缓存假设,但实验容易受预读、其他进程、文件系统和内存压力影响。

不要在共享或生产环境随意清空全局缓存;这会影响所有工作负载并改变系统状态。更安全的做法包括:

  • 使用独立测试机或容器/虚拟机环境。
  • 读取新生成且未被其他进程访问的测试文件。
  • 记录主要缺页和块 I/O,验证是否真的触发设备访问。
  • 多次重复并报告分布,而不是只取一次数字。

锁与并发

锁问题的证据可能包括:

  • 大量线程睡眠在 futex/锁等待。
  • 某个持锁线程长时间 off-CPU 或执行慢 I/O。
  • 高 CPU 伴随自旋和失败 CAS。
  • 吞吐不随核心数增长,反而下降。

诊断应收集等待者与持有者关系,而非只数等待线程。若看不到持有者,可能是条件变量、任务依赖或用户态运行时调度,而不是真正互斥锁。

网络与套接字

ss -tinp
ss -s

检查连接状态、发送/接收队列、重传和进程关联。应用队列增长可能表示下游消费慢,内核 socket 队列增长可能表示网络或应用读取不及时。

网络问题跨越应用、协议栈、网卡、交换网络和对端;本机 socket 状态只是其中一层。

日志与内核消息

错误计数和内核日志可发现设备重置、文件系统错误、OOM 和驱动异常。日志本身也可能:

  • 被限速而丢失重复信息。
  • 时间戳基准不同。
  • 含敏感数据。
  • 在故障后被覆盖或无法持久化。

应把日志与计数器、跟踪和业务请求 ID 关联,不只搜索某个关键词。

观测者效应

工具会改变被测系统:

  • 系统调用逐条跟踪增加切换与输出。
  • 高频采样占用 CPU 和存储。
  • 调试器暂停线程会改变锁和超时。
  • 打印日志改变 I/O 与调度。
  • 清缓存或压测改变全局工作集。

优先从低频聚合指标开始,再逐步缩小范围和提高精度。每次记录工具配置和持续时间,使结果可复现。

一套分层排查模板

现象:文件读取尾延迟突然升高

  1. 应用层:确认具体请求、文件、偏移和读取大小。
  2. 系统调用层:短窗口观察 read 耗时与返回值。
  3. 调度层:区分 I/O 等待和 runnable 延迟。
  4. 内存层:检查页缓存、主要缺页与回收。
  5. 文件系统层:检查日志、回写和锁竞争。
  6. 块层:检查请求延迟、队列深度和吞吐。
  7. 设备/宿主层:检查错误、重置和共享争用。

每一层都应提出可证伪假设。例如“设备慢”应能预测块请求完成延迟上升;若 read 慢但没有块 I/O,设备解释就不成立。

最小实验报告模板

问题:
环境与版本:
工作负载与输入:
观察窗口:
基线:
假设:
采集工具与参数:
关键证据:
替代解释:
结论与置信度:
下一步验证:

写出“替代解释”可以避免过早锁定单一原因。

综合自测

1. 特权与切换

一次系统调用进入内核后,是否必然发生进程切换?什么条件下才可能切换?

2. 线程状态

线程长时间不在 CPU 上,怎样区分它在运行队列等待 CPU,还是等待 I/O?

3. 并发

两个线程用原子读取余额、原子扣款,为什么仍可能透支?应保护什么不变量?

4. 条件变量

为什么 wait 返回后必须重新检查条件,而不能相信通知者已经为当前线程保留资源?

5. 死锁

全局锁顺序破坏死锁四条件中的哪一个?它是否自动保证没有饥饿?

6. 虚拟内存

TLB miss、写时复制缺页和从交换区换入的主要缺页,哪个通常最慢?为什么?

7. 页面回收

干净文件页和脏匿名页在回收时有何不同?

8. 文件对象

日志文件已被删除,但磁盘空间不下降。你会检查什么?

9. 崩溃一致性

应用写临时文件后原子重命名,为什么还可能需要同步文件和目录?

10. I/O 模型

非阻塞 socket 与完成式异步 I/O 在控制流上有何差异?

11. DMA

设备通过 DMA 把数据写入内存后,为什么仍需要完成通知和内存顺序协议?

12. 虚拟化

容器和虚拟机的主要隔离边界分别在哪里?为什么可以组合使用?

13. 综合 read

read 耗时高但 iostat 没有对应设备请求。至少列出三种候选解释。

14. 工具选择

一个服务 CPU 使用率低、请求延迟高,为什么第一步不应只做 on-CPU 火焰图?

参考思路

  1. 不必然;阻塞、抢占或更高优先级任务就绪后,调度器才可能换线程。
  2. 查看任务状态、运行队列延迟和等待栈;runnable 与 sleeping 的证据不同。
  3. 业务操作是检查加扣款的复合事务,要在同一同步或事务协议中保护“余额不为负”。
  4. 可能虚假唤醒,或其他线程先获得锁并消耗条件;通知只表示状态可能变化。
  5. 破坏环形等待;不保证公平,某线程仍可能长期竞争失败。
  6. 从交换区读取通常最慢,因为需要设备 I/O;TLB miss 可页表遍历,COW 多为内存复制。
  7. 干净文件页可丢弃后重读;脏匿名页需写交换、保留或终止所属工作负载。
  8. 检查仍打开已删除 inode 的进程和 fd,关闭最后引用后空间才可回收。
  9. 重命名的可见原子性与崩溃后的持久性不同,数据和目录项要按目标文件系统协议持久化。
  10. 非阻塞模式由应用收到就绪后调用读写;完成式接口提交具体请求,稍后消费完成结果。
  11. CPU 要确认成功、回收描述符、唤醒等待者;屏障确保设备和 CPU 以正确顺序看到描述符与数据。
  12. 虚拟机边界在虚拟硬件/VMM,容器边界在共享宿主内核机制;可用 VM 隔离租户、容器部署应用。
  13. 页缓存命中但调度延迟、文件系统锁竞争、用户缓冲缺页、网络/特殊文件、跟踪窗口错位等。
  14. 低 CPU 表示时间可能主要在 off-CPU 等待,应先区分 runnable、I/O、锁和定时器等待。

最后的检查清单

  • 我是否把抽象、机制和策略分开了?
  • 我是否画出了缓存命中与未命中的分支?
  • 我是否区分正在运行、就绪和等待?
  • 我是否取得了能反驳自己假设的证据?
  • 我是否记录了工具开销、版本和观测窗口?
  • 修复后,瓶颈是否只是转移到了另一个队列?

能够稳定回答这些问题,就已经从“背操作系统名词”走向了真正的系统推理。