诊断工具、实验方法与自测¶
操作系统诊断不是看到一个高指标就套结论,而是把现象放到控制流和资源层次中验证。本页以 Linux 常见工具为例;命令和字段会随内核、发行版、权限和工具版本变化,使用时应先阅读本机手册。
诊断的第一原则:先描述现象¶
“机器很慢”信息不足。应至少记录:
- 哪个操作慢,开始和结束点是什么?
- 平均延迟还是尾延迟异常?
- 单个进程、某类请求还是整机?
- 从何时开始,与发布、负载和配置有何关系?
- CPU、内存、I/O、网络和错误率是否同时变化?
先建立时间线,避免把结果发生后的伴随指标误当成原因。
从四种线程状态开始¶
诊断某线程时先分类:
- on-CPU:正在执行,可能计算或内核工作繁忙。
- runnable:可以运行但在等 CPU。
- sleeping:等待 I/O、锁、定时器或消息。
- stopped/dead:被暂停或已退出。
高延迟若来自 on-CPU,应找热点指令;来自 runnable,应看调度竞争;来自 sleeping,应找等待对象。三条路径不能混用。
低成本第一轮观测¶
进程与线程¶
关注状态、CPU、内存和线程分布。瞬时 ps 是快照,无法代表一段时间。
整机 CPU 与运行队列¶
运行队列高且 CPU 忙可能表示 CPU 竞争;CPU 空闲但任务延迟高,应检查睡眠和外部等待。load average 的组成依系统定义,不应简单等同 CPU 使用率。
每进程时间序列¶
观察 CPU、主动/非主动上下文切换。上下文切换高可能是 I/O 并发、锁竞争或过多短任务,不自动等于问题。
系统调用层¶
strace¶
可观察系统调用、时间戳、耗时和返回值。适合判断进程反复调用什么、阻塞在哪类调用、是否出现错误重试。
限制:
- 跟踪会增加明显开销,尤其高频系统调用。
read耗时长只说明调用路径等待,未直接证明物理磁盘慢。- 用户态计算和无系统调用锁竞争可能不可见。
- 附加其他进程需要权限,并可能改变时序。
先在测试环境或短窗口使用,并限制过滤范围。
文件描述符与映射¶
lsof 帮助找到仍打开但已删除的文件、套接字和设备。maps 显示虚拟区域,status 提供线程数、内存等摘要。/proc 是动态快照,多文件读取之间状态可能变化。
CPU 热点¶
perf stat 给周期、指令、分支、缓存等计数,record/report 采样调用栈。需要符号、权限和合适采样频率。
一个高 CPU 进程可能:
- 正在做有效计算。
- 忙轮询等待事件。
- 自旋锁竞争。
- 反复失败和重试。
- 执行垃圾回收或内核辅助工作。
火焰图或调用栈告诉“时间在哪里”,业务指标才能判断“这些工作是否有用”。
调度与 off-CPU¶
当任务大多不在 CPU 上,应观察阻塞栈、调度延迟和等待原因。可选工具包括 perf sched、内核跟踪设施和 eBPF 工具集,具体可用性取决于环境。
关键问题:
- 线程是 runnable 但没被调度,还是 sleeping?
- 若 sleeping,等待 I/O、futex、定时器还是子进程?
- 唤醒到真正运行之间延迟多大?
- 是否存在锁队列和优先级反转?
只看 on-CPU profile 会漏掉绝大多数等待时间。
内存与缺页¶
关注:
- 可用内存与缓存,而非只看
free列。 - 次要/主要缺页的时间序列。
- 交换活动,而非交换空间是否曾被使用。
- RSS、共享页和匿名页构成。
判读例子¶
| 证据 | 更合理的下一步 |
|---|---|
| 启动时大量次要缺页,随后稳定 | 可能是正常按需映射 |
| 持续主要缺页 + swap I/O | 检查工作集和内存压力 |
| RSS 高但无回收/缺页压力 | 不要仅因“内存用满”就优化 |
| 分配延迟高但总空闲不低 | 检查碎片、锁和 NUMA |
Linux 会把空闲内存用于缓存,这通常是提高性能,不等于泄漏。
块 I/O¶
关注吞吐、请求大小、队列、完成延迟和设备利用,但字段含义依工具版本和设备模型。虚拟设备、RAID、云盘和 NVMe 多队列会让单一 %util 解释变得复杂。
高 I/O 延迟可能来自:
- 应用提交过深队列。
- 文件系统回写或日志提交。
- 存储设备内部垃圾回收。
- 虚拟化宿主争用。
- 网络存储延迟。
- 内存压力引发交换。
要把应用 read 延迟、块层请求延迟和设备完成延迟放到同一时间线。
页缓存命中与冷读实验¶
对比同一文件第一次与第二次读取可以形成缓存假设,但实验容易受预读、其他进程、文件系统和内存压力影响。
不要在共享或生产环境随意清空全局缓存;这会影响所有工作负载并改变系统状态。更安全的做法包括:
- 使用独立测试机或容器/虚拟机环境。
- 读取新生成且未被其他进程访问的测试文件。
- 记录主要缺页和块 I/O,验证是否真的触发设备访问。
- 多次重复并报告分布,而不是只取一次数字。
锁与并发¶
锁问题的证据可能包括:
- 大量线程睡眠在 futex/锁等待。
- 某个持锁线程长时间 off-CPU 或执行慢 I/O。
- 高 CPU 伴随自旋和失败 CAS。
- 吞吐不随核心数增长,反而下降。
诊断应收集等待者与持有者关系,而非只数等待线程。若看不到持有者,可能是条件变量、任务依赖或用户态运行时调度,而不是真正互斥锁。
网络与套接字¶
检查连接状态、发送/接收队列、重传和进程关联。应用队列增长可能表示下游消费慢,内核 socket 队列增长可能表示网络或应用读取不及时。
网络问题跨越应用、协议栈、网卡、交换网络和对端;本机 socket 状态只是其中一层。
日志与内核消息¶
错误计数和内核日志可发现设备重置、文件系统错误、OOM 和驱动异常。日志本身也可能:
- 被限速而丢失重复信息。
- 时间戳基准不同。
- 含敏感数据。
- 在故障后被覆盖或无法持久化。
应把日志与计数器、跟踪和业务请求 ID 关联,不只搜索某个关键词。
观测者效应¶
工具会改变被测系统:
- 系统调用逐条跟踪增加切换与输出。
- 高频采样占用 CPU 和存储。
- 调试器暂停线程会改变锁和超时。
- 打印日志改变 I/O 与调度。
- 清缓存或压测改变全局工作集。
优先从低频聚合指标开始,再逐步缩小范围和提高精度。每次记录工具配置和持续时间,使结果可复现。
一套分层排查模板¶
现象:文件读取尾延迟突然升高¶
- 应用层:确认具体请求、文件、偏移和读取大小。
- 系统调用层:短窗口观察
read耗时与返回值。 - 调度层:区分 I/O 等待和 runnable 延迟。
- 内存层:检查页缓存、主要缺页与回收。
- 文件系统层:检查日志、回写和锁竞争。
- 块层:检查请求延迟、队列深度和吞吐。
- 设备/宿主层:检查错误、重置和共享争用。
每一层都应提出可证伪假设。例如“设备慢”应能预测块请求完成延迟上升;若 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 火焰图?
参考思路¶
- 不必然;阻塞、抢占或更高优先级任务就绪后,调度器才可能换线程。
- 查看任务状态、运行队列延迟和等待栈;runnable 与 sleeping 的证据不同。
- 业务操作是检查加扣款的复合事务,要在同一同步或事务协议中保护“余额不为负”。
- 可能虚假唤醒,或其他线程先获得锁并消耗条件;通知只表示状态可能变化。
- 破坏环形等待;不保证公平,某线程仍可能长期竞争失败。
- 从交换区读取通常最慢,因为需要设备 I/O;TLB miss 可页表遍历,COW 多为内存复制。
- 干净文件页可丢弃后重读;脏匿名页需写交换、保留或终止所属工作负载。
- 检查仍打开已删除 inode 的进程和 fd,关闭最后引用后空间才可回收。
- 重命名的可见原子性与崩溃后的持久性不同,数据和目录项要按目标文件系统协议持久化。
- 非阻塞模式由应用收到就绪后调用读写;完成式接口提交具体请求,稍后消费完成结果。
- CPU 要确认成功、回收描述符、唤醒等待者;屏障确保设备和 CPU 以正确顺序看到描述符与数据。
- 虚拟机边界在虚拟硬件/VMM,容器边界在共享宿主内核机制;可用 VM 隔离租户、容器部署应用。
- 页缓存命中但调度延迟、文件系统锁竞争、用户缓冲缺页、网络/特殊文件、跟踪窗口错位等。
- 低 CPU 表示时间可能主要在 off-CPU 等待,应先区分 runnable、I/O、锁和定时器等待。
最后的检查清单¶
- 我是否把抽象、机制和策略分开了?
- 我是否画出了缓存命中与未命中的分支?
- 我是否区分正在运行、就绪和等待?
- 我是否取得了能反驳自己假设的证据?
- 我是否记录了工具开销、版本和观测窗口?
- 修复后,瓶颈是否只是转移到了另一个队列?
能够稳定回答这些问题,就已经从“背操作系统名词”走向了真正的系统推理。