之前我们讲过,子进程退出,父进程如果还在执行中,就会造成 “ 僵尸进程 ” 的问题,进而造成内存泄漏。另外,进程一旦变成僵尸状态,那就刀枪不入,“ 杀人不眨眼 ” 的 kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
总结一下就是如下几点:
- 回收僵尸进程,避免内存泄漏
- 需要获取子进程的运行结束状态和结果(结束状态和结果不是必须的,注意区分运行状态和运行结果,两者是有区别的。)
- 尽量保证父进程要晚于子进程退出,可以规范化进行资源回收
将来我们写代码时,所有要做的事情都交给子进程,子进程把事办完了,由父进程统一回收。这点其实是与编码相关的策略,而并非属于系统级别的要求。(等后面我们讲信号部分结束就可以知道有一种方案可以让父进程既不等子进程又没有内存泄漏,就是通过 SIGCHILD 信号捕捉的时候使用 SIG_IGN 忽略处理即可!)
Ⅱ. 进程等待的方法
一、wait
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int* status);
// 返回值:成功返回被等待进程pid,失败返回-1
// 参数:status为输出型参数,获取子进程退出状态,若不关心则可以设置成为NULL 等到下面我们会详细讲 status 这个参数如何得到子进程的退出状态!
下面我们写一个代码来看看 wait() 函数接收到子进程 pid 的现象:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
// 子进程
int cnt = 3;
while(cnt)
{
printf("我是子进程:%d, 父进程:%d, cnt:%d\n", getpid(), getppid(), cnt--);
sleep(1);
}
exit(0); // 子进程退出
}
// 父进程
sleep(5);
pid_t ret = wait(NULL); // 调用wait接收子进程的pid,小于0说明失败
if(id > 0)
printf("wait success:%d\n", ret);
sleep(2);
return 0;
}
我们写一个脚本来循环打印一下进程的情况:
while :; do ps ajx | head -1 && ps ajx | grep mypro | grep -v grep; sleep 1; done
通过观察我们发现最开始为 S+ 状态即等待状态,子进程结束后未被父进程接受变成 Z+ 状态即僵尸状态,最后通过调用 wait 等待后返回子进程信息,子进程结束,只剩父进程运行。
子进程僵尸了,父进程也退出了 ❓❓❓
这种情况是不确定的。因为父进程退出,子进程会被操作系统领养。那么这个僵尸进程是在被操作系统领养后立马回收,还是积累到一定的僵尸进程再回收,这是由操作系统的策略决定的,同时也跟当前操作系统的状态有关系,如果操作系统发现内存资源已经很紧张了,就会提前回收。
二、waitpid
#include<sys/types.h>
#include<sys/wait.h>
pid_t waitpid(pid_t pid, int* status, int options);-
返回值:
- 如果正常返回时返回收集到的子进程的进程
ID - 如果调用中出错则返回
-1,这时errno会被设置成相应的值以指示错误所在 - 如果将
options设置成WNOHANG,在调用waitpid时,发现没有已退出的子进程可收集,则返回0
- 如果正常返回时返回收集到的子进程的进程
-
参数:
-
pid:因为父进程返回的是子进程的pid,所以父进程就可以等待指定的子进程,等待本质是管理的一种方式;
pid = -1:表示等待任何一个子进程,相当于wait;pid > 0:表示等待其进程ID与pid相等的子进程;
-
status:输出型参数,我们传了一个整数地址进去,最终通过指针解引用把期望的数据拿出来。与之对应的是实参传递给形参是输入型参数;
-
WIFEXITED(status): 查看进程是否正常退出,若为正常终止子进程返回的状态则为真,否则为假 -
WEXITSTATUS (status): 查看进程退出状态码,若WIFEXITED非零,提取子进程退出状态码 -
WTERMSIG(status):返回导致子进程终止的信号的编号(也可配合WIFEXITED使用,下面会讲),需要WIFSIGNALED(status)返回true,WIFSIGNALED(status)子进程被信号终止返回trueint main() { pid_t id = fork(); int status = 0; waitpid(id, &status, 0); // WIFEXITED 和 WEXITSTATUS 是两个宏 if(WIFEXITED(status)) // 判断子进程是否正常退出(判断退出信号) { printf("wait success, 退出状态:%d\n", WEXITSTATUS(status)); } else{ printf("quit error, 终止信号:%d\n", WTERMSIG(status)); } } 运行结果: exit code:0 exit code:0
-
-
options:
WNOHANG:非阻塞式等待,若pid指定的子进程没有结束,则waitpid()函数返回0,本次不予以等待,需要我们再次等待;若非正常结束,则返回该子进程的ID;若小于0,则失败了。0:阻塞式等待,相当于wait—— 子进程没退出、回收,父进程等待;
-
- 如果子进程已经退出,调用
wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。- 如果在任意时刻调用
wait/waitpid,子进程存在且正常运行,则进程可能阻塞(下面会讲阻塞)。- 如果不存在该子进程,则立即出错返回。
可以看到 waitpid() 内容比较多且杂,我们下面来解释这些参数和返回值的用法以及作用!
Ⅲ. status的意义与获取
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。- 如果传递
NULL,表示不关心子进程的退出状态信息。 - 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
status不能简单的当作整型来看待,可以当作 位图 结构来看待,具体细节如下图(只研究status低16比特位)
所以一般我们只用到 status 的低 16 位比特位,具体位置的信息获取我们可以用位操作符来获取:
- 退出状态(退出码)(
16位中的高8位):(status >> 8) & 0xFF ,当不是正常终止时候,退出状态就没有意义了,因为不会被填充! - 终止信号(
16位中的低7位):status& 0x7F ,正常的话都是0,不正常终止的话就变成了对应的终止信号! - core dump 标志:这个是异常终止时候的第
8位比特位(以后信号部分会讲,其实就是将异常终止的进程信息转化为core文件)
可以看到在 Linux 2.6.32 源码中,task_struct 里包含了退出码和终止信号:
若是被信号所杀而终止也就是非正常终止,其中 status 对应的低7位比特位对应的就是 kill -l 中的 64 个信号,如下图:
在进程退出时,终止信号是评判一个进程是否正常退出;退出状态是评判一个进程运行的结果是否正确。
-
使用
kill -l来查看对应的进程终止的信号。 -
根据退出码也就是终止信号确定进程的退出状态。
知道了如何获取对应的终止信号和退出状态,我们下面写几个程序来看看对应的输出:
① 子进程正常退出,且退出状态为10
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
// 子进程
int cnt = 3;
while(cnt)
{
printf("子进程:%d 父进程:%d cnt = %d\n", getpid(), getppid(), cnt--);
sleep(1);
}
exit(10); // 退出子进程
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(id > 0)
{
// 父进程
// 获取子进程的终止信号、退出状态
if(ret != -1)
printf("wait success,终止信号:%d,退出状态:%d\n", status & 0x7F, (status >> 8) & 0xFF);
}
sleep(3);
return 0;
}
运行结果:
[liren@VM-8-2-centos process]$ ./mypro
子进程:6989 父进程:6988 cnt = 3
子进程:6989 父进程:6988 cnt = 2
子进程:6989 父进程:6988 cnt = 1
wait success,终止信号:0,退出状态:10② 子进程因为野指针异常退出,退出状态没有意义
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
// 子进程
int cnt = 3;
while(cnt)
{
printf("子进程:%d 父进程:%d cnt = %d\n", getpid(), getppid(), cnt--);
sleep(1);
}
// 野指针,异常终止
int* ptr = NULL;
*ptr = 10;
exit(100);
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(id > 0)
{
// 父进程
if(ret != -1)
{
if((status & 0x7F) != 0)
printf("wait success,终止信号:%d,退出状态无意义\n", status&0x7F);
else
printf("wait success,终止信号:%d,退出状态:%d\n", status & 0x7F, (status >> 8) & 0xFF);
}
}
sleep(3);
return 0;
}
③ 通过宏来获取对应的退出状态或终止信号
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
// 子进程
int cnt = 3;
while(cnt)
{
printf("子进程:%d 父进程:%d cnt = %d\n", getpid(), getppid(), cnt--);
sleep(1);
}
exit(12); // 正常退出
// 野指针,异常终止
// int* ptr = NULL;
// *ptr = 10;
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(id > 0)
{
// 父进程
if(ret != -1)
{
if(WIFEXITED(status))
printf("wait success, 退出状态:%d\n", WEXITSTATUS(status));
else
printf("quit error, 终止信号:%d\n", WTERMSIG(status));
}
}
return 0;
}
Ⅳ. 图解父进程等待子进程的方式

