# CPU Diagnosis

## 平均负载

我们经常会用 uptime、top 等工具查询 cpu 的平均负载（Load Average），那到底什么是平均负载呢？

通过 man uptime 可以看到平均负载的定义：**System load averages is the average number of processes that are either in a runnable or uninterruptable state. A process in a runnable state is either using the CPU or waiting to use the CPU. A process in uninterruptable state is wait- ing for some I/O access, eg waiting for disk.**&#x20;

简单来说，即单位时间内系统处于可运行状态或不可中断状态的平均进程数。如平均负载为 2 时，系统有 2 个 CPU，意味着 CPU 刚好被完全占用。三个时间间隔的平均负载反映了趋势。所以**平均负载高并不能反映问题出在哪里，可能是 CPU 使用过高，也可能是 IO 等待过长**。

* 可运行状态（ps 命令中看到的 R 状态）：
  * 正在使用 CPU
  * 等待使用 CPU
* 不可中断状态（ps 命令中看到的 D 状态）：等待某些硬件设备的 IO 响应。

**经验：当平均负载高于 CPU 个数的 70% 的时候，就需要关注性能问题了。**

{% hint style="success" %}
**平均负载与 CPU 使用率**\
两者并不一定是完全对应的，从定义可以看出，平均负载是指活跃的进程个数，活跃的进程还包括了等待 CPU 和等待 IO 的进程。

* CPU 密集型：两个是一致的
* IO 密集型：等待 IO 会使平均负载升高，但 CPU 使用率不一定高
* 大量的进程调用会使平均负载升高，CPU 使用率也会升高
  {% endhint %}

### 压力测试

```bash
# CPU 密集型, 模拟一个 CPU 使用 100%
stress -c 1 -t 10

# IO 密集型, 不停地执行 sync
stress -i 1 -t 600

# 大量进程
stress -c 8 -t 600

# 查看 uptime 命令输出的变化
watch -d uptime

# 每隔 5 秒输出一组所有 CPU 的使用率
mpstat -P ALL 5

# 查看哪个进程占用的 CPU
pidstat -u 5 1
```

## 上下文切换

每个任务运行前，CPU 都需要知道从哪里加载、从哪个位置开始运行，所以系统需要先设置好 CPU 寄存器和程序计数器（Program Counter, PC）。

寄存器是 CPU 内置的容量小、速度极快的内存；PC 用于存储正在执行的指令位置、或下一条指令位置。这两者是 CPU 在运行任何任务前必须依赖的环境，所以也叫 CPU 上下文。

CPU 上下文切换就是把前一个任务的上下文（寄存器和 PC）保存到内核中，加载新任务的上下文到寄存器和 PC。

根据任务和不同，可以分为**进程上下文切换、线程上下文切换、中断上下文切换**。

### 系统调用

Linux 把进程的运行空间分为内核空间（最高权限，可以访问所有资源）和用户空间（必须通过系统调用陷入到内核中才能访问内存等硬件设备），分别对应 Ring0 和 Ring3。

![](/files/-MJWZRdiP7dG3sxlMri0)

系统调用时，CPU 寄存器中用户态的指令位置需要先保存起来，更新为内核态指令的位置；系统调用结束后，又恢复为用户态。所以**一次系统调用会发生两次 CPU 上下文切换**。

{% hint style="info" %}
系统调用不需要涉及虚拟内存等资源，也不会切换进程，所以系统调用通常称为特权模式切换，而不是上下文切换。
{% endhint %}

### 进程上下文切换

进程是由内核来管理和调度的，进程切换只能发生在内核态。所以进程切换不仅包括用户态资源（虚拟内存、栈、全局变量），还包括内核态资源（内核堆栈、寄存器）。

Linux 为每个 CPU 维护一个队列，将活跃进程（正在运行或正在等待 CPU）按照优先级和等待 CPU 的时长排序。进程调度的时机发生在：

* 一个进程执行结束。
* 分时操作系统中进程的时间片用完了
* 进程通过 sleep 将自己主动挂起
* 有更高优先级的进程
* 硬件中断

### 线程上下文切换

