孤儿进程和僵尸进程

孤儿进程和僵尸进程的概念,回收进程资源(wait、waitpid)。

孤儿进程

孤儿进程:父进程先于子进程结束,则子进程程伟孤儿进程,子进程的父进程成为 init 进程,称为 init 领养了这个孤儿进程。

init 进程会循环地 wait() 它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。

栗子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main(void) {
pid_t ret = fork();
if (ret == -1) {
perror("fork");
exit(-1);
} else if (ret == 0) {
// 子进程
printf("1. i' am child, my father is: %d\n", getppid());
sleep(5);
printf("2. i' am child, my father is: %d\n", getppid());
} else {
// 父进程
sleep(1);
printf("i'm going to die.\n");
}

return 0;
}

孤儿进程被init领养

僵尸进程

僵尸进程:进程终止,父进程对其不管不顾,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。

僵尸进程是不能使用 kill 命令清除掉的。因为 kill 命令指示用来终止进程的,而僵尸进程已经终止。杀掉僵尸进程的方法就是 kill 掉他的父进程。

想到个栗子:武林大哥让小弟去干一件事,小弟在办事途中意外挂了,但他留了一封血书信给大哥,告诉他大哥自己是怎么挂的,让大哥去给他报仇去。这封信就是PCB。但是如果大哥不在意这件事,这个 PCB 就会一直存在。

栗子

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 <unistd.h>
#include <stdlib.h>

int main(void) {
pid_t ret = fork();

if (ret == -1) {
perror("fork error");
exit(-1);
} else if (ret == 0) {
// 子进程
printf("child process : i'm going to die.\n");
} else {
// 父进程
while (1) {
printf("father process : pid = %d, and my son pid = %d\n", getpid(), ret);
sleep(2);
}
}

return 0;
}

运行中

僵尸进程,杀不死

回收

一个进程在终止的时候会关闭所有文件描述符,释放在用户控件分配的内存,但它的 PCB 还保留着,内核在其中保存了一些信息:如果进程是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。

这个进程的父进程可以调用 wait 或 waitpid 获取这些信息,然后彻底清除掉这个进程。

一个进程的退出状态可以在 shell 中用 $? 查看,因为 shell 是它的父进程,当它终止时 shell 调用 wait 或 waitpid 得到它的退出状态同时彻底清除掉这个进程占用的资源。

当进程终止时,操作系统的隐式回收机制:1.关闭所有文件描述符;2.释放用户空间分配的内存。内核的 PCB 仍存在。其中保存该进程的退出状态。(正常终止:退出值;异常终止:终止信号)

注意wait 和 waitpid 获取退出状态的时候并不是一个 int 都是退出的状态。在网上找到的资料画出来应该是下面这样的,这个status 是由操作系统填充的,他并不真的、简单的当做一个 int 类型。status 的低 16 位是真正我们关心的。当然也可以用宏获取这个进程的退出状态。下文中有

status

wait

1
2
3
4
5
6
7
8
#include <sys/wait.h>
pid_t wait(int *stat_loc);

返回:
成功:清理掉的子进程 ID
失败:-1(没有子进程)

参数:输出型参数,不关心子进程的退出状态可以填 NULL
  • 阻塞式的等待子进程退出
  • 回收子进程残留资源
  • 获取子进程结束状态
  • 一次 wait 只回收一个子进程

栗子1

直接回收,不关心子进程的退出码。

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
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main(void) {
pid_t ret = fork();

if (ret == -1) {
perror("fork error");
exit(-1);
} else if (ret == 0) {
// 子进程
printf("child process : i'm going to die.\n");
} else {
// 父进程
pid_t w_ret = wait(NULL);
if (w_ret == -1) {
perror("wait error");
exit(-2);
}

while (1) {
sleep(2);
}
}

return 0;
}

执行结果

ps 查看,子进程已经被回收

栗子2

使用 wait 的参数 status 来保存进程的退出状态。借助红函数来进一步判断进程终止的具体原因。宏函数在 man page 中有详细的介绍。

  1. WIFEXITED(status) 为非0,进程正常结束『wait, if exited』

    WEXITSTATUS(status) 如上宏为真,使用此宏,获取进程退出状态『wait, exit status』

  2. WIFSIGNALED(status) 为非0,进程异常终止『wait, if signaled』

    WTERMSIG(status) 如上宏为真,使用此宏,取得使进程终止的那个信号的编号『wait, term signaled』

  3. WIFSTOPPED(status) 为非0,进程处于暂停状态『wait, if stopped』

    WSTOPSIG(status) 如上宏为真,使用此宏,取得使进程暂停的那个信号的编号『wait, stop signal』

    WIFCONTINUED(status) 为真,进程暂停后已经继续运行『wait, if continued』

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
34
35
36
/*
调用 wait 回收子进程资源
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main(void) {
pid_t ret = fork();

if (ret == -1) {
perror("fork error");
exit(-1);
} else if (ret == 0) {
// 子进程
printf("child process : i'm going to die.\n");
exit(101);
} else {
// 父进程
int status = 0;
pid_t w_ret = wait(&status);
if (w_ret == -1) {
perror("wait error");
exit(-2);
}
if (WIFEXITED(status)) {
printf("child exit with %d\n", WEXITSTATUS(status));
}

while (1) {
sleep(2);
}
}

return 0;
}

获取子进程的退出状态

栗子3

获取让子进程退出的信号,奥,对还有信号这个知识还不是很了解,看看书找找资料学习学习写到博客里。kill -l查看信号

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
34
35
36
37
38
39
40
/*
调用 wait 回收子进程资源
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main(void) {
pid_t ret = fork();

if (ret == -1) {
perror("fork error");
exit(-1);
} else if (ret == 0) {
// 子进程
printf("child process : i'm going to die.\n");
sleep(100);
exit(101);
} else {
// 父进程
int status = 0;
pid_t w_ret = wait(&status);
if (w_ret == -1) {
perror("wait error");
exit(-2);
}
if (WIFEXITED(status)) {
printf("child exit with %d\n", WEXITSTATUS(status));
}
if (WIFSIGNALED(status)) {
printf("child exit by sig %d\n", WTERMSIG(status));
}

while (1) {
sleep(2);
}
}

return 0;
}

kill 子进程

kill by 15

waitpid

可以指定 pid 进行清理,可以不阻塞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *stat_loc, int options);

参数:
pid:
> 0,回收指定的子进程
-1 ,回收任意子进程
0 ,回收和当前调用 waitpid 一个组的所有子进程
< -1,回收指定进程组内的任意子进程
status:
输出参数,子进程的退出状态
options:
WNOHANG,非阻塞方式 // would no hang, 不会挂起(不会阻塞, hang 悬挂;暂停,中止)
0 ,阻塞方式
nohang

返回:
成功:返回清理掉的子进程pid
失败:-1(无子进程)
0 :参数 options 为 WNOHANG,且子进程正在运行

waitpid(-1, NULL, 0) == wait(NULL)
文章作者: ahoj
文章链接: https://ahoj.cc/2019/06/孤儿进程和僵尸进程/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 ahoj 的小本本