Linux CPU 原理与分析
- CPU 性能指标
- CPU 使用率
- 用户 CPU
- 内核 CPU
- IOWAIT
- 软中断
- 硬中断
- 窃取 CPU
- 客户 CPU
- 上下文切换
- 自愿上下文切换
- 非自愿上下文切换
- 平均负载
- CPU 缓存命中率
平均负载(Load Average)
使用 top
或 uptime
命令,都会有以下输出(其中 load average 表示过去 1、5、15 分钟的平均负载):
1 | 02:34:03 up 2 days, 20:14, 1 user, load average: 0.63, 0.83, 0.88 |
平均负载可理解为单位时间内,系统处于 可运行状态 和 不可中断状态 的 平均进程数,即 平均活跃进程数,与 CPU 使用率没有直接关系。
可运行状态:正在使用或等待 CPU 的进程,即使用
ps
命令可见状态为 R 的进程(Running/Runnable)。不可中断状态:处于内核态关键流程中、不可打断的进程,比如等待 I/O,使用
ps
命令可见状态为 D 的进程(Disk Sleep)。
如何监控?
平均负载的值可表示系统的繁忙程度,最理想的情况是等于 CPU 核心数(逻辑 CPU,
grep 'model name' /proc/cpuinfo | wc -l
)。当平均负载值小于 CPU 核心数表示有空闲,大于 CPU 核心数表示有进程竞争不到 CPU。如果 1、5、15 分钟的值基本相同或相差不大,表明系统负载平稳。
如果 1 分钟的值远小于 15 分钟的值,表明系统最近 1 分钟负载在减少,而过去 15 分钟内却有很大的负载。
如果 1 分钟的值远大于 15 分钟的值,就说明最近 1 分钟的负载在增加,需要持续观察这种情况是临时性还是持续性。当 1 分钟的平均负载接近或超过了 CPU 核心数,意味着系统正在过载,应该分析调查原因并做出优化。
经验上平均负载超过 CPU 核心数的 70%~80% 需要分析排查负载高的原因,根据更多历史数据判断负载的变化趋势。
压力测试
安装监控及测试工具:
1 | apt install -y stress sysstat |
模拟 CPU 使用率 100% 场景:
1 | stress --cpu 1 --timeout 600 |
观察平均负载变化:
1 | watch -d uptime # -d 参数表示高亮显示变化的区域 |
观察 CPU 使用率变化:
1 | mpstat -P ALL 5 # -P ALL 表示监控所有 CPU,其中主要参考 CPU、iowait 等。 |
找出导致 CPU 使用率 100% 的进程:
1 | pidstat -u 5 1 |
除此之外,还可以模拟 I/O 压力场景:
1 | stress -i 1 --timeout 600 |
或模拟大量进程的场景:
1 | stress -c 8 --timeout 600 |
使用率(Usage)
CPU 使用率即除了空闲时间外的其他时间占总 CPU 时间的百分比(== 1 - (空闲时间/总 CPU 时间)
)。
是单位时间内对 CPU 繁忙程度的统计,与平均负载不一定完全对应:
对于 CPU 密集型进程,使用大量 CPU 会导致平均负载升高,此时两者一致。
对于 I/O 密集型进程,等待 I/O 也会导致平均负载升高,但 CPU 使用率不一定很高。
大量等待 CPU 的进程调度也会导致平均负载升高,此时 CPU 使用率也会比较高。
节拍率
CPU 划分时间片,通过调度器轮流分配给多个进程使用,在宏观上可见进程同时运行。
其通过中断(Interrupt)维护时间片,按预定义的节拍率(内核选项 HZ,每秒触发中断数)触发中断,并使用全局变量 Jiffies
记录自启动起的节拍数。
1 | grep 'CONFIG_HZ=' /boot/config-$(uname -r) |
对于用户态进程,则参考用户态节拍率 USER_HZ
(一般固定为 100)。
统计信息
通过 /proc
虚拟文件系统可以查看用户空间系统内部状态的信息,比如 CPU:
1 | cat /proc/stat | grep ^cpu |
其每列的含义:
user(缩写 us),用户态 CPU 时间。它不包括 nice 时间,但包括了 guest 时间。
nice(缩写 ni),低优先级用户态 CPU 时间。进程的 nice 值被调整为 1-19 之间时的 CPU 时间。nice 可取值范围是 -20 到 19,数值越大优先级越低。
system(缩写 sys),内核态 CPU 时间。
idle(缩写 id),空闲时间。不包括等待 I/O 的时间(iowait)。
iowait(缩写 wa),等待 I/O 的 CPU 时间。
irq(缩写 hi),处理硬中断的 CPU 时间。
softirq(缩写 si),处理软中断的 CPU 时间。
steal(缩写 st),当系统运行在虚拟机中时,被其他虚拟机占用的 CPU 时间。
guest(缩写 guest),通过虚拟化运行其他操作系统的时间,也就是运行虚拟机的 CPU 时间。
guest_nice(缩写 gnice),以低优先级运行虚拟机的时间。
如何监控?
不同的监控工具都是取某段时间的统计结果,但时间间隔可能不一样(top
默认 3s,ps
取进程整个生命周期)。使用 ps
命令查看整体情况(关注 %CPU 找到 CPU 占用高的进程):
1 | top |
使用 pidstat
命令查看进程的具体情况:
1 | pidstat 1 5 |
其每列的含义:
用户态 CPU 使用率 (%usr)
内核态 CPU 使用率(%system)
运行虚拟机 CPU 使用率(%guest)
等待 CPU 使用率(%wait)
总 CPU 使用率(%CPU)。
问题排查
当 CPU 使用率过高,可以使用 perf 分析问题成因。
找出占用 CPU 时钟最多的函数或指令:
1 | perf top |
其每列的含义:
Overhead 是该符号的性能事件在所有采样中的比例,百分比表示。
Shared 是该函数或指令所在的动态共享对象(DSO),如内核、进程名、动态链接库名、内核模块名等。
Object 是动态共享对象的类型。比如 [.] 表示用户空间的可执行程序、或者动态链接库,而 [k] 则表示内核空间。
Symbol 是符号名(函数名),当函数名未知时,用十六进制的地址来表示。
或实时展示系统性能信息:
1 | perf record -g |
在实践中:
用户 CPU(%user)和 Nice CPU(%nice)高,说明用户态进程占用了较多 CPU,着重排查进程性能问题。
系统 CPU(%system)高,说明内核态占用了较多 CPU,着重排查内核线程或系统调用的性能问题。
I/O 等待 CPU(%iowait)高,说明等待 I/O 的时间比较长,着排查系统存储是否出现 I/O 问题。
软中断(%softirq)和硬中断(%irq)高,说明软中断或硬中断处理程序占用较多 CPU,着重排查内核中断服务程序。
当应用直接调用外部的二进制程序,或其本身在不断重启(可能是同名进程 pid 一直在改变),不会表现为某个进程 CPU 占用过高。
此时也许可以从其父进程中找到一些线索:
pstree | grep xxx
。结合
perf record -g
采集一段时间的数据,可见某同名进程占用的 CPU 时钟,确认是否导致 CPU 使用率高。也可以使用
execsnoop
监控短时进程,通过 ftrace 实时监控进程的 exec() 行为输出进程 PID、父进程 PID、命令行参数以及执行的结果。
上下文(Context)
上下文切换发生在进程竞争 CPU 时,即使此时进程没有运行,也会占用 CPU 负载。
CPU 时间片轮转分配给多任务,每个任务从何处加载、运行,依赖于事先保存在系统内核的上下文。在切换时新的任务上下文被加载到这些寄存器和程序计数器,再跳转到程序计数器所指的新位置来运行,因此上下文切换主要是更新 CPU 寄存器的值。
根据不同的场景,CPU 上下文切换分为进程上下文切换、线程上下文切换和中断上下文切换。
进程上下文切换
进程是资源的基本单位。按照特权等级,进程的运行空间分为 内核空间 和 用户空间,对应着图中 Ring 0 和 Ring 3。进程在内核空间运行时称为 内核态,在用户空间运行时称为 用户态。
1 | +----------Ring3----- 应用 |
从用户态到内核态的转变,需要通过 系统调用 完成。系统调用是为 特权模式切换 而不是上下文切换,在一次系统调用中会发生两次 CPU 上下文切换:先保存起来 CPU 寄存器里原来用户态的指令位置。CPU 寄存器更新为内核态指令的新位置,最后跳转到内核态运行内核任务。而系统调用结束后,CPU 寄存器恢复原来保存的用户态,然后再切换到用户空间继续运行进程。
需要注意的是系统调用过程中一直是同一个进程在运行,不会涉及到虚拟内存等进程用户态的资源,也不会切换进程(区别于进程的上下文切换)。
进程上下文切换与系统调用的区别
进程由内核来管理调度,进程切换只能发生在内核态。所以进程的上下文不仅包括虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态。
进程的上下文切换比系统调用时多了一步:先保存该进程的 虚拟内存、栈 等,再保存当前进程的 内核状态 和 CPU 寄存器;而加载了下一进程的内核态后,还需要 刷新进程的虚拟内存和用户栈。
切换成本
每次上下文切换都需要几十纳秒到数微秒的 CPU 时间(Tsuna’s blog: How long does it take to make a context switch?)。特别是在进程上下文切换次数较多的情况下,容易导致 CPU 将大量时间耗费在寄存器、内核栈以及虚拟内存等资源的保存和恢复上,大大缩短了真正运行进程的时间,导致平均负载升高。
Linux 通过 TLB(Translation Lookaside Buffer)管理虚拟内存到物理内存的映射关系。当虚拟内存更新后 TLB 也需要刷新,进程的内存访问随之变慢。在多处理器系统上缓存被多个处理器共享,刷新缓存还会影响共享缓存的其他处理器的进程。
切换时机
在进程调度时才需要切换上下文。Linux 为每个 CPU 都维护一个就绪队列,将活跃进程(Running or Runnable)按照优先级和等待 CPU 的时间排序,再选择优先级最高和等待 CPU 时间最长的进程来运行,比如:
进程执行完终止了,使用的 CPU 释放,此时再从就绪队列取出新进程运行。
为了保证进程都被公平调度,CPU 划分一段段时间片,轮流分配给各个进程。某个进程的时间片耗尽就会被系统挂起,切换到其它进程运行。
进程在系统资源不足(比如内存)时,要等到资源满足后才可以运行,此时进程会被挂起,由系统调度其他进程运行。
进程通过 sleep 等函数将自己主动挂起,触发重新调度。
有时为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行。
线程上下文切换
线程是调度的基本单位:
当进程只有一个线程时,进程就等于线程。
当进程拥有多个线程时,线程共享相同的虚拟内存和全局变量等资源(在上下文切换时不需要修改)。
线程有自己的私有数据,比如栈和寄存器等(在上下文切换时需要保存)。
线程上下文切换时,如果两个线程属于不同进程,切换过程就与进程上下文切换一样;如果属于同一进程,虚拟内存等资源保持不动,只切换线程的私有数据(寄存器等)。因此多线程比多进程开销更小。
中断上下文切换
中断用于快速响应硬件事件,会打断进程的正常调度和执行,转而调用中断处理程序响应设备事件。而在打断其他进程时需要将进程状态保存,在中断结束后进程仍然可以从原状态恢复运行。
中断上下文切换不涉及到进程的用户态。即便中断过程打断正处在用户态的进程,不需要保存和恢复这个进程的虚拟内存、全局变量等用户态资源。中断上下文只包括内核态中断服务程序执行所必需的状态,包括 CPU 寄存器、内核堆栈、硬件中断参数等。
在同一个 CPU 中中断处理比进程拥有更高的优先级,中断上下文切换不会与进程上下文切换同时发生。由于中断会打断正常进程的调度和执行,大部分中断处理程序都比较精简,以便尽可能快的执行结束。
中断上下文切换需要消耗 CPU,切换次数过多会严重降低系统的整体性能。
压力测试
安装 sysbench
和 sysstat
:
1 | apt install -y sysbench sysstat |
模拟多线程调度的场景:
1 | sysbench --threads=10 --max-time=300 threads run |
如何监控?
使用 vmstat
命令查看总体情况:
1 | vmstat 5 |
其每列的含义:
cs(context switch)是每秒上下文切换的次数。
in(interrupt)则是每秒中断的次数。
r(Running or Runnable)是就绪队列的长度,即正在运行和等待 CPU 的进程数,如果超出 CPU 核心数,表示竞争激烈。
b(Blocked)是处于不可中断睡眠状态的进程数。
us(user)和 sy(system)是用户态和内核态 CPU 使用率,表示 CPU 被内核或应用程序占用的情况。
使用 pidstat
具体到每个进程:
1 | pidstat -w -u -t 1 |
- cswch 表示每秒自愿上下文切换(voluntary context switches)次数,指进程无法获取所需资源,导致的上下文切换。
- nvcswch 表示每秒非自愿上下文切换(non voluntary context switches)的次数,指进程由于时间片已到等原因,被系统强制调度,进而发生的上下文切换。
可以在 /proc/interrupts
查看中断的详细信息:
1 | watch -d cat /proc/interrupts |
RES 表示重调度中断,即唤醒空闲状态的 CPU 来调度新的任务运行。多处理器系统(SMP)中,调度器用于分散任务到不同 CPU 的机制,也被称为 处理器间中断(Inter-Processor Interrupts,IPI)。
问题排查
上下文切换频率取决于系统 CPU 性能。
如果系统上下文切换次数稳定,从数百到一万以内都正常。但当超过一万次或者切换次数出现数量级的增长,就可能已经出现性能问题。
自愿上下文切换变多,说明进程在等待资源,可能发生 I/O 等问题;
非自愿上下文切换变多,说明进程都在被强制调度、争抢 CPU,此时 CPU 称为瓶颈;
中断次数变多,说明 CPU 被中断处理程序占用,还需要通过查看
/proc/interrupts
文件来分析具体中断类型。
异常进程
通常指 不可中断进程 和 僵尸进程。
使用 top
命令可见 S 列(Status)表示进程的状态:
R(Running 或 Runnable)表示进程在 CPU 的就绪队列中,正在运行或者正在等待运行。
D(Disk Sleep)表示不可中断状态睡眠(Uninterruptible Sleep),表示进程正在跟硬件交互,不允许被其他进程或中断打断。
Z(Zombie)表示僵尸进程,即实际上已经结束了,但是父进程还没有回收其资源(比如进程的描述符、PID 等)的进程。
S(Interruptible Sleep)是可中断状态睡眠,表示进程因为等待某个事件而被系统挂起。当进程等待的事件发生时,它会被唤醒并进入 R 状态。
I(Idle)是空闲状态,用在不可中断睡眠的内核线程上。硬件交互导致不可中断进程用 D 表示,但某些内核线程可能并没有任何负载,则处于 I 状态。D 状态进程会导致平均负载升高, I 状态的进程不会。
T 或者 t(Stopped 或 Traced)表示进程处于暂停或者跟踪状态。向一个进程发送 SIGSTOP 信号,就会因响应信号变成暂停状态(Stopped);再向它发送 SIGCONT 信号,则又会恢复运行。或使用调试器(GDB 等)时,进程就会变成跟踪状态。
X(Dead)表示进程已经消亡,不会在
top
或者ps
命令中看到它。
1 | top |
进程组:表示一组相互关联的进程,比如每个子进程都是父进程所在组的成员。比如以 SSH 登录客户端,就会打开一个控制中断(TTY),控制终端对应一个会话(指共享同一个控制终端的一个或多个进程组)。在终端中运行命令以及其子进程,就构成了进程组,其中在后台运行的命令,构成后台进程组;在前台运行的命令,构成前台进程组。
不可中断进程(D):当 iowait 升高时,进程很可能因为得不到硬件响应,长时间处于不可中断状态。短时间的不可中断状态一般可以忽略,但如果系统或硬件发生故障,进程可能在不可中断状态保持很久,导致在系统中大量出现。
iowait 分析
比如使用 top
查看系统资源使用情况:
1 | top |
以上输出中反映了几个问题:
过去 1 分钟、5 分钟和 15 分钟内的平均负载在依次减小,说明平均负载正在升高;而 1 分钟内的平均负载已经达到系统的 CPU 个数,说明系统很可能已经有了性能瓶颈。
Tasks 中可见有 1 个正在运行的进程,但僵尸进程比较多且还在不停增加,说明有子进程在退出时没被清理。
两个 CPU 的使用率:用户 CPU 和系统 CPU 都不高,但 iowait 高达 60.5% 和 94.6%。
每个进程的情况: CPU 使用率最高的进程只有 0.3%,但有两个进程处于 D 状态,其可能在等待 I/O。
可见 iowait 太高导致平均负载升高、达到系统 CPU 个数;僵尸进程在不断增多,有程序不能正确清理子进程的资源。使用 dstat
命令查看 CPU 和 I/O 使用情况:
1 | dstat 1 10 |
可见每当 iowait 升高(wai)时,磁盘的读请求(read)都很大,说明 iowait 的升高与读磁盘有关。
对于在 top
的输出中状态为 D 的进程,使用 pidstat
命令查看 I/O 具体情况:
1 | pidstat -d 1 20 |
其中 kB_rd 表示每秒读的 KB 数, kB_wr 表示每秒写的 KB 数,iodelay 表示 I/O 的延迟(时钟周期)。可见 app 进程在进行磁盘读,32 MB/s,重点是找到 app 进程的系统调用。
使用 strace -p
跟踪进程系统调用,前提是该进程未退出,不能是僵尸进程。因此更建议使用基于事件记录的动态追踪工具 perf
:
1 | perf record -g |
iowait 高不一定代表 I/O 有性能瓶颈。
当 iowait 升高时,进程很可能因为得不到硬件的响应,而长时间处于不可中断状态。
但当系统中只有 I/O 类型的进程在运行时 iowait 也会很高,实际上磁盘的读写远没有达到性能瓶颈的程度。
因此还需要结合 dstat、pidstat 等工具确认是否磁盘 I/O 的问题,再找出导致 I/O 的进程。
处理僵尸进程
僵尸进程(Zombie):正常情况下一个进程创建子进程后,应通过系统调用 wait()
或者 waitpid()
等待子进程结束并回收资源;子进程在结束时会向父进程发送 SIGCHLD 信号,父进程还可以注册 该信号的处理函数异步回收资源。如果父进程没回收资源或是子进程执行太快、父进程未来得及处理子进程状态就已经提前退出,子进程就会变成僵尸进程。此时 mm_struct
、files 等都已释放,但需要保留 task_struct
(即 PCB,程序控制块),通过父进程处理。通常僵尸进程持续的时间都比较短,在父进程回收其资源后就会消亡,或者在父进程退出后由 init 进程回收后也会消亡。
一旦父进程没有处理子进程的终止,还一直保持运行状态,子进程就会一直处于僵尸状态:
大量的僵尸进程会用尽 PID(进程号),导致不能创建新进程。
另一方面是
task_struct
会占用大量内存资源。
处理僵尸进程需要先找出其父进程,使用 pstree
命令:
1 | pstree -aps 3084 |
可见 3084 进程的父进程是 4009,此时应查看 4009 进程的应用代码,子进程结束的处理是否正确:是否调用 wait()
或 waitpid()
、有没有注册 SIGCHID 信号处理函数。
软中断
中断(Interrupt)是系统响应硬件设备请求的异步事件处理机制,会打断进程的正常调度和执行,再调用内核中的中断处理程序来响应设备的请求。为了减少对正常进程运行调度的影响,中断处理程序就需要尽可能快地运行。其分为两个阶段:
硬中断:处理硬件请求,特点是快速执行,它在中断禁止模式下运行,主要处理与硬件紧密相关或时间敏感的工作(
/proc/interrupts
)。软中断:由内核触发,特点是延迟执行。延迟处理上半部未完成的工作,通常以内核线程的方式运行(
/proc/softirqs
,包括 10 个类型 )。除此之外,软中断还包括一些内核自定义的事件,比如内核调度和 RCU 锁(Read-Copy Update)等。
比如网卡接收到数据包通过硬件中断的方式通知内核新数据就绪。
硬中断:把网卡的数据读到内存中,更新硬件寄存器的状态,发送软中断信号。
软中断:从内存中找到网络数据,再按照网络协议栈对数据进行逐层解析和处理,送给应用程序。
软中断以内核线程运行,每个 CPU 对应一个软中断内核线程,即为 ksoftirqd/CPU 编号,使用 ps
命令:
1 | ps aux | grep softirq |
问题分析
软中断(softirq)CPU 使用率升高是最常见的一种性能问题,尤其是网络收发类型的软中断。
安装 hping3、tcpdump 网络协议包工具。
1 | apt install -y hping3 tcpdump |
模拟 SYN FLOOD 攻击:
1 | hping3 -S -p 80 -i u100 192.168.0.30 |
此时系统响应变慢,使用 top
查看总体情况:
1 | top |
在 top
的输出可见:
- 平均负载为 0,就绪队列里面只有一个进程(1 running)。
- 每个 CPU 的使用率、所有进程的 CPU 使用率都比较低。
其中 CPU 使用率最高的进程都是软中断线程,问题可能出现在软中断上。观察软中断次数的变化速率:
1 | watch -d cat /proc/softirqs |
其中 NET_RX 变化特别快,可以初步确定是网络接收软中断出现问题,使用 sar
查看网络收发情况:
1 | sar -n DEV 1 |
可见 eth0 接收的 PPS 比较大(12607),而接收的 BPS 却很小(664KB/s),每个网络帧比较小(664*1024/12607 == 54bytes
),因此是小包问题。使用 tcpdump
在 eth0 上抓包分析:
1 | tcpdump -i eth0 -n tcp port 80 |
可见包的源、目的 IP 和端口,而 Flags [S] 表示这是 SYN 包。SYN 包的 PPS 高达 20000,可见是 SYN FLOOD 攻击。
缓存命中率
CPU 采用多级缓存用于匹配与内存速度的差距:
1 | +---------------------+ |
其中 L1 Cache、L2 Cache 在单核中,L3 则用在多核中。从 L1 到 L3,大小依次增大、性能依次降低(通过 cat /sys/devices/sytem/cpu/cpu0/cache/index0/size
)。
其命中率可衡量 CPU 缓存复用情况,命中率越高性能越好。
优化总结
要对系统 CPU 进行优化,可从应用和系统两个层面考虑了。其中系统优化:
CPU 绑定:把进程绑定到一或多个 CPU 上,可提高 CPU 缓存的命中率,减少跨 CPU 调度带来的上下文切换问题。
CPU 独占:类似 CPU 绑定,进一步将 CPU 分组,通过 CPU 亲和性机制为其分配进程。这些 CPU 就由指定的进程独占,不允许其他进程使用。
优先级调整:使用 nice 调整进程的优先级(正/负值调低/高)。适当降低非核心应用的优先级,增高核心应用的优先级,可确保核心应用得到优先处理。
为进程设置资源限制:使用 Linux cgroups 设置进程 CPU 使用上限,可防止由于某个应用自身的问题而耗尽系统资源。
NUMA(Non-Uniform Memory Access)优化:支持 NUMA 的处理器会被划分为多个 node,每个 node 都有自己的本地内存空间。就是让 CPU 尽可能只访问本地内存。
中断负载均衡:中断处理程序可能会耗费大量的 CPU,开启 irqbalance 服务或者配置 smp_affinity,可以把中断处理过程自动负载均衡到多个 CPU 上。
了解 CPU 相关概念与指标后,可通过下表找到合适的工具、分析问题。


参考
分析案例可参考: