一、进程的状态
当iowait升高时,进程很可能因为得不到硬件的响应,而长时间处于不可中断状态。
1.top
top和ps是最常用的查看进程状态的工具,下面是一个top命令的输出示例,S列(也就是status)表示进程的状态。
top - 10:56:11 up 168 days, 56 min, 1 user, load average: 0.00, 0.03, 0.05
Tasks: 136 total, 1 running, 135 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.7 us, 0.0 sy, 0.0 ni, 99.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 16264988 total, 14156348 free, 482544 used, 1626096 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 15380256 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
981 root 20 0 3178188 131176 12692 S 5.6 0.8 3234:50 hostguard
23729 root 20 0 320504 6112 4648 R 0.3 0.0 120:51.28 uniagentd
1 root 20 0 191056 3960 2608 D 0.0 0.0 0:56.10 systemd
2 root 20 0 0 0 0 S 0.0 0.0 0:00.00 kthreadd
4 root 0 -20 0 0 0 Z 0.0 0.0 0:00.00 kworker/0:0H
6 root 20 0 0 0 0 0.0 0.0 0:10.14 ksoftirqd/0
...
你可以看到R、D、Z、S、I等几个状态他们分别是什么意思呢?
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 信号,进程又会恢复运行。而当你的用的调试器调试一个进程是,在使用断点中断进程后,进程就会变
成跟踪状态,这其实也就是一种特殊的暂停状态,只不过你可以用调试器来跟踪并按需控制进程的运行。
X是Dead的缩写,表示进程已经消亡,所以不会在top或者ps命令中看到它。
了解了这些,我们再回到今天的主题,先看不可终端状态,这其实是为了保证进程数据与硬件状态一直,并且增长情况下,不可终端状态在很短时间内会结束,所以短时的不可终端进程我们一般可忽略。
但如果系统或硬件发生了故障,进程可能会在不可中断状态保持很久,甚至导致系统中出现大量不可中断进程,这时,你就得注意下,系统时不时出现了I/O 等性能问题。
再看僵尸进程,这是多进程应用很容易碰到的问题,正常情况下,当一个进程创建了子进程后,它应该通过系统调用 wait() 或者 waitpid() 等待子进程结束,回收子进程的资源;而子进程在结束时,会向它的父进程发送SIGCHLD 信号,所以,父进程还可以注册 SIGCHLD信号的处理函数,异步回收资源。
如果父进程这么做,或是子进程执行太快,父进程还没有来得及处理子进程状态,子进程就已经提前退出,那这时的子进程就会变成僵尸进程。
通常,僵尸进程持续的时间都比较短,在父进程回收它的资源后就会消亡;或者在父进程退出后,由init进程回收后也会消亡。
一旦父进程没有处理子进程的终止,还一直保持运行状态,那么子进程就会一直处于僵尸状态,大量的僵尸进程会用尽PID进程号,导致新进程不能创建,所以这种情况一定要避免
2.ps
ps -aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 191056 3960 ? Ss+ 2023 0:56 /usr/lib/systemd/systemd --switched-root --system --deserialize 22
root 2 0.0 0.0 0 0 ? S 2023 0:00 /usr/bin/dbus-daemon --system --address=systemd: --nofork --nopidfile --systemd-activation
root 4 0.0 0.0 0 0 ? D+ 2023 0:00 /app
root 6 0.0 0.0 0 0 ? S 2023 0:10 /usr/sbin/crond -n
root 7 0.0 0.0 0 0 ? S 2023 0:01 [migration/0]
root 8 0.0 0.0 0 0 ? S 2023 0:00 [rcu_bh]
root 9 0.0 0.0 0 0 ? S 2023 9:44 [rcu_sched]
1、s 和 + 是什么意思呢?
s 表示这个进程是一个会话的领导进程,而 + 表示前台进程组
这里又出现了两个新概念,进程组和会话,他们用来管理一组相互关联的进程,意思其实很好理解
2、什么是进程组?
进程组表示一组相互关联的进程,比如每个子进程都是父进程所在组的成员;
3、什么是会话?
会话是只共享同一个控制终端的一个或多个进程组。
比如,我么通过SSH登录服务器,就会打开一个控制终端(TTY),这个控制终端就对应一个会话,而我我们在终端中运行的命令以及他们的子进程就构成了一个个的进程组,其中,在后台运行的命令,构成后台进程组;在前台运行的命令,就构成前台进程组。
二、案例分析
# 按下数字1切换到所有CPU使用情况,观察一会儿按ctrl+c结束
# top
top - 11:29:56 up 168 days, 1:30, 1 user, load average: 2.00, 1.68, 1.39
Tasks: 260 total, 1 running, 90 sleeping, 0 stopped, 116 zombie
%Cpu0 : 0.0 us, 0.3 sy, 0.0 ni, 38.4 id, 79.5 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu1 : 0.0 us, 0.0 sy, 0.0 ni, 4.6 id, 95.4 wa, 0.0 hi, 0.0 si, 0.0 st
...
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
981 root 20 0 3178188 131176 12692 R 0.5 0.8 3236:40 top
1 root 20 0 191056 3960 2608 D 0.3 0.0 0:56.11 app
2 root 20 0 0 0 0 D 0.3 0.2 0:00.00 app
4 root 0 -20 0 0 0 S 0.0 0.0 0:00.00 systemd
...
从这里你能看出什么问题吗?逐行观察,别放过任何一个地方。
先看第一行平均负载,过去1、5、15分钟内的平均负载依次减小,说明平均负载正在升高,而1分钟内的平均负载已经达到了系统CPU的个数,说明系统可能已经有了性能瓶颈。 Tasks 有1个正在运行的进程,但僵尸进程比较多,而且还在不停增加,说明有子进程在退出时没有没清理。 CPU使用率:用户CPU和系统CPU都不高,但iowait分别是79.5%和95.4%好像有点不正常。 每个进程的情况:CPU使用率最高的进程只有0.3%,看起来并不高;但是有两个进程处于D状态,它们可能在等待I/O,但光凭这里并不能确定是它们导致了 iowait 升高。
我们把这四个问题在汇总一下,就可以得到很明确的两点:
第一点:iowait太高了,导致系统的平均负载升高,甚至达到系统CPU的个数
第二点:僵尸进程在不断增多,说明程序没能正确清理子进程的资源
1、iowait升高的原因分析
a、用dstat 命令同时查看cpu和i/o对比情况
我相信一提到iowait升高,你首先会想要查询系统的I/O情况。那么什么工具可以查询系统的I/O情况呢?这里我推荐使用dstat,他的好处是:可以同时查看CPU和I/O这两种资源的使用情况,便于对比分析。
# 间隔 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
0 3 39 58 0| 22M 0 | 66B 342B| 0 0 | 43 144
0 0 0 100 0| 34M 0 | 200B 450B| 0 0 | 46 147
0 0 2 98 0| 34M 0 | 66B 342B| 0 0 | 45 134
0 0 0 100 0| 34M 0 | 66B 342B| 0 0 | 39 131
0 0 83 17 0|5633k 0 | 66B 342B| 0 0 | 46 168
0 3 39 59 0| 22M 0 | 66B 342B| 0 0 | 37 134
通过结果可以发现iowait升高时,磁盘读请求(read)都会很大。这说明iowait的升高跟磁盘的读请求有关,很有可能就是磁盘读导致的。
b、定位磁盘读的进程
使用top命令查看处于不可中断状态(D)的进程PID。
# 观察一会儿按 Ctrl+C 结束
$ top
...
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
4340 root 20 0 44676 4048 3432 R 0.3 0.0 0:00.05 top
4345 root 20 0 37280 33624 860 D 0.3 0.0 0:00.01 app
4344 root 20 0 37280 33624 860 D 0.3 0.4 0:00.01 app
...
c、查看对应进程的磁盘读写情况
使用pidstat命令,加上-d参数,可以看到i/o使用情况(如 pidstat -d -p <pid> 1 3),发现处于不可中断状态的进程都没有进行磁盘读写。
# -d 展示 I/O 统计数据,-p 指定进程号,间隔 1 秒输出 3 组数据
$ pidstat -d -p 4344 1 3
06: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
kB_rd/s表示每秒读的KB数
kB_wr/s表示每秒写的KB数
iodelay表示I/O的延迟(单位是时钟周期)
都是0,那就表示此时没有任何的读写,说明问题不是4344进程导致的。去掉进程号,查看所有进程的i/o情况(pidstat -d 1 20),可以定位到进行磁盘读写的进程。
d、使用pidstat命令查看所有进程的i/o情况
# 间隔 1 秒输出多组数据 (这里是 20 组)
$ pidstat -d 1 20
...
06:48:46 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
06:48:47 0 4615 0.00 0.00 0.00 1 kworker/u4:1
06:48:47 0 6080 32768.00 0.00 0.00 170 app
06:48:47 0 6081 32768.00 0.00 0.00 184 app
06:48:47 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
06:48:48 0 6080 0.00 0.00 0.00 110 app
06:48:48 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
06:48:49 0 6081 0.00 0.00 0.00 191 app
06:48:49 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
06:48:50 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
06:48:51 0 6082 32768.00 0.00 0.00 0 app
06:48:51 0 6083 32768.00 0.00 0.00 0 app
06:48:51 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
06:48:52 0 6082 32768.00 0.00 0.00 184 app
06:48:52 0 6083 32768.00 0.00 0.00 175 app
06:48:52 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
06:48:53 0 6083 0.00 0.00 0.00 105 app
...
观察可以发现,的确实app进程在进行磁盘读,并且每秒读的数据有32MB,看来就是app的问题。不过,app进程到底在执行啥I/O操作呢?
进程想要访问磁盘,就必须使用系统调用,所有接下来,重点就是要找出app进程的系统调用了。
e、使用strace查看进程的系统调用 strace -p <pid>
从pidstat的输出中拿到进程的PID,比如:6088
strace -p 6088
strace: attach: ptrace(PTRACE_SEIZE, 6088): Operation not permitted
发现报了 strace:attach :ptrace(PTRACE_SIZE,6028):Operation not peritted,说没有权限,我是使用的root权限,所以这个时候就要查看进程的状态是否正常。
f、ps aux | grep <pid> 发现进程处于Z状态,已经变成了僵尸进程
ps aux | grep 6088
root 6082 0.0 0.0 0 0 pts/0 Z+ 13:43 0:00 [app] <defunct>
僵尸进程都是已经退出的进程,所以就没法继续分析它的系统调用。关于僵尸进程的处理方法,我们一会说,现在继续分析iowait的问题。
你应该注意到了,系统的iowait的问题还在继续,但是top、pidstat这类工具已经不能给出更多的信息了。这时,我们就应该求助那些基于事件记录的动态追踪工具:perf
$ perf record -g #持续一会儿(例如15s),然后按ctrl+c退出,再运行perf report查看报告
$ perf report
接着,找到关注的app进程,按回车展开调用栈,你就可以得到如下关系图:
图里的swapper是内核中的调度进程,你可以先忽略。
查看其他信息,你可以发现,app的确在通过系统调用sys_read()读取数据。并且从new_sync_read和blkdev_direct_IO能看出,进程正在对磁盘进行直接读,也就是绕过了系统缓存,每个请求都会从磁盘直接读,这就可以解释iowait升高了。
2、僵尸进程分析
找出父进程、然后在父进程里解决
# -a 表示输出命令行选项
# p 表 PID
# s 表示指定进程的父进程
[root@luoahong ~]# pstree -aps 3084
systemd,1 --switched-root --system --deserialize 22
└─containerd,9379
└─containerd-shim,12484 -namespace moby -workdir...
└─app,4009
└─(app,3084)
运行完, 你就会发现3084进程的父进程是4009,也就是app应用。
僵尸进程出现的原因是父进程没有回收子进程的资源出现的。解决办法是找到父进程,在父进程中处理,使用pstree查父进程,然后查看父进程的源码检查wait()/waitpid()的调用或SIGCHLD信号处理函数的注册。
小结
今天用了一个多进程的案例,带你分析系统等待I/O的CPU使用率(也就是iowait%)升高的情况。
虽然这个案例是磁盘I/O导致了iowait升高,不过,iowait高不一定代表I/O有性能瓶颈。当系统中只有I/O类型的进程正在运行是,iowait也会很高,但实际上,磁盘的读写远没达到性能瓶颈的程度。
因此,碰到iowait升高时,需要先用dstat、pidstat等工具,确认是不是磁盘I/O的问题,然后再找时那些进程导致了I/O。等待I/O的进程一般是不可中断状态,所以用ps命令找到D状态(即不可中断状态)的进程,多为可疑进程。但这个案例 中,进程是僵尸进程,所以不能用strace直接分析这个进程的系统调用。
这种情况下,我们用perf工具,来分析系统的CPU时钟事件,最终发现是直接I/O导致的问题。
而僵尸进程的问题相对容易排查,使用pstree找到父进程后,就可以相应处理。