Linux性能优化实战笔记
经常说的 CPU 上下文切换是什么意思
进程调度和进程切换什么区别
系统调用切换上下文的时候会把原来用户态的cpu指令位置保存起来,接着为了执行内核态代码CPU寄存器需要更新为内核态指令的位置。最后才会跳转到内核态执行内核任务。系统调度完成后,CPU寄存器需要恢复原来保存的用户态,然后切换用户空间,继续执行任务。
一次系统调度,会有两次CPU上下文切换。
进程调度比系统调用多了一步,在保存当前进程的内核状态和cpu寄存器的同时,还需要保存该进程的虚拟内存,栈。而加载了下一进程的内核状态后,还需要刷新进程的虚拟内存和用户栈。
如何查看上下文切换
vmstat 5 表示每五秒输出一组数据
# 每隔5秒输出1组数据
$ vmstat 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 0 7005360 91564 818900 0 0 0 0 25 33 0 0 100 0 0
-
cs(context switch)是每秒上下文切换的次数。
-
in(interrupt)则是每秒中断的次数。
-
r(Running or Runnable)是就绪队列的长度,也就是正在运行和等待 CPU 的进程数。
-
b(Blocked)则是处于不可中断睡眠状态的进程数。
# 每隔1秒输出1组数据(需要 Ctrl+C 才结束)
# -w参数表示输出进程切换指标,而-u参数则表示输出CPU使用指标
pidstat -w 5
# 每隔5秒输出1组数据
$ pidstat -w 5
Linux 4.15.0 (ubuntu) 09/23/18 _x86_64_ (2 CPU)
08:18:26 UID PID cswch/s nvcswch/s Command
08:18:31 0 1 0.20 0.00 systemd
08:18:31 0 8 5.40 0.00 rcu_sched
...
# 每隔1秒输出一组数据(需要 Ctrl+C 才结束)
# -wt 参数表示输出线程的上下文切换指标$
pidstat -wt 1
这个结果中有两列内容是我们的重点关注对象。一个是 cswch ,表示每秒自愿上下文切换(voluntary context switches)的次数.
另一个则是 nvcswch ,表示每秒非自愿上下文切换(non voluntary context switches)的次数。
所谓自愿上下文切换,是指进程无法获取所需资源,导致的上下文切换。比如说, I/O、内存等系统资源不足时,就会发生自愿上下文切换。
而非自愿上下文切换,则是指进程由于时间片已到等原因,被系统强制调度,进而发生的上下文切换。比如说,大量进程都在争抢 CPU 时,就容易发生非自愿上下文切换
中断
中断需要从 /proc/interrupts 这个只读文件中读取
# -d 参数表示高亮显示变化的区域
$ watch -d cat /proc/interrupts
CPU0 CPU1
...
RES: 2450431 5279697 Rescheduling interrupts
...
重调度中断(RES),这个中断类型表示,唤醒空闲状态的 CPU 来调度新的任务运行。这是多处理器系统(SMP)中,调度器用来分散任务到不同 CPU 的机制,通常也被称为处理器间中断(Inter-Processor Interrupts,IPI)。
某个应用的CPU使用率居然达到100%,我该怎么办?
为了维护 CPU 时间,Linux 通过事先定义的节拍率(内核中表示为 HZ),触发时间中断,并使用全局变量 Jiffies 记录了开机以来的节拍数。每发生一次时间中断,Jiffies 的值就加 1。
节拍率 HZ 是内核的可配选项,可以设置为 100、250、1000 等。不同的系统可能设置不同数值,你可以通过查询 /boot/config 内核选项来查看它的配置值。比如在我的系统中,节拍率设置成了 250,也就是每秒钟触发 250 次时间中断。
$ grep 'CONFIG_HZ=' /boot/config-$(uname -r)
CONFIG_HZ=250
节拍率是内核选项,用户无需关心。
内核还提供了一个用户空间节拍率USER_HX,固定为100,也就是1/100秒。
Linux通过/proc虚拟文件向用户空间提供了内部状态的信息,
而/proc/stat提供的就是系统的CPU和任务统计信息。
# 只保留各个CPU的数据
$ cat /proc/stat | grep ^cpu
cpu 280580 7407 286084 172900810 83602 0 583 0 0 0
cpu0 144745 4181 176701 86423902 52076 0 301 0 0 0
cpu1 135834 3226 109383 86476907 31525 0 282 0 0 0
第一行没有编号的 cpu ,表示的是所有 CPU 的累加。它的单位是 USER_HZ,也就是 10 ms(1/100 秒)。
-
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),代表以低优先级运行虚拟机的时间。
![](/Users/ben/Library/Application Support/marktext/images/2020-05-31-22-58-31-image.png)
CPU过高调查工具
perf
$ perf top
Samples: 833 of event 'cpu-clock', Event count (approx.): 97742399
Overhead Shared Object Symbol
7.28% perf [.] 0x00000000001f78a4
4.72% [kernel] [k] vsnprintf
4.32% [kernel] [k] module_get_kallsym
3.65% [kernel] [k] _raw_spin_unlock_irqrestore
...
-
第一列 Overhead ,是该符号的性能事件在所有采样中的比例,用百分比来表示。
-
第二列 Shared ,是该函数或指令所在的动态共享对象(Dynamic Shared Object),如内核、进程名、动态链接库名、内核模块名等。
-
第三列 Object ,是动态共享对象的类型。比如 [.] 表示用户空间的可执行程序、或者动态链接库,而 [k] 则表示内核空间。
-
最后一列 Symbol 是符号名,也就是函数名。当函数名未知时,用十六进制的地址来表示。
$ perf record # 按Ctrl+C终止采样
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.452 MB perf.data (6093 samples) ]
$ perf report # 展示类似于perf top的报告
加上 -g 参数,开启调用关系的采样,方便我们根据调用链来分析性能问题。
perf record -ag -- sleep 2;perf report
execsnoop
专为短时进程设计的工具。它通过 ftrace 实时监控进程的 exec() 行为,并输出短时进程的基本信息,包括进程 PID、父进程 PID、命令行参数以及执行的结果。
# 按 Ctrl+C 结束
$ execsnoop
PCOMM PID PPID RET ARGS
sh 30394 30393 0
stress 30396 30394 0 /usr/local/bin/stress -t 1 -d 1
sh 30398 30393 0
stress 30399 30398 0 /usr/local/bin/stress -t 1 -d 1
sh 30402 30400 0
stress 30403 30402 0 /usr/local/bin/stress -t 1 -d 1
sh 30405 30393 0
stress 30407 30405 0 /usr/local/bin/stress -t 1 -d 1
...
系统中出现大量不可中断进程和僵尸进程怎么办?
使用top查看进程的状态
-
R 是 Running 或 Runnable 的缩写,表示进程在 CPU 的就绪队列中,正在运行或者正在等待运行。
-
D 是 Disk Sleep 的缩写,也就是不可中断状态睡眠(Uninterruptible Sleep),一般表示进程正在跟硬件交互,并且交互过程不允许被其他进程或中断打断。
-
Z 是 Zombie 的缩写,如果你玩过“植物大战僵尸”这款游戏,应该知道它的意思。它表示僵尸进程,也就是进程实际上已经结束了,但是父进程还没有回收它的资源(比如进程的描述符、PID 等)。
-
S 是 Interruptible Sleep 的缩写,也就是可中断状态睡眠,表示进程因为等待某个事件而被系统挂起。当进程等待的事件发生时,它会被唤醒并进入 R 状态。
-
I 是 Idle 的缩写,也就是空闲状态,用在不可中断睡眠的内核线程上。前面说了,硬件交互导致的不可中断进程用 D 表示,但对某些内核线程来说,它们有可能实际上并没有任何负载,用 Idle 正是为了区分这种情况。要注意,D 状态的进程会导致平均负载升高, I 状态的进程却不会。
-
T 或者 t,也就是 Stopped 或 Traced 的缩写,表示进程处于暂停或者跟踪状态。向一个进程发送 SIGSTOP 信号,它就会因响应这个信号变成暂停状态(Stopped);再向它发送 SIGCONT 信号,进程又会恢复运行(如果进程是终端里直接启动的,则需要你用 fg 命令,恢复到前台运行)。而当你用调试器(如 gdb)调试一个进程时,在使用断点中断进程后,进程就会变成跟踪状态,这其实也是一种特殊的暂停状态,只不过你可以用调试器来跟踪并按需要控制进程的运行。
-
X, 也就是 Dead 的缩写,表示进程已经消亡,所以你不会在 top 或者 ps 命令中看到它。
关于不可中断状态
当进程正在跟硬件打交道,为了保护进程数据和硬件一致性,系统不允许其他进程或中断打断这个进程。进程长时间处于不可中断状态通常可能IO性能问题。
todo对于这个状态下周还要深入了解一些,目前的了解内容是这个状态的进程不会占着cpu不放,但是不会响应任何信号处理
iowait分析
可以使用dstat分析CPU和IO的情况
#间隔1秒输出10组数据
$ dstat 1 10
#You did not select any stats, using -cdngy by default.
#--total-cpu-usage-- -dsk/total- -net/total- ---paging-- ---system--
usr sys idl wai stl| read writ| recv send| in out | int csw
0 0 96 4 0|1219k 408k| 0 0 | 0 0 | 42 885
0 0 2 98 0| 34M 0 | 198B 790B| 0 0 | 42 138
0 0 0 100 0| 34M 0 | 66B 342B| 0 0 | 42 135
0 0 84 16 0|5633k 0 | 66B 342B| 0 0 | 52 177
可以用某一个进程的IO情况
# -d 展示 I/O 统计数据,-p 指定进程号,间隔 1 秒输出 3 组数据
$ pidstat -d -p 4344 1 306:38:50 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
06:38:51 0 4344 0.00 0.00 0.00 0 app
06:38:52 0 4344 0.00 0.00 0.00 0 app
06:38:53 0 4344 0.00 0.00 0.00 0 app
当有时候发现并没有进程发生大量io的时候。
可以用夸大范围,1.扩大进程监控范围,2.缩小打印周一
$ pidstat -d 1 20 #所有的进程,1秒内打印20组
$ strace -p 6082
strace: attach: ptrace(PTRACE_SEIZE, 6082): Operation not permitted
如果发现使用root操作还没有权限,那就很可能是进程状态不对,比如这个进程已经死掉了
$ ps aux | grep 6082
root 6082 0.0 0.0 0 0 pts/0 Z+ 13:43 0:00 [app] <defunct>
碰到上面这种情况,所有的top/pidstat都没法使用了。
$ perf record -g
$ perf report
可以用perf去查看报告
有可能存在的情况是读取的时候用O_DIRECT
,进程正在对磁盘进行直接读,也就是绕过了系统缓存,每个读请求都会从磁盘直接读,这就可以解释我们观察到的 iowait 升高了。
关于僵尸进程
-
当一个进程创建子进程,它应该通过wait()或者waitpid()等待子进程结束,需要他来回收子进程的资源。
-
当子进程结束时,需要向父进程发送SIGCHLD信号。父进程可以通过注册SIGCHILD信号的处理函数异步回收资源。
-
一旦父进程没有处理子进程的终止,还一直保持运行状态。那么子进程会一直处于僵尸状态。短暂的僵尸状态我们通常不必理会,但进程长时间处于僵尸状态,就应该注意了,可能有应用程序没有正常处理子进程的退出。
僵尸进程的主要切入点就是父进程的wait和子进程的SIGCHILD
如何找父进程
# -a 表示输出命令行选项
# p表PID
# s表示指定进程的父进程
$ pstree -aps 3084
systemd,1
└─dockerd,15006 -H fd://
└─docker-containe,15024 --config /var/run/docker/containerd/containerd.toml
└─docker-containe,3991 -namespace moby -workdir...
└─app,4009
└─(app,3084)
可以看出3084的父进程是4009