什么是进程等待
在了解进程等待之前,我们要回顾一下什么是僵尸进程:是指一个已经终止执行的进程,但其父进程还没有通过 wait() 系统调用来获取该进程的退出状态信息。当一个进程正常退出或者被终止时,其所占用的系统资源会被操作系统及时回收释放,而其退出状态(退出码)会被传递给其父进程。然而,如果父进程没有合理处理该状态,那么子进程将会变成一个僵尸进程。僵尸进程会占据系统资源,也就是僵尸进程的退出信息等资源,如果不即使清理僵尸进程的话可能会导致进程表被占满,从而导致系统运行缓慢甚至不稳定。
避免僵尸进程的产生:父进程通过调用 wait() 或 waitpid() 系统调用来处理已经终止的子进程,也就是处理僵尸进程所占据的资源信息。
为什么要进行进程等待
- 解决子进程僵尸问题带来的内存泄漏
- 我们知道父进程会通过等待子进程,从而接收子进程的退出信息(进程退出码和信号编号)而进程退出码标识的就是该进程运行完的退出结果,而信号编号标识的是该进程是什么导致的异常退出。
怎样等待
wait方法
wait() 是一个系统调用函数,用于父进程等待子进程的状态改变。它可以用来处理僵尸进程。wait() 函数的作用是:阻塞父进程的执行,直到一个子进程发生状态变化。当子进程终止时,内核会将该进程的退出状态保存在 status
中,并返回该子进程的进程ID(PID)。
pid_t wait(int*status);
返回值:成功则返回被等待进程pid,失败返回-1。
参数:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
void do_work()
{
int count=3;
while(count)
{
printf("我是子进程,mypid=%d myppid=%d count=%d\n",getpid(),getppid(),count--);
sleep(1);
}
}
int main()
{
pid_t i=fork();
if(i==0)//子进程
{
do_work();
}
else//父进程
{
cout<<"等待之前"<<endl;
pid_t ret=wait(NULL);
cout<<"等待之后"<<endl;
if(ret==i)//fork之后父进程会返回子进程的id
{
cout<<"等待成功"<<endl;
}
}
return 0;
}
我们知道fork之后会创建子进程,而且fork后面的代码子进程和父进程都会执行,而父子进程的执行先后顺序取决于调度器的调度,但是我们要知道父进程要接收子进程退出时的资源信息,所以可以明确:进程正常退出时,父进程一定后于子进程退出。而且父进程调用wait的时候会等待子进程执行完。
注意: wait() 函数只能处理直接子进程的状态变化,无法处理所有子进程的退出状态。如果希望处理所有子进程的状态变化,可以使用循环结构来多次调用 wait() 或者使用信号处理机制来实现。
waitpid方法
pid_ t waitpid(pid_t pid, int *status, int options);
waitpid() 函数等待成功会返回值为子进程的进程ID
第一个参数 pid 指定需要等待的子进程的进程ID。Pid=-1,等待任意一个子进程。与wait等效。
Pid>0.等待其进程ID与pid相等的子进程。
代替wait使用
void do_work()
{
int count=3;
while(count)
{
printf("我是子进程,mypid=%d myppid=%d count=%d\n",getpid(),getppid(),count--);
sleep(1);
}
}
int main()
{
pid_t i=fork();
if(i==0)//子进程
{
do_work();
}
else//父进程
{
cout<<"等待之前"<<endl;
pid_t ret=waitpid(-1,NULL,0);//三个参数(等价于wait)
cout<<"等待之后"<<endl;
if(ret==i)//父进程会返回子进程的id
{
cout<<"等待成功"<<endl;
}
}
return 0;
}
第二个参数的使用
参数 status 是一个指向整型变量的指针,用于保存子进程的退出状态信息。
如果不关心子进程的退出状态,可以将 status 设置为 NULL。
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。操作系统会根据该参数,将子进程的退出信息反馈给父进程status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):
我们知道子进程正常退出时会有退出码返回给父进程,而退出码标识该子进程任务的执行情况是否完成。但是子进程不仅仅有正常终止,还有可能会异常终止(收到操作系统发来的信号),所以我们还要接收子进程退出时的信号。所以当我们父进程在等待子进程时会得到两个数字(1.退出码 2.信号码)
退出码接收
int main()
{
pid_t i=fork();
if(i==0)//子进程
{
do_work();
exit(10);
}
else//父进程
{
cout<<"等待之前"<<endl;
int sta=0;
pid_t ret=waitpid(-1,&sta,0);
cout<<"等待之后"<<endl;
if(ret==i)//父进程会返回子进程的id
{
printf("子进程正常退出,pid:%d ,ret:%d ,exit code:%d ,exit sigal:%d \n",getpid(),ret,(sta>>8)&0xFF,sta&0x7F);//这里的&操作符就是得到对应的比特位
}
}
return 0;
}
退出信号接收
以上就是所有退出信号而8号对应的就是SIGFPE
第三个参数的认识
options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。
若正常结束,则返回该子进程的ID。
第三个参数我们一般默认是设为0的,而设为0所代表的含义是阻塞等待,就是父进程会一直等待子进程运行,直到子进程结束之后才开始调用wait函数,从而接收子进程的退出码和信号。但是显然阻塞等待缺陷是:父进程什么操作都干不了,必须等待子进程退出才可以继续执行父进程后续的操作。
而第三个参数为WNOHANG时则意味着非 阻塞等待,也就是说会进行重复的多次调用waitpid函数接口(尽管子进程未退出)。若子进程未退出则waitpid函数返回0,表明还需要继续等待;而返回值大于0则表明返回的是所等待的子进程ID,等待成功且子进程结束;还有返回值小于0的时候则表明等待失败。所以我们非阻塞等待一般会采用循环执行。也称为非阻塞轮询方案。而非阻塞等待相较于阻塞等待的优势就是还可以在进行等待的过程中,父进程可以顺便做一些占据时间不多的事情,而不是像阻塞等待一样一直等待着。
void do_work()
{
int count=3;
while(count)
{
printf("我是子进程,mypid=%d myppid=%d count=%d\n",getpid(),getppid(),count--);
sleep(1);
}
}
int main()//非阻塞等待
{
pid_t i=fork();//为父进程返回子进程的id,为子进程返回0
if(i==0)
{
//子进程
do_work();
exit(0);
}
while(1)//轮询等待
{
//父进程
pid_t ret = waitpid(i,NULL,WNOHANG);//等待成功返回所等待进程的id
if(ret==0)
{
cout<<"等待中"<<endl;
}
else if(ret==i)
{
cout<<"等待成功,所等待进程的id:"<<ret<<endl;
break;
}
else
{
cout<<"等待失败"<<endl;
break;
}
sleep(1);
}
return 0;
}
多进程的等待
void do_work()
{
int count=3;
while(count)
{
printf("我是子进程,mypid=%d myppid=%d count=%d\n",getpid(),getppid(),count--);
sleep(1);
}
}
int main()
{
int i=0;
for(i=0;i<10;i++)
{
pid_t id=fork();
if(id==0)//子进程进入
{
do_work();
exit(1);
}
}
for(i=0;i<10;i++)
{
pid_t ret = waitpid(-1,NULL,0);//等待任意一个子进程等价于wait
if(ret>0)
printf("进程等待成功,pid:%d\n",ret);
}
return 0;
}