一、进程的状态

当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进程,按回车展开调用栈,你就可以得到如下关系图:

Linux性能优化实战学习笔记五-不可中断进程和僵尸进程_top

图里的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找到父进程后,就可以相应处理。