zoukankan      html  css  js  c++  java
  • 《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. 总结

  • 相关阅读:
    树莓派4b 对于 Failed to execute command 的解决方案
    Windows 10 启用以太网/公共网络共享
    树莓派 配置 OMV 搭建 NAS(四) 文件夹共享
    树莓派 配置 OMV 搭建 NAS(三) 硬盘挂载
    树莓派 配置 OMV 搭建 NAS(二) 配置 OMV 5
    树莓派 系统配置
    树莓派/Debian 网线连接主机
    树莓派 配置 OMV 5 搭建 NAS(一) 安装 OMV 5
    Vmware 15 虚拟机挂载硬盘
    p9半幺群
  • 原文地址:https://www.cnblogs.com/arnoldlu/p/13625553.html
Copyright © 2011-2022 走看看