《Linux/UNIX系统编程手册》第25章 进程的终止

关键词:_exit()、exit()、atexit()、on_exit()等等。

1. 进程的终止:_exit()和exit()

_exit()正常终止当前进程:

#include <unistd.h>
void _exit(int status);

_exit()的status参数定义了进程的终止状态,父进程可调用wait()获取该状态。status仅有低8位可为父进程所用。

虽然status取值范围为0~255,但是一般使用0~127。原因在于,当以信号终止一命令时,shell会将变量$?置为128和信号值之和。

#include <stdlib.h>
void exit(int status);

exit()会在调用_exit()前执行各种动作:

  • 调用退出处理程序,其执行顺序与注册顺序相反
  • 刷新stdio流缓冲区
  • 使用由status提供的值执行_exit()系统调用

程序终止的另一种方法是从main()函数中返回(return),或者一直执行到main()函数的结尾处。执行return n等同于执行exit(n),因为调用main()的运行时函数会将main()的返回值作为exit()的参数。

如果退出处理过程中所执行的任何步骤需要访问main()函数的本地变量,那么从main()函数中返回会导致未定义的行为。此时从main()函数中返回和调用exit()并不相同。

执行未指定返回值的return,或是无声无息执行到main()函数结尾,返回值根据C编译器版本有所不同:C89退出状态取自于CPU寄存器中的随机值;C99要求执行main()函数结尾处的情况应等同于调用exit(0)

2. 进程终止的细节

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

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

3. 退出处理程序

 退出处理程序是一个由程序设计者提供的函数,可于进程生命周期的任意点注册,并在该进程调用exit()正常终止时自动执行。如果程序直接调用_exit()或因信号而异常终止,则不会调用退出处理程序。

#include <stdlib.h>
int atexit(void (*func)(void));
    Returns 0 on success, or nonzero on error

atexit()将func加到一个函数列表中,进程终止时会调用该函数列表的所有函数,func无入参也无返回值

atexit()可以注册多个处理程序,也可以将同一函数注册多次。当应用程序调用exit()时,这些函数的执行顺序与注册顺序相反

一旦有任一退出处理程序无法返回,那么就不会再调用剩余的处理程序。此外,调用exit()时通常需要执行的剩余动作也将不再执行。

默认exit()可以注册32个退出处理程序,使用sysconf(_SC_ATEXIT_MAX)可以确定可注册退出处理程序的数量上限。

通过fork()创建的子进程会继承父进程注册的退出处理函数exec()会移除所有已注册的退出处理程序

atexit()注册的退出处理程序会受到两种限制:退出处理程序在执行时无法获知传递给exit()的状态;无法给退出处理程序指定参数。

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

func()两个参数:提供给exit()的status参数和注册时供给on_exit()的一份arg参数拷贝。

on_exit()可以注册多个退出处理程序,和atexit()注册的退出处理程序位于同一函数列表。按照注册的想法顺序来执行相应的退出处理程序。

对于保证移植性来说,atexit()优于on_exit(),避免使用on_exit()

#define _BSD_SOURCE     /* Get on_exit() declaration from <stdlib.h> */
#include <stdlib.h>
#include "tlpi_hdr.h"

#ifdef __linux__        /* Few UNIX implementations have on_exit() */
#define HAVE_ON_EXIT
#endif

static void
atexitFunc1(void)
{
    printf("atexit function 1 called
");
}

static void
atexitFunc2(void)
{
    printf("atexit function 2 called
");
}

#ifdef HAVE_ON_EXIT
static void
onexitFunc(int exitStatus, void *arg)
{
    printf("on_exit function called: status=%d, arg=%ld
",
                exitStatus, (long) arg);
}
#endif

int
main(int argc, char *argv[])
{
#ifdef HAVE_ON_EXIT
    if (on_exit(onexitFunc, (void *) 10) != 0)
        fatal("on_exit 1");
#endif
    if (atexit(atexitFunc1) != 0)
        fatal("atexit 1");
    if (atexit(atexitFunc2) != 0)
        fatal("atexit 2");
#ifdef HAVE_ON_EXIT
    if (on_exit(onexitFunc, (void *) 20) != 0)
        fatal("on_exit 2");
#endif

    exit(2);
}

执行结果如下:

on_exit function called: status=2, arg=20
atexit function 2 called
atexit function 1 called
on_exit function called: status=2, arg=10

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

#include "tlpi_hdr.h"

int
main(int argc, char *argv[])
{
    printf("Hello world
");
    write(STDOUT_FILENO, "Ciao
", 5);

    if (fork() == -1)
        errExit("fork");

    /* Both child and parent continue execution here */

    exit(EXIT_SUCCESS);
}

分别将上面程序执行结果输出到stdio和文件中:

al@al-B250-HD3:~/tlpi/procexec$ ./fork_stdio_buf 
Hello world
Ciao
al@al-B250-HD3:~/tlpi/procexec$ ./fork_stdio_buf > a
al@al-B250-HD3:~/tlpi/procexec$ cat a
Ciao
Hello world
Hello world

当输出到文件中时,printf()输出出现了两次,且write()先于printf()出现。

printf()出现两次

进程是在用户空间内存中维护stdio缓冲区,fork()子进程时会复制这些缓冲区。

标准输出定向到终端时,缺省为行缓冲,所以会立即显示函数printf()输出的包含换行符的字符串。

重定向到文件时,缺省为块缓冲,printf()输出的字符串仍在父进程的stdio缓冲区中,并随子进程的创建而产生一份副本。父子进程调用exit()时会刷新各自的stdio缓冲区,从而导致重复的输出结果。

因为write()会将数据直接传给内核缓冲区,fork()不会复制这一缓冲区

解决方法

  • 针对stdio缓冲区问题的特定解决方法,在调用fork()之前使用函数fflush()来刷新stdio缓冲区。或者使用setvbuf和setbuf()来关闭stdio流的缓冲功能。
  • 子进程可以调用_exit()而非exit(),一遍不再刷新stdio缓冲区。

write()出现先于printf()

wirte()会将数据立即穿给内核高速缓存,而printf()输出则需要等到调用exit()刷新stdio缓冲区。

5. 总结

原文地址:https://www.cnblogs.com/arnoldlu/p/13625553.html