**线程是调度的基本单位，而进程是资源拥有的基本单位**。内核中的任务调度对象都是线程，而进程只是给线程提供了虚拟内存、全局变量等资源。所以：

* 进程只有一个线程时，进程就等于线程。
* 进程有多个线程时，线程会共享相同的虚拟内存和全局变量等资源，这些资源在上下文切换时不需要修改。
* 线程也有自己的私有数据，如栈和寄存器，这些在上下文切换是需要保存的。

由此可见，若两个线程不属于同一进程，则上下文切换与进程切换一样；若属于同一进程，切换时只需要切换线程的私有数据。**这也是多线程替代多进程的一个优势**。

### 中断上下文切换

为了快速响应硬件事件，中断会打断进程的正常执行，转而调度中断处理程序。中断不涉及进程的用户态，所以中断上下文切换不需要保存和恢复用户态资源。对于同一 CPU， 中断处理比进程有更高的优先级。

### 案例

```bash
# 1. 查看空闲状态时的状态
vmstat 1 1

# 2. 模拟8个线程
sysbench --threads=10 --max-time=300 threads run

# 3. 查看此时的上下文切换
vmstat 1

# 4. 查看是哪个进程导致的
pidstat -w -u 1

# 5. 查看线程上下文切换
pidstat -wt 1

# 6. 查看中断类型
watch -d cat /proc/interrupts
```

### 总结

上下文切换 1 万以内都算正常，若超过 1 万次、或出现量级增长，就可能出现了性能问题。不过这是经验数据，具体还要看配置等实际场景。

* 自愿上下文切换变多，说明进程都在等待资源，可能发生了 IO 等其它问题。
* 非自愿上下文切换变多，说明都在争抢 CPU，确实 CPU 出现了瓶颈。
* 中断次数变多，需要查看 /proc/interrupts 具体分析。

## 使用率

Linux 将 CPU 运行划分成很短的时间片，通过定义节拍率（HZ）来触发时间中断，Jiffies 记录了开机以来的节拍数。

```bash
# 查看内核配置的节拍率。
➜  ~ grep 'CONFIG_HZ=' /boot/config-4.19.0-11-amd64
CONFIG_HZ=250 # 表示每秒触发 250 次时间中断
```

用户空间的节拍率 USER\_HZ 固定为 100。

Linux 通过 /proc 虚拟文件系统，向用户提供了内核信息。/proc/stat 提供了 CPU 和任务统计信息，进程的的 CPU 使用情况可以查看 /proc/\[pid]/stat。

```bash
# 后面的每一列表示不同场景下 CPU 的累积节拍数，单位为 USER_HZ（10ms）
➜  ~ cat /proc/stat | grep cpu
cpu  62730 26 81989 40792338 728 0 424 0 0 0
cpu0 31578 11 42637 20389529 667 0 237 0 0 0
cpu1 31152 14 39352 20402809 61 0 186 0 0 0
```

可通过 man proc 查看每一列的意义：

* user：用户态。不包括 nice，包括 guest。
* nice：低优先级用户态。
* system：内核态。
* idle：空闲。不包括 iowait。
* iowait：等待 I/O。
* irq：硬中断。
* softirq：软中断。
* steal：若当前系统运行在虚拟机中，CPU 被其它虚拟机占用的时间。
* guest：虚拟化运行其它操作系统时，运行虚拟机的时间。
* guest\_nice：低优先级运行虚拟机的时间。

CPU 使用率为：

$$
utility = 1 - 空间时间 / 总 CPU 时间
$$

上面是开机以来的平均使用率，应该计算某段时间范围内的使用率，所以需要做个差值。需要注意：不同的性能分析工具做差值的时间范围不同，如 top 默认是 3s，ps 是进程的整个生命周期。

通常查看 CPU 使用率的工具有：ps、top、pidstat。可用 perf、GDB 来定位具体的函数。

{% hint style="info" %}
系统的 CPU 使用率很高，不一定能找到对应高 CPU 使用率的进程。

* 进程在不断的崩溃重启，如配置错误等等。
* 应用调用其它二进制程序，这些程序运行时间较短。

若要检测上述问题，可通过：

