07 | 大量不可中断进程和僵尸进程(上)

177 阅读7分钟

1. 概述

在我的职业生涯中,确实还没有碰到过僵尸进程。不过该学习的还是要学一下,万一以后碰到了呢?

重温一下常见的进程状态:

top命令如下

top - 13:06:10 up 12:02,  2 users,  load average: 0.00, 0.06, 0.76
Tasks: 109 total,   1 running,  59 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.0 us,  0.0 sy,  0.0 ni,100.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  8070116 total,   711064 free,  6883448 used,   475604 buff/cache
KiB Swap:  4194300 total,  4175856 free,    18444 used.   938196 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
    1 root      20   0  159956   6940   4784 S   0.0  0.1   0:04.48 systemd
    2 root      20   0       0      0      0 S   0.0  0.0   0:00.00 kthreadd
    4 root       0 -20       0      0      0 I   0.0  0.0   0:00.00 kworker/0:0H
    6 root       0 -20       0      0      0 I   0.0  0.0   0:00.00 mm_percpu_wq
    7 root      20   0       0      0      0 S   0.0  0.0   0:05.34 ksoftirqd/0
    8 root      20   0       0      0      0 I   0.0  0.0   0:02.04 rcu_sched
    9 root      20   0       0      0      0 I   0.0  0.0   0:00.00 rcu_bh
   10 root      rt   0       0      0      0 S   0.0  0.0   0:00.28 migration/0
   11 root      rt   0       0      0      0 S   0.0  0.0   0:00.06 watchdog/0
   12 root      20   0       0      0      0 S   0.0  0.0   0:00.00 cpuhp/0
  • 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 的缩写,表示进程处于暂停或者跟踪状态。

在 Unix 系统中,"T" 或 "t" 代表进程处于暂停或跟踪状态。发送 SIGSTOP 信号给进程,它会进入暂停状态(Stopped);发送 SIGCONT 信号,它会恢复运行。如果是用调试器(如 gdb)调试,进程在触发断点后会进入跟踪状态,这也是一种暂停状态,但允许使用调试器进行控制和跟踪进程。

  • X,也就是 Dead 的缩写,表示进程已经消亡,所以你不会在 top 或者 ps 命令中看到它。

不可中断一般是在等待I/O的情况较多,大部分场景下因为I/O速度远比内存慢。

再看僵尸进程,这是多进程应用很容易碰到的问题。正常情况下,当一个进程创建了子进程后,它应该通过系统调用 wait() 或者 waitpid() 等待子进程结束,回收子进程的资源;而子进程在结束时,会向它的父进程发送 SIGCHLD 信号,所以,父进程还可以注册 SIGCHLD 信号的处理函数,异步回收资源。

一旦父进程没有处理子进程的终止,还一直保持运行状态,那么子进程就会一直处于僵尸状态。大量的僵尸进程会用尽 PID 进程号,导致新进程不能创建,所以这种情况一定要避免。

2. 案例

环境准备:

  • 操作系统:Ubuntu 18.04
  • 机器配置:2 CPU,8GB 内存
  • 预先安装 docker、sysstat、dstat 等工具,如 apt -y install docker.io dstat sysstat

这里,dstat 是一个新的性能工具,它吸收了 vmstat、iostat、ifstat 等几种工具的优点,可以同时观察系统的 CPU、磁盘 I/O、网络以及内存使用情况。

执行案例

$ docker run --privileged --name=app -itd feisky/app:iowait

检查启动是否成功

root@calvin:~# ps aux | grep /app
root     32333  0.0  0.0   4512  1492 pts/0    Ss+  13:50   0:00 /app
root     32414  0.0  0.8  70052 65840 pts/0    D+   13:51   0:00 /app
root     32415  0.0  0.8  70052 65840 pts/0    D+   13:51   0:00 /app
root     32417  0.0  0.0  13140  1088 pts/0    S+   13:51   0:00 grep --color=auto /app

在这个界面中,多个 app 进程的状态分别是 "Ss+" 和 "D+"。其中:

  • S 表示可中断睡眠状态,D 表示不可中断睡眠状态。
  • s 表示进程是会话的领导进程。
  • + 表示该进程属于前台进程组。

进程组是一组相互关联的进程,会话则是共享同一控制终端的一个或多个进程组。例如,通过 SSH 登录时,会打开一个控制终端(TTY),这个控制终端就是一个会话,里面的进程组根据是否在前台或后台运行而不同。

明白了这些,我们再用 top 看一下系统的资源使用情况(top之后按一次键盘 1):

root@calvin:~# top
top - 14:06:27 up 13:02,  2 users,  load average: 2.00, 1.47, 0.70
Tasks: 494 total,   1 running,  61 sleeping,   0 stopped, 384 zombie
%Cpu0  :  0.0 us,  1.7 sy,  0.0 ni, 78.1 id, 20.2 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu1  :  0.0 us,  2.6 sy,  0.0 ni, 93.7 id,  3.6 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  8070116 total,   622640 free,  6660536 used,   786940 buff/cache
KiB Swap:  4194300 total,  4175856 free,    18444 used.  1156660 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
  307 root      20   0       0      0      0 Z   1.7  0.0   0:00.05 app
  308 root      20   0       0      0      0 Z   1.7  0.0   0:00.05 app
    7 root      20   0       0      0      0 S   0.3  0.0   0:05.44 ksoftirqd/0
    1 root      20   0  159956   6940   4784 S   0.0  0.1   0:04.50 systemd
    2 root      20   0       0      0      0 S   0.0  0.0   0:00.00 kthreadd
    4 root       0 -20       0      0      0 I   0.0  0.0   0:00.00 kworker/0:0H
    6 root       0 -20       0      0      0 I   0.0  0.0   0:00.00 mm_percpu_wq
    8 root      20   0       0      0      0 I   0.0  0.0   0:02.09 rcu_sched
    9 root      20   0       0      0      0 I   0.0  0.0   0:00.00 rcu_bh
   10 root      rt   0       0      0      0 S   0.0  0.0   0:00.28 migration/0
   11 root      rt   0       0      0      0 S   0.0  0.0   0:00.07 watchdog/0
  • 先看第一行的平均负载( Load Average),过去 1 分钟、5 分钟和 15 分钟内的平均负载在依次减小,说明平均负载正在升高;而 1 分钟内的平均负载已经达到系统的 CPU 个数,说明系统很可能已经有了性能瓶颈。
  • 再看第二行的 Tasks,有 1 个正在运行的进程,但僵尸进程比较多,而且还在不停增加,说明有子进程在退出时没被清理。
  • 接下来看两个 CPU 的使用率情况,用户 CPU 和系统 CPU 都不高,但 iowait 分别是 20.2% 和 3.6%,好像有点儿不正常。
  • 最后再看每个进程的情况, CPU 使用率最高的进程只有 1.7%,看起来并不高;但有两个进程处于 Z(前一秒还是D) 状态,它们可能在等待 I/O,但光凭这里并不能确定是它们导致了 iowait 升高。

到此我们可以得出两个结论:

  • iowait 太高了,导致系统的平均负载升高,甚至达到了系统 CPU 的个数。
  • 僵尸进程在不断增多,说明有程序没能正确清理子进程的资源。

未完待续。。。