一、 进程状态
1. 状态含义
从 ps或者 top 命令的输出中,可以看到处于不同状态的进程
- R:Running 或 Runnable,表示进程在 CPU 的就绪队列中,正在运行或者正在等待运行
- D:Disk Sleep,不可中断状态睡眠(Uninterruptible Sleep)
- S:Interruptible Sleep,可中断状态睡眠,表示进程因为等待某个资源而被系统挂起,等到之后,会被唤醒并进入 R 状态
- I:Idle,空闲状态
- Z:Zombie ,僵尸进程,子进程实际上已经结束了,但是父进程还没有回收它的资源(比如进程的描述符、PID 等)
- T 或t:即 Stopped 或 Traced,表示进程处于暂停或者跟踪状态。
向一个进程发送 SIGSTOP 信号,它就会因响应这个信号变成暂停状态(Stopped),再向它发送 SIGCONT 信号,进程又会恢复运行。当你用调试器(如 gdb)调试一个进程时,在使用断点中断进程后,进程就会变成跟踪状态,这其实也是一种特殊的暂停状态,只不过你可以用调试器来跟踪并按需要控制进程的运行。
- X:Dead,表示进程已经消亡,所以你不会在 top 或者 ps 命令中看到它。
2. 进程组与会话
有时用ps还能看到以下状态的进程
小写的s和+是什么意思呢?不知道也没关系,查一下 man ps 就可以。s 表示这个进程是一个会话的领导进程,而 + 表示前台进程组。这里又出现了两个新概念,进程组和会话,它们用来管理一组相互关联的进程。
- 进程组表示一组相互关联的进程,比如每个子进程都是父进程所在组的成员
- 会话是指共享同一个控制终端的一个或多个进程组。
比如,我们通过 SSH 登录服务器,就会打开一个控制终端(TTY),这个控制终端就对应一个会话。而我们在终端中运行的命令以及它们的子进程,就构成了一个个的进程组,其中,在后台运行的命令,构成后台进程组;在前台运行的命令,构成前台进程组。
二、 不可中断状态
不可中断状态其实是一种保护机制,是为了保证进程数据与硬件状态一致。如果进程在进行IO操作时被随意中断,很有可能出现数据不一致问题。正常情况下,不可中断状态在很短时间内就会结束,短时间的不可中断状态进程一般可以忽略。
但如果系统或硬件发生了故障,或者 iowait 明显升高,进程可能会在不可中断状态保持很久,甚至导致系统中出现大量不可中断进程。这时,你就得注意下,系统是不是出现了 I/O 等性能问题。
三、 僵尸进程
通常,当一个进程创建了子进程后,它应该通过系统调用 wait() 或者 waitpid() 等待子进程结束,回收子进程的资源;而子进程在结束时,会向它的父进程发送 SIGCHLD 信号,所以,父进程还可以注册SIGCHLD 信号的处理函数,异步回收资源。
如果父进程没这么做,或是子进程执行太快,父进程还没来得及处理子进程状态,子进程就已经提前退出,那这时的子进程就会变成僵尸进程。通常,僵尸进程持续的时间都比较短,在父进程回收它的资源后就会消亡;或者在父进程退出后,由 init 进程回收后也会消亡。
一旦父进程没有处理子进程的终止,还一直保持运行状态,那么子进程就会一直处于僵尸状态。大量的僵尸进程会用尽 PID 进程号,导致新进程不能创建,所以这种情况一定要避免。
四、 案例
来看这个top,有发现什么奇怪的地方吗?
- 2个CPU,平均负载为2,说明此时系统已经满载,且根据时间来看有上升的趋势
- 115个僵尸进程
- CPU使用率不高但 iowait 很高,有超过90%
- 有两个app进程状态为D
根据本节及之前的知识可以推断:
- IO负载过高导致了平均负载升高,可能达到了性能瓶颈
- 僵尸进程多,说明有子进程在退出时没被清理
下面分别来看应该如何分析及处理
五、 iowait分析
什么工具可以查询系统的 I/O 情况呢?首先可以使用 dstat ,它可以同时查看 CPU 和 I/O 的使用情况,便于对比分析。
从 dstat 的输出,我们可以看到,每当 iowait 升高(wai)时,磁盘的读请求(read)都会很大。这说明 iowait 的升高跟磁盘的读请求有关,很可能就是磁盘读导致的。
运行 top 命令观察,发现有两个 D 状态的进程,PID 分别是 4344 和 4345。
接下来可以使用pidstat -d 观察指定进程 I/O 使用情况,或者直接观察所有进程的 I/O 使用情况(观察久一点)
- # -d 展示 I/O 统计数据,-p 指定进程号,间隔 1 秒,输出 3 组数据
- pidstat -d -p 4344 1 3
kB_rd 表示每秒读的 KB 数, kB_wr 表示每秒写的 KB 数,iodelay 表示I/O 的延迟(单位是时钟周期)。
可以发现,的确是 app 进程在进行磁盘读,并且每秒读的数据有 32 MB,看来就是 app 的问题。不过,app 进程到底在执行啥 I/O 操作呢?这里,我们需要回顾一下进程用户态和内核态的区别。进程想要访问磁盘,就必须使用系统调用,所以接下来,重点就是找出 app 进程的系统调用了。
strace 正是最常用的跟踪进程系统调用的工具。我们从 pidstat 的输出中拿到进程的 PID 号,比如 6082,然后在终端中运行 strace 命令,并用 -p 参数指定 PID 号:
- $ strace -p 6082
- strace: attach: ptrace(PTRACE_SEIZE, 6082): Operation not permitted
这儿出现了一个奇怪的错误,strace 命令居然失败了,并且命令报出的错误是没有权限。按理来说,我们所有操作都已经是以 root 用户运行了,为什么还会没有权限呢?
一般遇到这种问题时,我会先检查一下进程的状态是否正常。比如,继续在终端中运行 ps命令,并使用 grep 找出刚才的 6082 号进程:
- ps aux | grep 6082
- root 6082 0.0 0.0 0 0 pts/0 Z+ 13:43 0:00 [app] <defunct
果然,进程 6082 已经变成了 Z 状态,也就是僵尸进程。僵尸进程都是已经退出的进程,也就没法继续分析它的系统调用。
到这一步,你应该注意到了,系统 iowait 的问题还在继续,但是 top、pidstat 这类工具已经不能给出更多的信息了。这时,我们就应该求助那些基于事件记录的动态追踪工具了。
你可以用 perf top 看看有没有新发现,或者在终端中运行 perf record,持续一会儿(例如 15 秒),然后按 Ctrl+C 退出,再运行 perf report 查看报告:
- perf record -g
- perf report
找到我们关注的 app 进程,按回车键展开调用栈,你就会得到下面这张调用关系图
这个图里的 swapper 是内核中的调度进程,你可以先忽略掉。我们来看其他信息,你可以发现, app 的确在通过系统调用 sys_read() 读取数据。并且从new_sync_read 和 blkdev_direct_IO 能看出,进程正在对磁盘进行直接读,也就是绕过了系统缓存,每个读请求都会从磁盘直接读,这就可以解释我们观察到的 iowait 升高了。
看来,罪魁祸首是 app 内部进行了磁盘的直接 I/O 啊!
我们接下来应该从代码层面分析,究竟是哪里出现了直接读请求。查看源码文件你会发现它果然使用了 O_DIRECT 选项打开磁盘,绕过了系统缓存,直接对磁盘进行读写。在大部分情况下,我们最好还是通过系统缓存来优化磁盘I/O,换句话说,解决方法,删除 O_DIRECT 这个选项就是了。
open(disk, O_RDONLY|O_DIRECT|O_LARGEFILE, 0755)
六、 僵尸进程分析
既然僵尸进程是因为父进程没有回收子进程的资源而出现的,那么,要解决掉它们,就要找到它们的根,也就是找出父进程,然后在父进程里解决。最简单的就是运行 pstree 命令:
- # -a表示输出命令行选项,p表示PID,s表示指定进程的父进程
- pstree -aps 3084
你会发现 3084 号进程的父进程是 4009,也就是 app 应用。我们接着查看 app 应用程序的代码,看看子进程结束的处理是否正确,比如有没有调用 wait() 或 waitpid() ,或者有没有注册 SIGCHLD 信号的处理函数。
找到代码中子进程的创建和清理的地方:
循环语句本来就容易出错,你能找到这里的问题吗?这段代码虽然看起来调用了 wait() 函数等待子进程结束,但却错误地把 wait() 放到了 for 死循环的外面,也就是说,wait() 函数实际上并没被调用到,我们把它挪到 for 循环的里面就可以了。
七、 小结
- iowait 高不一定代表 I/O 有性能瓶颈。当系统中只有 I/O 类型的进程在运行时,iowait 也会很高,但实际上,磁盘的读写远没有达到性能瓶颈的程度。因此,碰到 iowait 升高时,需要先用 dstat、pidstat 等工具,确认是不是磁盘 I/O 的问题,然后再找是哪些进程导致了 I/O。
- 等待 I/O 的进程一般是不可中断状态,所以用 ps 命令找到的 D 状态的进程,多为可疑进程。但这个案例中,在 I/O 操作后,进程又变成了僵尸进程,所以不能用 strace 直接分析这个进程的系统调用。这种情况下,我们用了 perf 工具,来分析系统的 CPU 时钟事件,最终发现是直接 I/O 导致的问题。这时,再检查源码中对应位置的问题,就很轻松了。
- 而僵尸进程的问题相对容易排查,使用 pstree 找出父进程后,去查看父进程的代码,检查wait() / waitpid() 的调用,或是 SIGCHLD 信号处理函数的注册就行了