1、调用 wait/waitpid 后,父进程只能阻塞等待子进程变成僵尸状态。
2、子进程退出后变成僵尸状态,task_struct 中的代码和数据会被释放掉,并把自己的退出信号、退出码写入到自己的 task_struct 中;
3、wait/waitpid 是系统调用,操作系统有资格也有能力去读取子进程的 task_struct。
4、父进程通过进程等待的方式,获取子进程的退出信息,回收子进程资源,让子进程结束僵尸状态。
Ⅴ. 阻塞与非阻塞式等待
阻塞式等待:当父进程调用 wait/waitpid(第三个参数为 0)等待子进程,如果子进程暂未退出,父进程会被阻塞,暂停运行,如果父进程刚好没事干,可以选择使用阻塞等待。
非阻塞式等待:当父进程调用 waitpid(第三个参数为 WNOHANG)等待子进程,如果父进程检测到子进程未退出,父进程并不会原地等待,而是继续执行自己的代码。如果使用 while 循环,便能达到轮询的效果。
非阻塞式等待的好处就是不会占用父进程太多精力,父进程可以在轮询的过程中做其他事情!
举个小例子帮助理解:
在某一时刻你需要某个人的帮忙,你打电话给小强,而小强说要稍等一会,于是你一直挂着电话没有关,跟小强说好了就说一声,这个就是 阻塞式等待,此时你的电话就无法做其它的事情;
而如果你跟小强说你等会好了告诉我一声,然后就将电话挂了,此时你也可以做其它的事情,并且你觉得等了太久了,就打电话问一下小强好了没有,但是小强还是没好,你只能又挂了电话,过了一会又打电话过去问......这个过程就叫做 轮询 ,也是我们的 非阻塞等待。
下面是非阻塞等待的代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
#define NUM 5
typedef void (*func_t)(); //函数指针
func_t handlerTask[NUM];//函数指针数组
//任务
void task1()
{
printf("任务1\n");
}
void task2()
{
printf("任务2\n");
}
void task3()
{
printf("任务3\n");
}
void loadTask()
{
memset(handlerTask, 0, sizeof(handlerTask));//将函数指针数组初始化为0
handlerTask[0] = task1;//函数指针数组handlerTask[0]存放task1的地址
handlerTask[1] = task2;
handlerTask[2] = task3;
}
int main()
{
pid_t id = fork();
assert(id != -1);
if(id == 0)
{
// 子进程:
int cnt = 3;
while(cnt)
{
printf("child running, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
sleep(1);
}
exit(10);
}
loadTask(); // 加载任务
// 父进程:
int status = 0;
while(1)//父进程对子进程状态轮询
{
pid_t ret = waitpid(id, &status, WNOHANG); // 第三个参数为0表示阻塞式等待,为WNOHANG表示非阻塞式等待
if(ret == 0) // 等于0代表没有被等待的进程暂未退出
{
printf("wait done, but child is running...., parent running other things\n");
for(int i = 0; handlerTask[i] != NULL; i++) // 遍历到NULL,即0停止
{
handlerTask[i](); // 采用回调的方式,执行我们想让父进程在空闲的时候做的事情
}
}
else if(ret > 0) // waitpid调用成功,并且子进程退出,返回值为被等待子进程的pid
{
printf("wait success, exit code: %d, sig: %d\n", (status>>8)&0xFF, status & 0x7F);
break;
}
else // 等于-1表示等待失败,waitpid中的第一个参数值传错会导致调用失败
{
printf("waitpid call failed\n");
break;
}
sleep(1);
}
return 0;
}
// 调用结果:
[liren@VM-8-2-centos process]$ ./mypro
wait done, but child is running...., parent running other things
任务1
任务2
任务3
child running, pid: 8057, ppid: 8056, cnt: 3
wait done, but child is running...., parent running other things
任务1
任务2
任务3
child running, pid: 8057, ppid: 8056, cnt: 2
wait done, but child is running...., parent running other things
任务1
任务2
任务3
child running, pid: 8057, ppid: 8056, cnt: 1
wait done, but child is running...., parent running other things
任务1
任务2
任务3
wait success, exit code: 10, sig: 0为什么现实世界中大部分选择非阻塞轮询 ❓❓❓
这种高效体现在:主要是对调用方高效,你给张三打电话,张三就要 10 分钟,那就是 10 分钟,类似于计算机,你再怎么催都没用,所以我们就不会死等,我们可以先做其它的事,反正不会让因为等待你,而让我做不了事情。
那为什么我们写的代码大部分都是阻塞调用 ❓❓❓
根本原因在于我们的代码都是单执行流,所以选择阻塞调用更简单!
为什么是 WNOHANG ❓❓❓
在服务器资源即将被吃完时,卡住了,我们一般称服务器 hang(悬挂) 住了,进而导致 宕机。所以 W 表示等待,NO 表示不要,HANG 表示卡了,所以这个宏的意思是等待时不要卡住(也就是非阻塞!)。
如何理解父进程等子进程中的 “ 等 ” ❓❓❓
所谓的等并不是把父进程放在 CPU 上,让父进程在 CPU 上边跑边等。本来父子进程都在运行队列中等待 CPU 运行,当子进程开始被 CPU 运行后,就把父进程由 R 状态更改为 !R 状态,并放入等待队列中,此时父进程就不运行了,它就在等待队列中等待。当子进程运行结束后,操作系统就会把父进程放入运行队列,并将状态更改为 R 状态,让 CPU 运行,这个过程叫做 唤醒等待 的过程。
操作系统是怎么知道子进程退出时就应该唤醒对应的父进程呢 ❓❓❓
wait 和 waitpid 是系统函数,是由操作系统提供的,你是因为调用了操作系统的代码导致你被等待了,操作系统当然知道子进程退出时该唤醒谁。
这里,我们只要能理解等待就是将当前进程放入等待队列中,将状态设置为 !R 状态。所以一般我们在平时使用计算机时,肉眼所发现的一些现象,如某些软件卡住了,根本原因是要么进程太多了,导致进程没有被 CPU 调度;要么就是进程被放到了等待队列中,长时间不会被 CPU 调度。我们曾经在写 VS 下写过一些错误代码,一旦运行,就会导致 VS 一段时间没有反应。所谓的没有反应就是因为程序导致系统出现问题,操作系统在处理问题区间,把 VS 进程设置成 !R 状态,操作系统处理完,再把 VS 唤醒。