文章目录
- 1、进程等待
- 1.1 为什么要进程等待
- 1.2 进程等待必要性
- 1.3 进程等待的方法
- 1.3.1 wait方法
- 1.3.2 waitpid方法
- 1.4 获取子进程的status
- 1.5 waitpid的第三个参数options
1、进程等待
1.1 为什么要进程等待
- 解决子进程僵尸问题带来的内存泄漏问题。
- 子进程将父进程交给的任务完成的如何,父进程需要通过进程等待的方式,获取子进程退出的信息。本质是获取 退出码和信号编号。
1.2 进程等待必要性
- 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏.
- 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
- 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
- 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
1.3 进程等待的方法
1.3.1 wait方法
我们首先查看一个wait接口,并简单介绍一下:
wait这里先不说参数,下面waitpid会谈,可以传NULL。
我们了解了用法,接下来就开始写代码试试:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
void Worker()
{
int cnt = 5;
while(cnt)
{
printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
sleep(1);
}
}
int main()
{
pid_t id = fork();
if(0 == id)
{
// child
Worker();
exit(0);
}
else
{
sleep(6); // 不会让父进程立即回收子进程
// father
pid_t rid = wait(NULL);
if(rid == id)
{
printf("wait success, pid: %d\n", getpid());
}
sleep(2); // 最后只剩父进程跑
}
return 0;
}
以上的测试验证了,进程等待是可以回收僵尸进程的!
我们再来看,如果我们不让父进程回收前休眠,直接就是回收子进程会怎么样,测试一下:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
void Worker()
{
int cnt = 5;
while(cnt)
{
printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
sleep(1);
}
}
int main()
{
pid_t id = fork();
if(0 == id)
{
// child
Worker();
exit(0);
}
else
{
//sleep(6); // 不会让父进程立即回收子进程
// father
// wait before和wait after连着打出来那么就说明直接没有在等待时卡住
printf("wait before\n");
pid_t rid = wait(NULL);
printf("wait after\n");
if(rid == id)
{
printf("wait success, pid: %d\n", getpid());
}
sleep(2); // 最后只剩父进程跑
}
return 0;
}
我们发现,子进程先跑完才打印的wait after,并且这次子进程没有显示出僵尸状态(变成僵尸立即被回收),这说明:如果子进程没有退出,父进程必须在wait上进行阻塞等待,直到子进程僵尸,wait自动回收,返回!
一般而言,谁先运行不知道,但是父进程一般都是最后退出(wait阻塞等待)!
1.3.2 waitpid方法
还是先了解一下接口再说:
我们继续写代码试试:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
void Worker()
{
int cnt = 5;
while(cnt)
{
printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
sleep(1);
}
}
int main()
{
pid_t id = fork();
if(0 == id)
{
// child
Worker();
exit(0);
}
else
{
sleep(6); // 不会让父进程立即回收子进程
// father
printf("wait before\n");
pid_t rid = waitpid(id, NULL, 0); // 等待子进程
printf("wait after\n");
if(rid == id)
{
printf("wait success, pid: %d\n", getpid());
}
sleep(2); // 最后只剩父进程跑
}
return 0;
}
这里可以看到子进程僵尸状态一下就别回收了。
我们这里讲一下waitpid的第二个参数:它是一个整型指针变量,做输出型参数,os将数值赋值给status。它是将子进程的退出码写入然后带回来。
我们继续写代码测试:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
void Worker()
{
int cnt = 5;
while(cnt)
{
printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
sleep(1);
}
}
int main()
{
pid_t id = fork();
if(0 == id)
{
// child
Worker();
exit(10);
}
else
{
// father
printf("wait before\n");
int status = 0;
pid_t rid = waitpid(id, &status, 0);
printf("wait after\n");
if(rid == id)
{
printf("wait success, pid: %d, status: %d\n", getpid(), status);
}
sleep(2); // 最后只剩父进程跑
}
return 0;
}
我们子进程退出码为10,但是父进程接收后打印出来不是10,而是2560!怎么回事?我们看下一个小节,里面细讲!!!
1.4 获取子进程的status
status是一个int的整数,32位,但是status只取低16位。
今天先不谈core dump标志。我们刚给子进程的退出码设置的是10,由上面的图可以看出,status的低16位的次低8位代表了退出状态,因此10的二进制表示在次低8位,10二进制序列为1010,所以16位的表示为0000 1010 0000 0000。
因此,我们知道了,status不能直接使用,得我们对二进制序列经过位运算后才能得出正确的结果。 下面我们就从三个测试入手:代码跑完结果对,代码跑完结果不对,异常。
代码跑完结果对:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
void Worker()
{
int cnt = 5;
while(cnt)
{
printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
sleep(1);
}
}
int main()
{
pid_t id = fork();
if(0 == id)
{
// child
Worker();
exit(10);
}
else
{
// father
printf("wait before\n");
int status = 0;
pid_t rid = waitpid(id, &status, 0);
printf("wait after\n");
if(rid == id)
{
printf("wait success, pid: %d, rid: %d, exit sig: %d, exit code: %d\n", getpid(), rid, status&0x7F, (status>>8)&0xFF);
}
sleep(2); // 最后只剩父进程跑
}
return 0;
}
这里我们退出码设置的10,所以是我们的预料之内。
所以,信号为0,退出码为0,代表代码跑完结果正常。
异常:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
void Worker()
{
int cnt = 5;
while(cnt)
{
printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
sleep(1);
}
int a = 10;
a /= 0;
}
int main()
{
pid_t id = fork();
if(0 == id)
{
// child
Worker();
exit(10);
}
else
{
// father
printf("wait before\n");
int status = 0;
pid_t rid = waitpid(id, &status, 0);
printf("wait after\n");
if(rid == id)
{
printf("wait success, pid: %d, rid: %d, exit sig: %d, exit code: %d\n", getpid(), rid, status&0x7F, (status>>8)&0xFF);
}
sleep(2); // 最后只剩父进程跑
}
return 0;
}
我们故意写了一个除零错误,信号就是8,信号8为SIGFPE。
但是退出状态为0,这里 信号为非0,退出码为0,代表异常!
所以, 代码跑完结果不对的表现就是,信号为0,退出码为非0!
父进程如何得知子进程的退出信息呢?原理是什么?
我们来看下面的图:
还有一个 问题:父进程在等待子进程的时候是阻塞状态,那么它是在哪里阻塞呢?
PCB中也维护了等待队列,当父进程等待子进程的时候,父进程的PCB会被链入到子进程的等待队列中,等子进程结束了,父进程的PCB就被链入到运行队列中,等待调度,被调度后立马回收子进程的PCB。这就叫做 软件条件。
大家记住,进程只要是阻塞了,就会被链入到等待队列中。
上面我们拿到退出码和信号是我们自己手动使用位操作实现的,那么下面我们来介绍一下Linux下封装好的系统调用,以后我们直接调用即可。
我们继续写代码测试一下:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
void Worker()
{
int cnt = 5;
while(cnt)
{
printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
sleep(1);
}
}
int main()
{
pid_t id = fork();
if(0 == id)
{
// child
Worker();
exit(1);
}
else
{
//sleep(6); // 不会让父进程立即回收子进程
// father
printf("wait before\n");
//pid_t rid = wait(NULL);
int status = 0;
pid_t rid = waitpid(id, &status, 0);
printf("wait after\n");
if(rid == id)
{
if(WIFEXITED(status))
{
printf("child process normal quit, exit code: %d\n", WEXITSTATUS(status));
}
else
{
printf("child process quit except!\n");
}
}
sleep(2); // 最后只剩父进程跑
}
return 0;
}
这里应该有人会疑惑,获取子进程的status这么麻烦,为什么不定义个全局变量直接拿呢?
如果定义一个全局变量,子进程势必需要对变量在自己的进程中写入,但是父子进程是两个进程,进程具有独立性,子进程对数据的更改,父进程是看不到的!!!
1.5 waitpid的第三个参数options
我们先来说waitpid的返回值,返回值分三类:
- 0<:等待成功;
- == 0:等待是成功的,但是进程还没有退出;
- <0:等待失败。
我们之前说options直接给0就可以,这里说一下,**0代表的是阻塞等待(子进程不退出,wait不返回)!**下面再说其他的:
WNOHANG:等待的时候,以非阻塞(等待条件不满足,不阻塞,直接返回)的方式等待!非阻塞等待,往往要进行重复调用(轮询)。可以顺便做一些占据时间并不多的事情!非阻塞等待可以与返回值并用,实现轮询。
这里我们来试一下非阻塞等待,并使用轮询的方式等待:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
void Worker(int cnt)
{
printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
sleep(1);
}
// waitpid第三个参数测试的单个子进程
int main()
{
pid_t id = fork();
if(0 == id)
{
// child
int cnt = 3;
while(cnt)
{
Worker(cnt);
cnt--;
}
exit(0);
}
// father
while(1)
{
int status = 0;
pid_t rid = waitpid(id, &status, WNOHANG);
if(rid > 0) // waitid返回值大于0,等待成功
{
printf("child quit success, exit code: %d, exit signal: %d\n", (status>>8)&0xFF, status&0x7F);
break;
}
else if(rid == 0) // 返回值等于0,等待成功,但是子进程没有退出,可以做占用时间少的事
{
printf("father do other thing...\n");
sleep(1);
}
else // 等待失败
{
printf("wait failed!\n");
break;
}
}
return 0;
}
我们能看到父进程最后退出。这就是非阻塞轮询等待。这样的方式等待,父进程就可以去做一些自己的事情,这样就可以更好的将时间利用起来!
我们来写一个这样的场景:
这里我们定义一个函数指针,里面存放一些函数指针,使用回调方式将代码封装一下:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#define TASK_NUM 5
typedef void (*task_t)();
/
void download()
{
printf("this is a download task is runing!\n");
}
void printLog()
{
printf("this is a write log task is runing!\n");
}
void show()
{
printf("this is a show info task is runing!\n");
}
/
void initTasks(task_t tasks[], int num)
{
for(int i = 0; i < num; i++) tasks[i]= NULL;
}
int addTask(task_t tasks[], task_t t)
{
int i = 0;
for(; i < TASK_NUM; i++) // 循环+判断将函数指针依次放入函数指针数组中
{
if(tasks[i] == NULL)
{
tasks[i] = t;
return 1;
}
}
return 0;
}
void executeTask(task_t tasks[], int num)
{
for(int i = 0; i < num; i++)
{
if(tasks[i]) tasks[i]();
}
}
void worker(int cnt)
{
printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
}
// waitpid第三个参数测试的单个子进程
int main()
{
task_t tasks[TASK_NUM]; // 函数指针数组
initTasks(tasks, TASK_NUM);
addTask(tasks, download);
addTask(tasks, printLog);
addTask(tasks, show);
pid_t id = fork();
if(0 == id)
{
// child
int cnt = 3;
while(cnt)
{
worker(cnt);
sleep(1);
cnt--;
}
exit(0);
}
// father
while(1)
{
int status = 0;
// 非阻塞等待,可以让等待方在等待的时候,顺便做做自己的事情
pid_t rid = waitpid(id, &status, WNOHANG);
if(rid > 0)
{// waitid返回值大于0,等待成功
printf("child quit success, exit code: %d, exit signal: %d\n", (status>>8)&0xFF, status&0x7F);
break;
}
else if(rid == 0)
{// 返回值等于0,等待成功,但是子进程没有退出,可以做自己的事
printf("####################################\n"); // 分隔符
printf("father do other thing...\n");
// 该函数内部,其实是函数回调方式
executeTask(tasks, TASK_NUM);
printf("####################################\n");
sleep(1);
}
else
{// 等待失败
printf("wait failed!\n");
break;
}
}
return 0;
}
虽然是打印的方式说明在执行其他任务,但是这个原理用这简单的方式讲通了。在子进程没有退出时,父进程可以去做一些自己的任务!