* perf record -g，perf report
* execsnoop，专为短时进程设计的工具
  {% endhint %}

### 不可中断状态

不可中断状态是一种保护机制，可以保证硬件的交互过程不被意外打断。所以，短时间的不可中断状态是很正常的，但一个进程长时间处于 D 状态，通常表示系统有 I/O 性能问题。若系统中出现大量 D 状态的进程，则要考虑下是不是出现了 I/O 性能问题。

但是 iowait 高不一定就是 I/O 有性能瓶颈，当系统中只有 I/O 密集型的进程运行时，iowait 也会很高，但是磁盘还可能远没到读写瓶颈。

可以通过 pidstat、dstat 等工具确认是否有磁盘 I/O 问题。

### 僵尸进程

大量僵尸进程会占用进程号，导致新进程不能创建。

若系统中出现大量 Z 状态的进程，则要考虑父进程是否回收自己进程的资源。回收的方式：

1. 父进程通过 wait(), waitpid() 等待子进程结束，并回收资源。
2. 父进程可以注册 SIGCHLD 信号处理函数异步回收。
3. 父进程退出，init 进程也会回收。

### 软中断

softirq 也是 CPU 使用率升高的常见原因。

中断是系统用来响应硬件设备请求的一种机制，会打断进程的正常调度和执行，然后调用内核中的中断处理程序来响应设备的请求。**中断是一种异步的事件处理机制，可以提高系统的并发处理能力**。

中断处理程序在响应中断时，会临时关闭中断，所以本次中断处理完成之前，其它中断都不能响应，即中断可能会丢失。所以**中断处理程序的特点是尽可能快地运行**。为了防止中断处理程序执行过长，Linux 将中断分成两个阶段：

* **上半部**：快速处理中断，在中断禁止模式下运行。直接处理硬件请求，常说的硬中断，特点是快速。会打断 CPU 正在执行的任务，立即执行中断处理程序。
* **下半部**：延迟处理上半部未完成的工作，通常以内核线程的方式运行。由内核触发，常说的软中断，特点是延迟执行。每个 CPU 都对应一个软中断内核线程，名字为 ksoftirq/CPU编号。

{% hint style="info" %}
比如网卡接收到数据，通过硬件中断，内核启动响应的中断处理程序。上半部，网卡把数据读到内存，更新寄存器状态（表示数据已经就绪），再发送一个软中断信号，通知下半部程序做进一步处理；下半部：从内存中读取数据，按照网络协议栈对数据逐层梳理。
{% endhint %}

软中断不仅包含硬件中断处理程序的下半部，一些内核自定义事件也属于软中断，比如内核调度和 RCU 锁。

```bash
# 查看软中断运行情况
cat /proc/softirqs

# 查看硬中断运行情况
cat /proc/interrupts

# 查看软中断内核线程
ps aux | grep softirq
```

## 总结

### CPU 性能指标

![](/files/-MLAwgPRs89QSK8OZVQw)

### 指标 -> 工具

![](/files/-MLAyYoba4k4BI7Dnnk2)

### 工具 -> 指标

![](/files/-MLAyjcJDJv37C6nhpJm)

### 指标的关联性

![](/files/-MLAzDb015JvM5KxtpeJ)

## CPU 性能优化

### 方法论

* 怎么判断性能优化是否有效？优化后能提升多少性能？
* 多个性能问题同时发生，应该先优化哪个？80% 的问题都由 20% 的代码导致，并不是所有的问题都值得优化。
* 有多种优化方法时，选用哪个？

### 评估优化效果

1. 确定量化指标。不要局限于单一维度，至少从应用程序（如吞吐量、延迟）和系统资源（如 CPU 使用率）两个维度。
2. 测试优化前指标。
3. 测试优化后指标。

### 应用程序优化

* 编译器优化
* 算法优化
* 异步
* 多线程替代多进程
* 缓存

### 系统优化

* CPU 绑定
* CPU 独占
* 优先级调整
* 为进程设置资源限制
* NUMA
* 中断负载均衡

{% hint style="info" %}
避免过早优化
{% endhint %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://yunzhao.gitbook.io/notes/computer-science/linux/cpu-diagnosis.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
