Linux ---- 进程等待
文章目录
一、fork 之后的问题
1. 如何保证每个进程有一个父进程?
在说明 fork 函数时,子进程是在父进程调用 fork 后生成的。子进程将其终止状态返回给父进程。但是如果父进程在子进程结束前终止,也就是对于父进已经终止的所有进程,他们的父进程都改变为 init 进程,这也叫这些进程由init 进程收养。在说明 fork 函数时,子进程是在父进程调用 fork 后生成的。子进程将其终止状态返回给父进程。但是如果父进程在子进程结束前终止,也就是对于父进已经终止的所有进程,他们的父进程都改变为 init 进程,这也叫这些进程由init 进程收养。
在一个进程终止时,内核逐个检查所有活动进程,以判断它是否是正要终止进程的子进程,如果是,则该进程的父进程 ID 就改为 1(init 进程的 ID)。
2. 为什么要进行进程等待?
如果子进程在父进程之前终止,在这种情况下,如果子进程完全消失了,那父进程就无法获取它的终止状态了。
所以内核为每个终止子进程保存了一定量的信息,当终止进程的父进程调用 wait 或 waitpid 时,可以得到这些信息。
这些信息至少包括:
- 进程 ID
- 该进程的终止状态
- 该进程使用 CPU 时间总量
3. 不进行进程等待会有后果?
僵尸进程(zombie):一个已经终止、但其父进程尚未对其做善后处理(获取终止子进程的有关信息,释放它所占用的资源)。
僵尸进程的危害:占用系统资源;内存泄漏
我们写了一个程序:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t pid = fork(); // 需要包含头文件 <unistd.h>
if(pid == 0)
{// child
}
else if(pid > 0)
{// father
sleep(1000);
while(1)
{
// 这个死循环的目的是为了方便查看进程状态
}
}
else
{//error
perror("fork"); // 需要包含 <stdio.h>
exit(1); // 需要包含头文件 <stdlib.h>
}
return 0;
}
我们 gcc 编译(编译环境:CentOS 7),然后 ./a.out 行
我们通过指令 ps aux | grep a.out 查找刚刚执行的程序
我们看到,ps 命令将僵死进程的状态打印为 Z
4. 一个由 init 进程收养的进程终止时会发生什么?
首先我们考虑会不会变成僵尸进程?答案是不会,因为 init 被编写成无论何时只要有一个子进程终止, init 就会调用一个 wait 函数取得其终止状态。这就防止了在系统中塞满僵尸进程。
二、 wait 函数
- 函数原型:
pid_t wait(int * status); - 需要包含头文件 #include <sys/wait.h>
- 函数作用:等待子进程,避免僵尸进程
- 参数 status :
这是一个输出型参数,保存子进程的退出状态,但是这并不是进程的退出码
上图只画了2字节,那我们就可以写个小程序来获取子进程的退出码或终止信号。
正常终止:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
pid_t pid = fork(); // 需要包含头文件 <unistd.h>
int status = 0;
wait(&status);
if(pid == 0)
{// child
printf("child pid = %d ", getpid());
fflush(stdout);
exit(3); // 正常终止
// while(1){} // 把上行代码替换掉,通过键盘发送信号
}
else if(pid > 0)
{// father
if(status & 0xff)
{
printf("子进程异常终止 singnal = %d\n", status & 0x7f);
}
else
{
printf("子进程正常终止 exit = %d\n", (status >> 8) & 0xff);
}
sleep(1000);
while(1)
{
// 这个死循环的目的是为了方便查看进程状态
}
}
else
{//error
perror("fork"); // 需要包含 <stdio.h>
exit(1); // 需要包含头文件 <stdlib.h>
}
return 0;
}
程序运行结果:
异常终止:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
pid_t pid = fork(); // 需要包含头文件 <unistd.h>
int status = 0;
wait(&status);
if(pid == 0)
{// child
printf("child pid = %d ", getpid());
fflush(stdout);
//exit(3); // 正常终止
while(1){} // 把上行代码替换掉,通过键盘发送信号
}
else if(pid > 0)
{// father
if(status & 0xff)
{
printf("子进程异常终止 singnal = %d\n", status & 0x7f);
}
else
{
printf("子进程正常终止 exit = %d\n", (status >> 8) & 0xff);
}
sleep(1000);
while(1)
{
// 这个死循环的目的是为了方便查看进程状态
}
}
else
{//error
perror("fork"); // 需要包含 <stdio.h>
exit(1); // 需要包含头文件 <stdlib.h>
}
return 0;
}
给子进程发送 9 号信号
程序运行结果:
大家可以自己试试 ?
如果不关心子进程的终止状态,传 NULL 即可。
其实 POSIX.1 规定了几个互斥的宏来取得进程终止的原因
宏 | 说明 |
---|---|
WIFEXITED(status) | 若为正常终止子进程返回的状态,则为真 |
WIFSIGNALED(status) | 若为异常终止子进程返回的状态,则为真 |
WIFSTOPPED(status) | 若为当前暂停子进程返回的状态,则为真 |
WIFCONTINYED(status) | 若为作业控制暂停后已经继续的的子进程的返回状态,则为真 |
我们来写个小程序测试一把:
// 演示不同的 exit 值
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
void print_exit(int status);
int main()
{
pid_t pid;
int status;
if((pid = fork()) < 0)
{
perror("fork");
exit(1); // 出错返回
}
else if(pid == 0)
{// child
exit(7); // 子进程退出
}
if(wait(&status) != pid)
{
perror("wait");
exit(2); // 出错返回
}
print_exit(status);
if((pid = fork()) < 0)
{
perror("fork");
exit(1); // 出错返回
}
else if(pid == 0)
{//child
abort();
}
if(wait(&status) != pid)
{
perror("wait");
exit(3); // 出错返回
}
print_exit(status);
if((pid = fork()) < 0)
{
perror("fork");
exit(1); // 出错返回
}
else if(pid == 0)
{//child
status /= 0;
}
if(wait(&status) != pid)
{
perror("wait");
exit(4); // 出错返回
}
print_exit(status);
return 0;
}
void print_exit(int status)
{
if(WIFEXITED(status))
{
printf("normal termination, exit status = %d\n", WIFEXITED(status));
}
else if(WIFSIGNALED(status))
{
printf("abnormal termination, signal number = %d%s\n", WIFSIGNALED(status),
#ifdef WCOREDUMP
WCOREDUMP(status) ? "core file generated" : "");
#else
"");
#endif
}
else if(WIFSTOPPED(status))
{
printf("child stopped, signal number = %d\n", WIFSTOPPED(status));
}
}
编译运行(编译环境 CentOS 7):
在编译时报了一个警告,是因为除 0 了,但是这是我们需要的,无需担心。
- 返回值:
若成功,返回进程 ID
若出错,返回 0 或 -1
wait 函数的有几个需要注意的点:
- wait 只等待一个子进程,并且是阻塞式的等
- 如果有多个子进程,则需要使用对应个数的 wait 函数
- 如果一个子进程已终止,正等待父进程获取其状态,则取得该进程的终止状态立即返回。
- 如果没有任何子进程,出错返回。
三、 waitpid 函数
- 函数原型:
pid_t waitpid(pid_t pid, int * status, int options) - 需要包含的头文件: #include <sys/wait.h>
- 函数作用:等待子进程,避免僵尸进程
- 参数 pid
pid 的值 | 使用说明 |
---|---|
pid > 0 | 只等待进程 ID 等于 pid 的子进程,不管其他已经有多少子进程运行结束退出了,只要等待的子进程还没结束,waitpid 就会一直等下去 |
pid = -1 | 等待任何一个子进程退出,没有任何限制,此时 waitpid 和 wait 作用一样 |
pid = 0 | 等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid 不会对它做任何理睬 |
pid < -1 | 等待一个指定进程组中的任何子进程,这个进程组的 ID 等于 pid 的绝对值 |
- 参数 status
与 wait 函数的 status 一样,将子进程的终止状态存放在 status 指向的存储单元中。 - options
可以控制调用者是否阻塞
options | 使用说明 |
---|---|
WNOHANG | 若由 pid 指定的子进程状态未发生改变(没有结束),则 waitpid 不阻塞,立即返回 0 |
WUNTRACED | 返回终止子进程信息和因信号停止的子进程信息 |
WCONTINUED | 返回收到 SIGCONT 信号而恢复执行的已终止子进程状态信息 |
- 返回值
若成功,返回终止子进程的进程 ID
若出错,返回 0 或 -1
waitpid 函数提供了 wait 函数没有提供的 3 个功能。
- waitpid 可等待一个特定的进程,而 wait 则返回任一终止子进程的状态。
- waitpid 提供了一个 wait 的非阻塞版本。
- waitpid 通过 WUNTRACED 和 WCONTINUED 选项支持作业控制。
四、fork 两次避免僵尸进程
如果一个进程 fork 一个子进程,但不要它等待子进程终止,也不希望子进程处于僵死状态直到父进程终止,实现这一需求的诀窍是调用 fork 两次。
看代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid;
if((pid = fork()) < 0)
{
perror("fork");
exit(1);// 出错返回
}
else if(pid == 0)
{ // first child
if((pid = fork()) < 0)
{
perror("fork");
exit(1);
}
else if(pid > 0)
{
exit(0); // parent from sencond fork == first child
}
/*
* We're the second child; our parent becomes init as soon as our real parent calls exit() in the statement above.
* Here's where we'd continue executing, knowing that when we're done, init will reap our status.
* 我们是第二个孩子;在上面的语句中,一旦真正的父类调用exit(),父类就变成了init。这就是我们继续执行的地方,我们知道当我们完成时,init将获得我们的状态
* */
sleep(2);
printf("second child, parent pid = %ld\n", (long)getpid());
exit(0);
}
if(waitpid(pid, NULL, 0) != pid)
{
perror("waitpid");
exit(2);
}
/*
* We're the parent (the original process); we continue executing,
* knowing that we're not the parent of the second child.
* 我们是父进程(原始进程);我们继续执行,知道我们不是第二个孩子的父亲。
* */
return 0;
}
编译(编译环境 CentOS 7)执行得到:
第二个子进程调用 sleep 以保证在打印父进程 ID 时第一个子进程已终止。在 fork 之后,父进程和子进程都可继续执行,并且我们无法预知哪一个会先执行。在 fork 之后,如果不使第二个子进程休眠,那么它可能比其父进程先执行,于是它打印的父进程 ID 将是创建它的父进程,而不是 init 进程(进程 ID 1)。
注意,当原先的进程(也就是 exec 本程序的进程)终止时,shell 打印提示符,这在第二个子进程打印父进程 ID 之前。