1 退出处理函数
如果在程序中注册了退出处理函数,那么当程序正常执行结束之前,就会自动调用该函数。
先注册的退出处理函数最后执行,后注册的退出处理函数先执行。
可以将程序的收尾性工作,放在退出处理函数中。
退出处理函数,要经过注册才能使用。
1.1 atexit()
#include <stdlib.h>
int atexit(void (*function)(void));
功能:中间商,告知内核,退出处理函数的地址
返回值:成0败-1
注意,atexit()函数本身并不调用退出处理函数,而只是将function参数所表示的退出处理函数的地址,保存(注册)在系统内核的进程表项。待到exit函数被调用,或在main函数里执行return语句时,再由系统内核根据这些退出处理函数的地址来调用它们。此过程也称回调。
退出处理函数是内核调用的。
1.2 on_exit()
#include <stdlib.h>
int on_exit(void (*function)(int, void*), void* arg);
功能:中间商,告知内核,退出处理函数的地址,函数有2个参数
function:函数指针,指向退出处理函数。第一个参数来自传递给exit函数的status函数
或在main函数里执行return语句的返回值,而第二个参数来自传递给on_exit()
函数的arg参数。
arg:泛型指针,将作为第二个参数传递给function所指向的退出处理函数。
返回值:成0败-1
2 令进程终止的方式(3正常,3异常)
2.1 main()中return n;
进程是内存中的代码和数据,而线程则是执行代码的过程。
每个进程可以包含1个多个线程,但至少要有1个主线程。
每个线程都可以被看做是在一个独立的执行过程中调用了一个特殊的函数:线程过程函数。线程开始,线程过程函数即被调用,线程过程函数一旦返回,线程即告终止。因此main函数也可以被看做是进程的主线程的线程过程函数。main函数一旦返回,主线程即终止,进程即终止。进程一旦终止,进程中的所有线程统统终止。这就是main函数的返回与其它函数的返回在本质上的区别。
main()函数的返回值即进程的退出码,父进程可以在回收子进程的同时获得该退出码,以了解导致其终止的具体原因。
2.2 调用exit()
谁调谁死,在哪调在哪死。
#include <stdlib.h>
void exit(int status);
功能:令进程终止
status:进程的退出码,相当于main()函数的返回值,低八位
该函数不返回!
通过return语句终止进程只能在mian()函数中实现,但是调用exit()函数终止进程可以在包括main()函数在内的任何函数中使用(谁调谁死,在哪调在哪死)。
exit()函数在终止调用进程之前还会做几件收尾工作:
A、调用实现:通过atexit()或on_exit()函数注册的退出处理函数
B、冲刷并关闭所有仍处于打开状态的标准I/O流
C、删除所有通过tmpfile()函数创建的临时文件
D、_exit(status);
习惯上,还经常使用EXIT_SUCCESS和EXIT_FAILURE两个宏作为调用exit()函数的参数,分别表示成功和失败。它们的值在多数系统中被定义为0和-1,但一般建议使用宏,兼容性更好,可读性更强。
2.3 调用_exit()、_Exit()
#include <unistd.h>
void _exit(int status);
status:进程退出码,相当于main()函数的返回值
该函数不返回!(进程都没了,返回给谁呢)
#include <stdlib.h>
void _Exit(int status);
status:进程退出码,相当于main()函数的返回值
该函数不返回!(进程都没了,返回给谁呢)
_exit()在终止调用进程之前也会做几件收尾工作,但与exit()函数不同。事实上,exit()函数在做完3件收尾工作之后,紧接着就会调用_exit()函数。
_exit()函数被调用时,不会执行ABC,直接执行D。
D1、关闭所有仍处于打开状态的文件描述符
D2、将调用进程的所有子进程托付给孤儿院进程收养
D3、向调用进程的父进程发送SIGCHLD(17)信号
D4、令调用进程终止运行,将status的低八位作为退出码保存在其终止状态中
//exit_exit.c
#include<stdio.h>
#include<stdlib.h> //exit()
#include<unistd.h> //_exit()
//退出处理函数
void exitfun(void){
printf("我是退出处理函数exitfun....\n");
}
void exitfun1(int status,void* arg){
//status 进程退出码
printf("status ---> %d\n",status);
//arg on_exit()第二个参数
printf("arg ---> %s\n",(char*)arg);
}
int fun(void){
printf("我是fun函数\n");
//exit(0); //谁调谁死,在这调,在这死
_exit(0); //谁调谁死,在这调,在这死,不再执行退出处理函数
return 10;
}
int main(void){
//注册退出处理函数
atexit(exitfun); //先注册,后执行
on_exit(exitfun1,"hello"); //后注册,先执行
printf("调用fun,fun返回%d\n",fun());
return 0;
}
//编译执行
2.4 作死
当进程执行了某些在系统看来具有危险性的操作,或系统本身发生了某种故障或意外,内核忍不了,会向相关进程发送特定的信号。如果进程无意针对收到的信号采取补救措施,那么内核将按照缺省方式将进程杀死,并视情形生成核心转储文件(core)以备时候分析,俗称吐核。
SIGILL(4) 进程视图执行非法指令
SIGBUS(7) 硬件或对齐错误
SIGFPE(8) 浮点异常(比如分母为0)
SIGSEGV(11) 无效内存访问
SIGPWR(30) 系统供电不足
2.5 他杀
人为触发以下信号:
SIGINT(2) Ctrl + C (父进程被异常关闭,子进程变孤儿进程)
SIGQUIT(3) Ctrl + \
SIGKILL(9) 不能被捕获或忽略的进程终止信号
SIGTERM(15) 可以被捕获或忽略的进程终止信号
叉掉bash终端也算异常终止。
2.6 自杀 abort()
向进程自己发送信号:
#include <stdlib.h>
void abort(void);
功能:向调用进程发送SIGABRT(6)信号,该信号默认情况下可使进程结束
无参数,不返回!
3 回收子进程(收尸)
父进程的父进程是bash。
3.1 为什么?
清除僵尸进程,避免消耗系统资源。
有时父进程需要等待子进程终止后,才继续后续工作。
有时父进程需要知道子进程终止的原因:
如果正常终止,那么子进程的退出码是多少?
如果异常终止,那么进程是被哪个信号所终止的?
3.2 wait()
#include <sys/wait.h>
pid_t wait(int* status);
功能:等待并回收任意子进程(阻塞函数)
status:输出型参数,输出子进程的终止状态,可置NULL
返回值:成功返回所回收的子进程的PID,失败返回-1
在任何一个子进程终止前,wait()函数职能阻塞调用进程,如果有一个子进程在wait()函数被调用之前,已经终止并处于僵尸状态,wait()函数会立即返回,并取得该子进程的终止状态,同时子进程僵尸消失。由此可见,wait()函数主要完成3个任务:
1. 阻塞父进程的运行,直到子进程终止,再继续,停等同步。
2. 获取子进程的PID和终止状态,令父进程得知:谁,因何而死。
3. 为子进程收尸,防止大量僵尸进程耗费系统资源。
以上3个任务中,即使前两个与具体需求无关,仅仅第三个也足以凸显wait()函数的重要性,尤其是对那些多进程服务器型的应用而言。
子进程的终止状态通过wait()函数的status参数输出给该函数的调用者。
<sys/wait.h>头文件提供了几个辅助分析进程终止状态的工具宏。
WIFEXITED(status) 真:正常终止,WEXITSTATUS(status) -> 进程退出码
假:异常终止,WTERMSIG(status) -> 终止进程的信号
WIFSIGNALED(status) 真:异常终止,WTERMSIG(status) -> 终止进程的信号
假:正常终止,WEXITSTATUS(status) -> 进程退出码
//wait.c wait()函数演示
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>// fork()
#include<sys/wait.h>//wait()
int main(void){
//创建子进程
pid_t pid = fork();
if(pid == -1){
perror("fork");
return -1;
}
//子进程代码,假装很忙
if(pid == 0){
printf("%d进程:我是子进程,暂时不结束\n",getpid());
/*sleep(5);
return 0;*/ //!!!
//exit(300); //返回44
//_exit(300); //返回44
//_Exit(300); //返回44
/*int* p = NULL;
*p = 1;*/ //作死
abort(); //自杀
}
//父进程代码,等待并收尸
int s;//用来输出子进程的终止状态
pid_t childpid = wait(&s); //置NULL则不关心终止状态
if(childpid == -1){
perror("wait");
return -1;
}
printf("%d进程:回收了%d进程的僵尸\n",getpid(),childpid);
if(WIFEXITED(s)){
printf("正常终止:%d\n",WEXITSTATUS(s));
}else{
printf("异常终止:%d\n",WTERMSIG(s));
}
return 0;
}
//waitall.c 回收多个子进程 //还不是最完美的回收方式。用信号最完美。
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<errno.h>// int errno
int main(void){
//创建多个子进程
for(int i = 0;i < 5;i++){
pid_t pid = fork();
if(pid == -1){
perror("fork");
return -1;
}
if(pid == 0){
printf("%d进程:我是子进程\n",getpid());
sleep(i + 1);
return 0;
}
}
//父进程回收多个子进程的僵尸
for(;;){
pid_t childpid = wait(NULL);//NULL,不存子进程终止状态
if(childpid == -1){ //得到-1,需要进一步if判断
if(errno == ECHILD){
printf("%d进程:没有子进程了\n",getpid());
break;
}else{
perror("wait");
return -1;
}
}
printf("%d进程:回收了%d进程的僵尸\n",getpid(),childpid);
}
return 0;
}
//编译执行
3.3 waitpid()
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int* status, int options);
功能:等待并回收任意或特定子进程
pid:可取以下值
-1 等待并回收任意子进程,相当于wait()函数
>0 等待并回收特定子进程
status:用于输出子进程的终止状态,可置NULL
options:可取如下值
0 阻塞模式,若所等子进程仍在运行,则阻塞至其终止
WNOHANG 非阻塞模式,若所等子进程仍在运行,则返回0
返回值:成功返回所回收子进程的PID或者0,失败返回-1
事实上,无论一个进程是正常终止还是异常终止,都会通过系统内核向其父进程发送SIGCHLD(17)信号。父进程可以忽略此信号,也可以提供一个针对该信号的信号处理函数,在信号处理函数中以异步的方式回收子进程。这样做不仅流程简单,而且僵尸的存活时间短,回收效率高。
//wnohang非阻塞收尸
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
int main(void){
//创建子进程
pid_t pid = fork();
if(pid == -1){
perror("fork");
return -1;
}
//子进程代码,暂时不结束
if(pid == 0){
printf("%d进程:我是子进程,暂时不结束\n",getpid());
sleep(10);
return 0;//!!!
}
//父进程代码,非阻塞收尸
for(;;){
pid_t childpid = waitpid(pid,NULL,WNOHANG);
if(childpid == -1){
perror("waitpid");
return -1;
}else if(childpid == 0){
printf("%d进程:子进程在运行,收不掉,干点别的区\n",getpid());
sleep(1);
}else{
printf("%d进程:回收了%d进程的僵尸\n",getpid(),childpid);
break;
}
}
return 0;
}
本博文介绍了wait() 和waitpid()收子进程尸,但不完美。
加太平间信号(SIGCHLD 17)才完美。 太平间信号的内容在uc_10。