您现在的位置是:首页 >技术交流 >那年我手执『wait』桃木剑,轻松解决僵尸进程~网站首页技术交流
那年我手执『wait』桃木剑,轻松解决僵尸进程~
文章目录
?专栏导读
?作者简介:花想云 ,在读本科生一枚,C/C++领域新星创作者,新星计划导师,阿里云专家博主,CSDN内容合伙人…致力于 C/C++、Linux 学习。
?专栏简介:本文收录于 Linux从入门到精通,本专栏主要内容为本专栏主要内容为Linux的系统性学习,专为小白打造的文章专栏。
?文章导读
前几章我们讲了关于如何创建进程与进程状态。那么本章我们就来看看进程在退出时
又有哪些花样吧~ 为了解决之前所讲的僵尸进程
问题,我们必须要让父进程得到子进程的退出状态,这就是本章的另一个话题——进程等待
~
?进程退出
思考一下,我们创建一个进程的目的是什么?当然是想让进程帮助我们完成某件事情。例如:①将一个文件拷贝到某个目录下、②判断某个某个文件是否为空…
有些情况下,我们只需要进程去做某件事即可,并不需要关心它做的是否合格(这显然是不可取的行为);有时候不仅需要进程去做某件事,还需要关心它是否成功了,即关心一个进程的结果。
那么当一个进程结束时,一共可分为3中情况:
- 代码运行完毕,且结果正确;
- 代码运完毕,结果错误;
- 代码异常终止;
?进程常见的退出方法
?正常终止
?return 退出
在C语言中,我们编写程序时都会在main
函数的最后加上一条语句——return 0;
main
函数中的return
并不等同于其他函数的return
,main
函数返回的其实是进程退出码
。
在Linux中,我们可以使用指令来查看一个进程的退出码:
$ echo $?
?
只会保存最近
一次进程退出的退出码。
示例
#include <stdio.h>
int main()
{
int a = 10;
int b = 30;
int ret = a + b; // 代码1
//int ret = a + b / 2; // 代码2
// 结果正确返回0,错误返回1
if(ret == 40)
return 0;
else
return 1;
printf("mytest......
");
}
代码1
代码2
?exit 退出
我们可能对这个函数并不陌生,它的作用就是终止进程。exit
的参数就是退出进程时需要返回的退出码。
我们故意写一段错误的代码来看看exit
返回的退出码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
void Sort(int* array,int n)
{
if(array == NULL)
{
perror("Array Fail");
exit(111);
}
// 排序略...
}
int main()
{
int * prt = NULL;
Sort(prt,10);
return 0;
}
?_exit 退出
_exit
是系统调用
,并不像exit
是C语言的库函数
。_exit
与exit
使用方法完全相同,但是两者某些行为却有差别:
exit
在退出进程之前会刷新缓冲区
;_exit
直接退出进程;
示例
exit 的表现
#include <stdio.h>
#include <stdlib.h>
void Sort(int* array,int n)
{
if(array == NULL)
{
//per1ror("Array Fail");
printf("函数发生错误");
sleep(1);
exit(111);
}
// 排序略...
}
int main()
{
int * prt = NULL;
Sort(prt,10);
return 0;
}
_exit 的表现
#include <stdio.h>
#include <stdlib.h>
void Sort(int* array,int n)
{
if(array == NULL)
{
//per1ror("Array Fail");
printf("函数发生错误");
sleep(1);
_exit(111);
}
// 排序略...
}
int main()
{
int * prt = NULL;
Sort(prt,10);
return 0;
}
很显然,在_exit
的表现中,“函数发生错误”还在缓冲区中未刷新,进程就已经退出了。
exit
最后其实也会调用_exit
, 但在调用_exit
之前,还做了其他工作:- 执行用户通过
atexit
或on_exit
定义的清理函数; - 关闭所有打开的流,所有的缓存数据均被写入;
- 调用_exit;
- 执行用户通过
?异常终止
除了程序自己发现异常而终止,一个程序还可能因为外力因素而提前终止。例如我们之前学习过的指令:kill -9
,它的作用就是从外部“杀”掉进程、或者我们经常使用的Ctrl+c
。
$ kill -9 进程ID
?进程等待
?必要性
-
之前讲过,子进程退出,父进程如果不管不顾,就可能造成
僵尸进程
的问题,进而造成内存泄漏
; -
另外,进程一旦变成
僵尸状态
,那就刀枪不入,“杀人不眨眼”的kill -9
也无能为力,因为谁也没有办法杀死一个已经死去的进程; -
最后,父进程派给子进程的任务完成的如何,我们需要知道。如:子进程运行完成,结果对还是不对,或者是否正常退出;
-
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息;
?是什么
我们谈进程等待,那么我们究竟要等待什么呢?
- 进程等待,就是通过
系统调用
,获取子进程的退出码
或者退出信号
的方式,顺便解决内存释放的问题。
?如何等待
这里就要介绍两个系统调用接口了:
wait
;
pid_t wait(int *status);
-
返回值
:成功返回被等待进程pid
,失败返回-1
。 -
参数
:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
;
?解决子进程僵尸问题
之前讲到过,由于父进程为对子进程进行等待,子进程就会进入僵尸状态
,且危害极大。那么意味着,要避免僵尸问题,我们必须对子进程进行等待。代码如下:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
int cnt = 50;
if(id == 0)
{
// 子进程
while(cnt)
{
printf("我是子进程,我还在运行,我还有%d...
",cnt--);
sleep(1);
}
exit(111);
}
// 父进程
pid_t ret_id = wait(NULL);
printf("%d %d
",id,ret_id);
sleep(10);
}
如图所示,在kill -9
终止子进程后,子进程并没有进入僵尸状态。而且可以清晰的看到,wait的返回值就是子进程的PID
。
?如何获取子进程status
-
wait
和waitpid
,都有一个status
参数,该参数是一个输出型参数
,由操作系统填充。 -
如果传递
NULL
,表示不关心子进程的退出状态信息。 -
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
-
status
不能简单的当作整型
来看待,可以当作位图
来看待,具体细节如下图(只研究status
低16比特位):
做个形象的比喻,如果你正在考试,如果你以正确的方式参加了考试,那么你的成绩必定有好有坏,就看你如何看待自己考试的结果了;但是,如果你考试作弊了,考试还未结束,你就被监考老师叉出去了,提前结束了考试,此时,你的卷子尽管可能有得分,这个得分有没有参考价值呢?当然没有。
所以,如果一个进程正常终止,我们可以拿到它的退出状态即进程退出码;如果一个进程被信号(监考老师)终止,此时退出状态是没有意义的,但是我们可以查看终止信号,(至少看看是什么原因导致的考试异常结束)。
示例
- 进程正常终止;
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
int cnt = 5;
if(id == 0)
{
// 子进程
while(cnt)
{
printf("我是子进程,我还在运行,我还有%d...
",cnt--);
sleep(1);
}
exit(111);
}
// 父进程
int status = 0;
pid_t ret_id = wait(&status);
printf("child exit code:%d,child exit singl:%d
",(status >> 8) & 0xFF,status & 0x7F);
}
kill -9
终止进程(把计时改为20,因为我手速没那么快…);
waitpid
;
pid_ t waitpid(pid_t pid, int *status, int options);
-
返回值:
- 当正常返回的时候
waitpid
返回收集到的子进程的PID
; - 如果设置了选项
WNOHANG
,而调用中waitpid
发现没有已退出的子进程可收集,则返回0
; - 如果调用中出错,则返回
-1
,这时errno
会被设置成相应的值以指示错误所在;
- 当正常返回的时候
-
参数:
pid
:- Pid = -1,等待任意一个子进程,与
wait
等效。 - Pid > 0,等待其进程
ID
与pid
相等的子进程。
- Pid = -1,等待任意一个子进程,与
status
:WIFEXITED(status)
:若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)WEXITSTATUS(status)
:若WIFEXITED
非零,提取子进程退出码。(查看进程的退出码)
-
options
:WNOHANG
:若pid
指定的子进程没有结束,则waitpid()
函数返回0
,不予以等待。若正常结束,则返回该子进程的ID
。
waitpid
与wait
的用法大致是类似的,这里就不做专门的演示了。
?阻塞等待
仔细观察上文中示例中的结果,父进程的输出总在子进程结束之后,如图:
在子进程运行期间,父进程一直在等待,并没有做其他事情,直到等待子进程成功。我们把这种情况称为父进程在进行阻塞等待
。
如果父进程不想干干地等待子进程结束,而是想在等待的期间做点其他有意义的事情该如何处理呢?
?非阻塞等待
- 首先我们先预设一批任务,在父进程等待期间执行这些任务;
void sync_disk()
{
printf("这是一个刷新数据的任务
");
}
void sync_log()
{
printf("这是一个同步日志的任务
");
}
void net_send()
{
printf("这是一个网络发送的任务
");
}
- 设置任务加载函数、任务运行函数、任务初始化函数等;
#define TASK_NUM 10 // 任务的数量
typedef void (*FUNC_PTR)(); // 函数指针类型重定义
FUNC_PTR other_task[TASK_NUM]={NULL}; // 定义一个函数指针数组
int LoadTask(FUNC_PTR func)
{
int i = 1;
for(; i < TASK_NUM; ++i)
{
if(other_task[i]==NULL) break;
}
if(i == TASK_NUM) return -1;
else other_task[i] = func;
return 0;
}
void InitTask()
{
int i = 0;
for(; i < TASK_NUM; ++i) other_task[i] == NULL;
LoadTask(sync_disk);
LoadTask(sync_log);
LoadTask(net_send);
}
void RunTask()
{
for(int i = 0; i < TASK_NUM; i++)
{
if(other_task[i] == NULL) continue;
other_task[i]();
}
}
main
函数设计;
int main()
{
pid_t id = fork();
int cnt = 15;
if(id == 0)
{
// 子进程
while(cnt)
{
printf("我是子进程,我还在运行,我还有%d...
",cnt--);
sleep(1);
}
exit(111);
}
InitTask();
// 父进程
while(1)
{
int status = 0;
pid_t ret_id = waitpid(id,&status,WNOHANG);
if(ret_id < 0)
{
printf("等待出错
");
exit(1);
}
else if(ret_id == 0) // 子进程还未结束,做做其他事情
{
RunTask();
sleep(1);
continue;
}
else // 已等待成功
{
if(WIFEXITED(status)) // 子进程正常退出
{
printf("等待成功,子进程pid:%d,子进程退出码:%d
",id,WEXITSTATUS(status));
}
else // 子进程异常退出
{
printf("等待成功,子进程pid:%d,子进程退出信号:%d
",id,status&0x7F);
}
break;
}
}
}
效果如下
?完整代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#define TASK_NUM 10 // 任务的数量
void sync_disk()
{
printf("这是一个刷新数据的任务
");
}
void sync_log()
{
printf("这是一个同步日志的任务
");
}
void net_send()
{
printf("这是一个网络发送的任务
");
}
typedef void (*FUNC_PTR)(); // 函数指针类型重定义
FUNC_PTR other_task[TASK_NUM]={NULL};
int LoadTask(FUNC_PTR func)
{
int i = 1;
for(; i < TASK_NUM; ++i)
{
if(other_task[i]==NULL) break;
}
if(i == TASK_NUM) return -1;
else other_task[i] = func;
return 0;
}
void InitTask()
{
int i = 0;
for(; i < TASK_NUM; ++i) other_task[i] == NULL;
LoadTask(sync_disk);
LoadTask(sync_log);
LoadTask(net_send);
}
void RunTask()
{
for(int i = 0; i < TASK_NUM; i++)
{
if(other_task[i] == NULL) continue;
other_task[i]();
}
}
int main()
{
pid_t id = fork();
int cnt = 15;
if(id == 0)
{
// 子进程
while(cnt)
{
printf("我是子进程,我还在运行,我还有%d...
",cnt--);
sleep(1);
}
exit(111);
}
InitTask();
// 父进程
while(1)
{
int status = 0;
pid_t ret_id = waitpid(id,&status,WNOHANG);
if(ret_id < 0)
{
printf("等待出错
");
exit(1);
}
else if(ret_id == 0) // 子进程还未结束,做做其他事情
{
RunTask();
sleep(1);
continue;
}
else // 已等待成功
{
if(WIFEXITED(status)) // 子进程正常退出
{
printf("等待成功,子进程pid:%d,子进程退出码:%d
",id,WEXITSTATUS(status));
}
else // 子进程异常退出
{
printf("等待成功,子进程pid:%d,子进程退出信号:%d
",id,status&0x7F);
}
break;
}
}
}
本章的内容就到这里了,觉得对你有帮助的话就支持一下博主把~
点击下方个人名片,交流会更方便哦~
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