1 进程状态
在使用top
或ps
查看进程信息时,有一列字段用来标识进程当前的状态。以ps
为例:
进程状态一般用单个大写字母来表示,具体每种状态表示什么含义,为什么ps
显示出来的状态有多个字母,这个查阅官方文档(https://man7.org/linux/man-pages/man1/ps.1.html)中的PROCESS STATE CODE部分即可。
这些命令本质上是从进程的/proc/PID/status
目录下读取的状态信息,因此也可参考 https://man7.org/linux/man-pages/man5/proc.5.html 了解所有状态的演化发展情况。
几种常见进程状态含义如下:
- R:进程正在运行或处于可运行状态,只要有时间片了,就会调度到 CPU 上运行
- S:可中断的 sleep 状态,进程调用了 sleep 或在等待网络 IO 事件
- D:不可中断的 sleep 状态,一般是进程在等待磁盘 IO(这种一般也 kill 不掉,除非阻塞解除)
- T:收到了 STOP 信号,处于暂停状态(在命令行中按下
Ctrl+z
就会让进程进入这个状态)
- t:进程被 ptrace 调试导致的暂停(GDB 底层用的 ptrace)
- Z:僵尸进程,进程的生命周期已经结束了,但内核中还保留了一些关于它的信息,没有被彻底回收
2 僵尸进程如何产生
进程状态为 Z 的进程,就是僵尸进程。
当一个进程终止(正常或异常)后,该进程使用的系统资源会被回收,只在内核中保留了很少的信息,主要记录了一些退出状态和退出原因。这个时候进程状态就会切换为 Z,需要父进程调用 wait() 来让系统彻底回收资源。
这么设计是因为在一些场景中,父进程需要感知子进程退出的原因,调用 wait() 的同时也能拿到子进程退出时的信息,这就是为什么在 bash 中可以通过echo $?
拿到上一条命令的退出码。
但是在一些特殊情况下,父进程没能正确回收子进程的资源(例如,执行到什么逻辑的时候卡住了),此时子进程会一直维持 Z 状态,这时就能通过系统命令捕获到僵尸进程了。
虽然僵尸进程不会被调度,大部分系统资源也已经释放,但是它会占用一个进程 PID,如果系统中有大量的僵尸进程产生,会耗尽 PID 资源,导致新进程无法派生。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
int pid = fork();
if (pid != 0) {
printf("father process is stucking...\n");
sleep(60);
} else {
printf("child process pid: %d\n", getpid());
return 0;
}
return 0;
}
|
可以看到,父进程在 sleep,而子进程已经退出了,此时状态为 Z,成为了僵尸进程。
但是过一会,再使用ps
查看进程状态时,发现僵尸进程已经消失了。这是因为当父进程退出后,僵尸子进程会挂到systemd
进程下,systemd
会周期调用wait()
来回收子进程,最终清除了刚才的僵尸进程。
https://unix.stackexchange.com/questions/155012/how-does-linux-handles-zombie-process
3 如何处理僵尸进程
3.1 直接同步调用 wait()
最直接的办法就是在父进程的逻辑中调用 wait() 来回收终止的子进程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
int pid = fork();
if (pid != 0) {
printf("father process is stucking...\n");
int child_pid = wait(NULL); // 这里传入非 NULL 可以获取更多子进程信息,这里不讨论
printf("child process(%d) is finished, reap it\n", child_pid);
sleep(60);
} else {
printf("child process pid: %d\n", getpid());
return 0;
}
return 0;
}
|
这个时候就没有僵尸进程了。
直接使用 wait() 调用有如下几个缺点:
- 该调用是阻塞的,如果子进程一直不退出,父进程调用后就会卡住,无法执行其它逻辑
- 如果有多个子进程,该调用无法指定回收并获取具体的某个子进程,每次可操作的都是最先终止的
于是可以使用 watipid() 来进行更好的控制。它拥有更多的控制参数,可以让父进程只阻塞在特定的子进程上,也可以设置为不阻塞,通过轮询的的方式来回收子进程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
int pid = fork();
if (pid != 0) { // father
int ret = 0;
while (ret == 0) {
printf("father process do something else...\n");
sleep(1);
ret = waitpid(pid, NULL, WNOHANG);
}
printf("child process(%d) is finished, reap it\n", ret);
} else {
printf("child process pid: %d\n", getpid());
sleep(5);
return 0;
}
return 0;
}
|
3.2 使用信号机制异步处理
当有子进程终止时,内核会向它的父进程发送一个SIGCHLD
信号,默认该信号是被忽略的,我们可以给该信号注册一个 handler,在 handler 的逻辑中回收子进程。
当信号处理函数被调用时,接下来的SIGCHLD
会被默认屏蔽,如果在处理信号的时候,又有子进程退出了,产生的信号就捕获不到了。因此,在 handler 中要考虑尽可能回收所有子进程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
void handle_child_exit(int sig)
{
int ret = 0;
while ((ret = waitpid(-1, NULL, WNOHANG)) > 0) {
printf("child process(%d) is finished, reap it\n", ret);
continue;
}
return;
}
int main()
{
signal(SIGCHLD, handle_child_exit); // call this before fork()
int pid = fork();
if (pid != 0) { // father
while (1) {
printf("father process do something else...\n");
sleep(1);
}
} else {
printf("child process pid: %d\n", getpid());
sleep(2);
return 0;
}
return 0;
}
|