进程的终止

一、进程的终止方式

  • 进程通过接受信号异常退出
  • 通过调用_exit(status)正常退出,其中status保存进程退出的状态,0为正常退出,非0为异常退出,但这并不是明文规定的标准,SUSv3 规定有两个常量: EXIT_SUCCESS(0)和 EXIT_FAILURE(1)
  • 通过执行exit(status)正常退出
    exit()会执行的动作如下:
    1、调用退出处理函数(通过 atexit()和 on_exit()注册的函数),其执行顺序和注册顺序相反
    2、刷新缓冲区
    3、使用由status提供的值执行_exit()系统调用。exit属于C库函数,而_exit()属于系统调用
  • 执行return或执行到main函数的结尾退出
    return n;等同于exit(n);如果是return;或执行到main函数的末尾退出,将默认调用exit()函数,但是对于status值未知。C89返回任意值,C
    99中默认等同于调用exit(0)

二、进程终止的细节

无论进程是否正常终止,都会发生如下动作:

  • 关闭所有的文件描述符、目录流、信息目录描述符、以及转换描述符。
  • 作为文件描述符关闭的后果之一,将释放该进程所持有的任何文件锁。
  • 分离(detach)任何已连接的 System V 共享内存段,且对应于各段的 shm_nattch 计数器值将减一。
  • 进程为每个 System V 信号量所设置的 semadj 值将会被加到信号量值中 如果该进程是一个管理终端(terminal)的管理进程,那么系统会向该终端前台(foreground)进程组中的每个进程发送 SIGHUP信号,接着终端会与会话(session)脱离。
  • 将关闭该进程打开的任何 POSIX 有名信号量,类似于调用sem_close()。
  • 将关闭该进程打开的任何 POSIX 消息队列,类似于调用 mq_close()。
  • 作为进程退出的后果之一,如果某进程组成为孤儿,且该组中存在任何已停止进程(stopped processes),则组中所有进程都将收到SIGHUP 信号,随之为 SIGCONT 信号。
  • 移除该进程通过 mlock()或 mlockall()所建立的任何内存锁。 取消该进程调用mmap()所创建的任何内存映射(mapping)。

三、退出处理程序

如果程序调用了其他程序库,当程序退出时,需要对程序库进行退出处理,因为其他程序库对当前进程没有控制权,所以处理工作需要当前进程来做,因此引入退出处理程序。

退出处理程序需要在程序退出前注册,在执行到exit()函数时自动调用,注册函数的执行顺序跟注册程序刚好相反,逻辑上认为先注册的处理函数是一些基本处理函数,需要其他具体处理函数执行完后再执行。

只有执行到exit()函数时才会调用退出处理程序,这也意味着通过接收信号退出、通过_exit()退出的进程无法调用退出处理程序

如果在退出处理程序中调用了exit(),结果是未定义的,可能会无限调用退出处理程序,直到栈溢出将该函数杀死。

GNU C函数库提供两种方式来注册退出处理程序:

#include <stdlib.h>

int atexit(void(*func)(void));
//return 0 on success, or nonzero on error
#include <stdlib.h>
#define _BSD_SOURCE	/*Or: #define _SVID_SOURCE  */

int on_exit(void(*func)(int, void*), void *arg);
//return 0 on success, or nonzero on error

在调用atexit()时,无法获取进程的退出状况,然而获取进程的退出状况是可取的,通过不同的进程退出状况执行不同的退出处理程序。同时,也无法给atexit()传递指定参数,根据参数的不同执行不同的进程处理函数。为此glibc提供了一(非标准的)可替换的方法on_exit()。但如果为了可移植性,应尽量避免使用该函数。
调用时,第一个参数为exit()的参数status,第二个参数为注册时供给on_exit()的一份arg参数拷贝。

另外,atexit()和on_exit()可以交替使用

四、fork()、 stdio 缓冲区以及_exit()之间的交互

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char const *argv[])
{
	int fd = open("./my_file", O_RDWR|O_CREAT, S_IWUSR|S_IRUSR);
	if (fd < 0)	
	{
		perror("open");
		return -1;
	}

	printf("Hello world\n");
	fflush(stdout);
	write(fd, "Amos\n", strlen("Amos\n"));

	if(fork() == -1)
	{
		perror("fork");
	}

	exit(EXIT_SUCCESS);
}

程序的执行结果如下:
在这里插入图片描述将程序的运行结果重定向到文件中时的结果令人迷惑:
1、为什么"Amos"先输出
2、为什么"Hello world"打印了两遍

首先进程是在用户空间维护stdio缓冲区的。如果将程序结果直接打印到stdout,缺省为行缓冲,printf结果会按行打印到控制台;当程序结果重定向到文件my_file中时,缺省为块缓冲,此时"Hello world"仍存在于父进程的用户空间中,fork()创建子进程后,将赋值一份父进程的用户空间,这样子进程的stdio缓冲中将也有一份"Hello world"当父子进程执行到exit()时,将对各自进程的缓冲区进行刷新,此时父子进程才会将"Hello world"都刷新到文件my_file中,而在stdio缓冲区刷新之前早已通过write将"Amos"直接传给文件缓冲区。

为了避免这种重复输出,有两种方法:
1、在fork之前使用fflush(stdout)刷新缓冲区。作为另一种选择,也可以使用 setvbuf()setbuf()来关闭 stdio 流的缓冲功能。
2、父进程退出时调用exit(),子进程退出时调用_exit()以不再刷新缓冲区。同时这也例证了一个更为通用的原则:在创建进程的应用中,典型情况下只有一个进程调用exit()终止,而其他进程调用_exit()终止,保证只有一个进程退出时刷新缓冲区,而一般情况下父进程将是调用exit()的那个进程。

总结:
1、进程退出时,状态0表示正常退出,非0表示异常退出。
2、调用 exit()正常终止一个进程,将会引发执行经由 atexit()和 on_exit()注册的退出处理程序(执行顺序与注册顺序相反),同时刷新 stdio 缓冲区。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值