进程终止——笔记

进程终止

进程的终止:_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缓冲区。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值