前言:
进程控制不仅仅是管理程序的执行顺序,还涉及到资源的分配等问题,那么话不多说,开始我们今天的话题!
🚀进程退出函数
✈️exit函数
上次我们说到,进程退出时,都会返回一个退出码,用来表示进程退出的状态,而在更前面,我们曾经说过exit函数用来退出进程:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
while(1)
{
printf("I'm a process, pid=%d\n", getpid());
sleep(1);
exit(3);
}
return 0;
}
进程退出函数exit,函数参数可作为进程退出状态:
eixt:退出进程 status:进程退出状态退出码。
✈️_exit函数
这是man手册里的_exit(),我们通过下面这段代码测试一下他们功能有何异同:
#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<stdlib.h>
#include<unistd.h>
void Print()
{
printf("hello Print\n");
_exit(4);
}
int main()
{
while(1)
{
printf("I am a process: %d\n", getpid());
sleep(1);
Print(1);
//exit(3);放出另外一个_exit,反之则放出exit关闭_exit
}
return 0;
}
这是注释掉了exit(),调用Print函数的_exit()的结果。
这是注释掉Print函数内的_exit,使用exit()函数返回的结果。
从此看来我们并没有发现什么不同之处,返回的错误码也没问题。他们的区别可以通过一下代码分析:
#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
printf("hello guy\n");
sleep(1);
exit(1);
return 0;
}
exit函数返回的结果符合我们的预期,如果把打印的\n删除:
也没什么问题,但是如果换成_exit函数:
从这里可以看到,当_exit函数遇到像printf打印却没有换行符的时候,就不能正确打印出自己想要的数据。
区别: exit是用来终止进程的,exit(退出码),在我们进程的任何地方调用exit() 都表示进程退出,而_exit与exit的区别就是,exit() 会刷新缓冲区(以后会详谈)。
实际上,_exit()函数是Linux下的一种系统调用,为什么要存在exit() 和 _exit() 两个不同的接口呢?exit() 函数是语言层面的封装,使用这种封装的好处就是语言具有 跨平台、可移植性。
不同系统、平台可能给你提供的系统调用不相同,但是在语言层的封装却都是相同的,所以 C语言具有可移植性、跨平台性。
🚀进程等待
我们之前说过,如果一个进程变僵尸那么使用kill -9也无能为力,造成僵尸的原因就是子进程退出了,父进程并没有将资源回收所导致的,所以系统提供了一些系统调用,通过父进程等待的方式获取子进程退出信息。
✈️wait接口
在Linux中,为了防止进程变僵尸,系统系统了这样一个接口 wait():
wait接口是用来回收子进程资源的一个接口,我们看到wait接口的参数是一个指针,这其实是一个 输出型参数 ,我们现将其设置为 NULL。
接口功能:
默认会进行阻塞等待,等待任意一个子进程。返回值:>0时,等待成功,等待的子进程pid。<0时, 等待失败
我们通过代码来认识一下它的功能:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
int cnt = 5;
while(cnt)
{
printf("child is running, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
cnt--;
}
printf("child prepare for exit, will be ZM!\n");
exit(0);
}
printf("father is sleep...\n");
sleep(10);
printf("father start recycle\n");
//father
pid_t rid = wait(NULL);
if(rid > 0)
{
printf("wait sucess, rid: %d\n", rid);
}
printf("father get source sucess!\n");
sleep(3);
return 0;
}
图中画的红色框框表示僵尸了一段时间,而当父进程开始回收之后,子进程的僵尸状态就没有了。
由此我们可以间接得出,fork之后,父进程一定要最后退出!
✈️waitpid接口
Linux也提供了wait方式来获取子进程退出信息的接口 waitpid():
其中waitpid返回值与wait的返回值含义相同,第一个参数的pid有很多种表示方法,但是常见的表示有这两种:
pid参数意义:
pid = -1,等待任意子进程,与wait等效。pid>0,等待其进程id与pid相等的子进程。
status同样是一个输出型参数。options有两种传值,一种是0,一种是非阻塞,我们目前将其设置为0表示阻塞状态。
通过下面代码感受getpid():
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
int cnt = 5;
while(cnt)
{
printf("child is running, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
cnt--;
}
exit(1);
}
//father
int status = 0;
pid_t rid = waitpid(id, &status, 0);//阻塞等待
if(rid > 0)
{
printf("wait sucess, rid: %d status: %d\n", rid, status);//查看rid和status
}
return 0;
}
我们给status的初始值为0,但是这里却变成了256。这里的status并不是指单纯的整数,与进程的 退出码 和 退出信号 有关,而其存储形式类似于位图的存储:
我们只看比特位的后十六位:
中间有一个名为core dump的比特位我们在信号部分在详谈。
所以我们status为什么是256其实就很简单了:
0000 0000 0000 0000 0000 0001 0000 0000 转换为10进制就是2的8次方也就是256。
✈️导出错误码
上面说了status的最后16位比特位有效,并且这十六位由退出码和信号编号所组成,所以我们可以使用位运算的方式将退出码和退出信号提取出来:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
int cnt = 50;
while(cnt)
{
printf("child is running, pid: %d, ppid: %d\n", getpid(), getppid());
cnt--;
sleep(1);
}
exit(0);
}
//father
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
//status & 0x7F: 0000 0000 0000 0000 0000 0000 0111 1111
printf("wait sucess, rid: %d status: %d exit signo%d exit code: %d\n", rid, status, status&0x7F, (status>>8)&0xFF);
}
return 0;
}
将status按位与上 0x7F就可提取出退出信号,因为只有后七位有效。将status右移8位再按位与上0xFF即可提取退出码。
使用了kill -9得出的退出信号就是9,进程被杀死退出码为0。如果在代码中出现了逻辑错误,比如除零错误:
还有类似空指针等情况,有兴趣可以自己尝试,这里就不在试了。
实际上,Linux给我们提供了两个常见的宏定义:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
有了这些宏,我们就不用那么麻烦直接使用位操作了,我们可以直接使用对应的宏:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
int cnt = 5;
while(cnt)
{
printf("child is running, pid: %d, ppid: %d\n", getpid(), getppid());
cnt--;
sleep(1);
}
exit(123);
}
//father
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
//status & 0x7F: 0000 0000 0000 0000 0000 0000 0111 1111
// printf("wait sucess, rid: %d status: %d exit signo%d exit code: %d\n", rid, status, status&0x7F, (status>>8)&0xFF);
if(WIFEXITED(status))//使用宏定义
{
printf("wait sucess, rid: %d status: %d exit code: %d\n", rid, status, WEXITSTATUS(status));
}
else
{
printf("child process error!\n");
}
}
return 0;
}
源码中的status:
✈️阻塞与非阻塞
我们前面代码所采用的等待方式均为阻塞等待,即在回收子进程的资源之前,父进程什么事情可不干,可以用grep 查询:
ps ajx | grep -i mybin#查询当前进程状态
既然有阻塞,也必然有非阻塞:
表1 阻塞与非阻塞区别
阻塞 | 非阻塞 | |
方式 | 当waitpid函数以阻塞模式调用时,父进程等待子进程退出,这期间父进程不做任何事情 | 当waitpid函数以非阻塞调用时,父进程以轮询的方式每段时间检测子进程是否退出,没退出就返回做父进程的事情 |
参数 | 0 | WNOHANG |
我们以下面这段代码来理解非阻塞:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
int cnt = 5;
while(cnt)
{
printf("child is running, pid: %d, ppid: %d\n", getpid(), getppid());
cnt--;
sleep(1);
}
exit(123);
}
//father
int status = 0;
while(1)
{
pid_t rid = waitpid(id, &status, WNOHANG);//非阻塞调用
if(rid > 0)
{
printf("wait sucess, rid: %d, status: %d, exit code: %d\n", rid, status, WEXITSTATUS(status));
break;
}
else if(rid == 0)
{
printf("father say: child is running, do other things\n");
}
else
{
perror("waitpid");
break;
}
sleep(1);
}
return 0;
}
父进程在等待子进程资源回收的时候自己也在不断地执行,最后也可以成功回收子进程。
为了更直观看到现象,我们不妨模拟进程等待的环境:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
#define NUM 5
typedef void(*fun_t)();
fun_t tasks[NUM];
void printLog()
{
printf("this is a log print task\n");
}
void printNet()
{
printf("this is a net\n");
}
void printNPC()
{
printf("this is a flush NPC\n");
}
void initTask()
{
tasks[0] = printLog;
tasks[1] = printNet;
tasks[2] = printNPC;
tasks[3] = NULL;
}
void executeTask()
{
for(int i = 0; tasks[i]; ++i) tasks[i]();
}
int main()
{
initTask();
pid_t id = fork();
if(id == 0)
{
//child
int cnt = 5;
while(cnt)
{
printf("child is running, pid: %d, ppid: %d\n", getpid(), getppid());
cnt--;
sleep(1);
}
exit(123);
}
//father
int status = 0;
while(1)
{
pid_t rid = waitpid(id, &status, WNOHANG);
if(rid > 0)
{
printf("wait sucess, rid: %d, status: %d, exit code: %d\n", rid, status, WEXITSTATUS(status));
break;
}
else if(rid == 0)
{
printf("father say: child is running, do other things\n");
printf("##############task begin############\n");
executeTask();
printf("##############task end##############\n");
}
else
{
perror("waitpid");
break;
}
sleep(1);
}
return 0;
}
📒✏️总结
- 在Linux下,进程退出提供了两个接口,exit() 和 _exit(),他们的区别就是 _exit()函数没有刷新缓冲区这一功能。
- 一个创建子进程的父进程有必要进行进程等待,用来防止进程变为僵尸进程从而危害系统。其中进程等待有两个接口 wait() 和 waitpid()。
- wait和waitpid:
区别 | wait | waitpid |
参数 | *status | id, *status, options |
状态 | 等待 任意 子进程退出,并返回终止子进程id | 等待 指定 子进程退出,并返回终止子进程id |
阻塞 | 无(默认阻塞调用) | 0 和 WNOHANG(用来支持阻塞非阻塞的宏) |
- 根据你所期望进程执行的行为从而选择对应的接口。
创作不易,如果对您有帮助的话,还望留下一个小小的赞~~