进程终止
进程的终止:_exit()和exit()
进程有两种终止方式。其一为异常终止,由一对信号的接收而引发,该信号的默认动作为终止当前进程。此外,进程可使用_exit()
系统调用正常终止。
#include <unistd.h>
void _exit(int status);
_exit()
的status
参数定义了进程的终止状态,父进程可调用wait()
以获取该状态。按照惯例,终止状态为0表示进程”功成身退“,而非0值则表示进程因异常而退出。
调用_exit()
的程序总会成功终止,即(_exit()
从不返回)。
程序一般不会直接调用_exit()
,而是调用库函数exit()
,它会在调用_exit()
前执行各种动作。
exit()
执行的动作如下:
- 调用退出处理程序(通过
atexit()
和on_exit()
注册的函数),其执行顺序与注册顺序相反。 - 刷新
stdio
流缓冲区。 - 使用由
status
提供的值执行_exit()
系统调用。
程序的另一种终止方法是从main()
函数返回return
,或者或明或暗地一直执行到main()
函数的结尾处。执行return n
等同于执行对exit(n)
的调用,因为调用main()
的运行时会将main()
的返回值作为exit()
的参数。
进程终止的细节
无论进程是否正常终止,都会发生如下动作。
- 关闭所有打开文件描述符,目录流,信息目录描述符以及(字符集)转换描述符。
- 作为文件描述符关闭的后果之一,将释放该进程所持有的任何文件锁。
- 分离(
detach
)任何已连接的System V
共享内存段,且对应于各段的shm_nattch
计数器值将减1。 - 进程为每个
System V
信号量所设置的semadj
值将会被加到信号量值中。 - 如果该进程是一个管理终端的管理进程,那么系统会向该终端前台进程组中的每个进程发送
SIGHUP
信号,接着终端会与会话脱离。 - 将关闭该进程打开的任何POSIX命名信号量,类似于调用
sem_close()
。 - 将关闭该进程打开的任何POSIX消息队列,类似于调用
mq_close()
。 - 作为进程退出的后果之一,如果某进程组成为孤儿,且该组中存在任何已停止进程,则组中所有进程都将收到
SIGHUP
信号,随之为SIGCONT
信号。 - 移除该进程通过
mlock()
或mlockall()
所建立的任何内存锁。 - 取消该进程调用
mmap()
所创建的任何内存映射。
退出处理程序
退出处理程序是一个由程序设计者提供的函数,可用于进程生命周期的任意时间点注册,并在该进程调用exit()
正常终止时自动执行。如果程序直接调用_exit()
或因信号而异常终止,则不会调用退出处理程序。
GNU C语言函数库提供两种方式来注册退出处理程序。
#include <stdlib.h>
int atexit(void (*func)(void));
//成功,返回0;出错返回非零值
函数atexit()
将func
加到一个函数列表中,进程终止时会调用该函数列表的所有函数。应将函数func
定义为不接受任何参数,也无返回值。
注意atexit()
在出错时返回非0值。
可以注册多个退出处理程序(甚至可以将同一函数注册多次)。当应用程序调用exit()
时,这些函数的执行顺序与注册顺序相反。
本质上,可以在退出处理程序中执行任何希望的动作,包括注册附加的退出处理程序,并将其置于留待调用的剩余函数列表的头部。不过,一旦有任一退出处理程序无法返回——无论因为调用了_exit()
还是进程因收到信号而终止,那么就不会再调用剩余的处理程序。此外,调用exit()
时通常需要执行的剩余动作也将不再执行。
通过fork()
创建的子进程会继承父进程注册的退出处理函数。而进程调用exec()
时,会移除所有已注册的退出处理程序。
经由atexit()
注册的退出处理程序会受到两中限制。其一,退出处理程序在执行时无法获知传递给exit()
的状态。其二,无法给退出处理程序指定参数。如果拥有这一特性,退出处理程序能根据传入参数的不同而执行不同的动作,或使用不同参数多次注册同一个函数。
为摆脱这些限制,glibc
提供了一个替代方法:on_exit()
。
#include <stdlib.h>
int on_exit(void (*func)(int, void*), void* arg);
//成功,返回0,错误,返回非零
函数on_exit()
参数func
是一个指针,指向如下类型的函数
void func(int status, void* arg){}
调用时,会传递两个参数给func()
:提供给exit()
的status
参数和注册时供给on_exit()
的一份arg
参数拷贝。虽然定义为指针类型,参数arg
的意义仍然可由设计者支配。可将其用作指向结构的指针。
fork(),stdio缓冲区以及_exit()之间的交互
执行以下程序
int main(int argc, char *argv[])
{
printf("Hello world\n");
write(STDOUT_FILENO, "Ciao\n", 5);
if(fork() == -1)
errExit("fork");
exit(EXIT_SUCCESS);
}
当程序标准输出定向到终端时,会看到以下结果。
$ ./fork_stdio_buf
Hello world
Ciao
当重定向标准输出到一个文件时,结果如下:
$ ./fork_stdio_buf > a
$ cat a
Ciao
Hello world
Hello world
以上输出中,printf()
的输出行出现了两次,且write()
的输出先于printf()
。
要理解为什么printf()
的输出消息出现了两次,首先要记住,是在进程的用户空间内存中维护stdio
缓冲区的。因此,通过fork()
创建子进程时会复制这些缓冲区。当标准输出定向到终端时,因为是行缓冲,所以立即显示函数printf()
输出的包含换行符的字符串。不过,当标准输出重定向到文件时,由于为全缓冲,当调用fork()
时,printf()
输出的字符串仍在父进程的stdio
缓冲区中,并随子进程的创建而产生一份副本。父,子进程调用exit()
会刷新各自的stdio
缓冲区,从而导致重复的输出结果。
可以采用以下任一方法来避免重复的输出结果。
- 作为针对
stdio
缓冲区问题的特定解决方案,可以在调用fork()
之前使用函数fflush()
来刷新stdio
缓冲区。 - 子进程可以调用
_exit()
而非exit()
,以便不再刷新stdio
缓冲区。这一技术例证了一个更为通用的原则:**在创建子进程的应用中,典型情况下仅有一个进程(一般为父进程)应通过调用exit()
终止,从而确保只有一个进程调用退出处理程序并刷新stdio
缓冲区。