# 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。

![](https://3232244687-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LYZow-MmROshIrkwdtE%2F-MJPZSaC8xos1wvKsGyb%2F-MJWZRdiP7dG3sxlMri0%2Fimage.png?alt=media\&token=a8e9ed45-d578-475d-9411-76730bef3b68)

系统调用时，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 性能指标

![](https://3232244687-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LYZow-MmROshIrkwdtE%2F-MLAsq0fMcHNPstrxLq2%2F-MLAwgPRs89QSK8OZVQw%2Fimage.png?alt=media\&token=23090510-e954-4898-a3e3-c0bb40f31814)

### 指标 -> 工具

![](https://3232244687-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LYZow-MmROshIrkwdtE%2F-MLAsq0fMcHNPstrxLq2%2F-MLAyYoba4k4BI7Dnnk2%2Fimage.png?alt=media\&token=7b95677a-645e-450b-a3eb-db5861a9b36b)

### 工具 -> 指标

![](https://3232244687-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LYZow-MmROshIrkwdtE%2F-MLAsq0fMcHNPstrxLq2%2F-MLAyjcJDJv37C6nhpJm%2Fimage.png?alt=media\&token=3bcb129a-42e3-4e63-8735-674b4d8319f9)

### 指标的关联性

![](https://3232244687-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LYZow-MmROshIrkwdtE%2F-MLAsq0fMcHNPstrxLq2%2F-MLAzDb015JvM5KxtpeJ%2Fimage.png?alt=media\&token=928e2e67-485e-4609-ad19-6b6d9f019cbc)

## CPU 性能优化

### 方法论

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

### 评估优化效果

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

### 应用程序优化

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

### 系统优化

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

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