D 25章 进程的终止

D 25章 进程的终止 440

25.1 进程的终止:_exit()和exit() 440

  1. _exit(int status), status 定义了终止状态,父进程可调用 wait 获取。仅低8位可用,

调用 _exit() 总是成功的。

2.程序一般不会调用 _exit(), 而是调用库函数 exit()。

exit() 执行如下动作:

1.调用退出处理程序(通过 atexit() 和 on_exit() 注册的函数),其执行顺序与注册顺序相反。

2.刷新 stdio 流缓冲区

3.使用由 status 提供的值执行 _exit() 系统调用。

与专属的Unix的 _exit 不同, exit() 则属于标准C语言函数库。

25.2 进程终止的细节 441

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

1.关闭所有打开文件描述符,目录流,信息目录描述符,以及转换描述符

2.作为文件描述符关闭的后果之一,将释放该进程所持有的任何文件锁

3.分离任何已经连接的 System V 共享内存段,且对应于各段的 shm_nattch 计数器值减一

4.进程为每个 System V 信号量所设置的 semadj 值将会被加到信号量值中。

5.如果该进程是一个管理终端的管理进程,那么系统会向该终端前台进程组中的每个进程发送 sighup 信号,

接着终端会于会话脱离。

6.将关闭该进程打开的任何 POSIX 有名信号量,类似调用 sem_close()

7.将关闭该进程打开的任何 POSIX 消息队列,类似于调用 mq_close()

8.作为进程退出的后果之一,如果某进程组称为孤儿,且该组中存在任何已停止进程,则组中所有进程都将收到 sighup

信号,随着为 sigcont 信号。

9.移除该进程通过 mlock() 和 mlockall() 锁建立的任何内存锁

10.取消该进程调用 mmap() 所创建的任何内存映射。

25.3 退出处理程序 442

退出处理程序是一个由程序设计者提供的函数,可于进程生命周期的任意时间点注册,并在该进程调用 exit() 正常终止时

自动执行。如果程序直接调用 _exit() 或者因信号而异常终止,则不会调用退出处理程序。

当程序收到信号而终止时,将不会调用退出处理程序。这一事实一定程序上限制了它们的效用。此时最佳的应对方式莫若为可能发送

给进程的信号建立信号处理程序,并于其中设置标志位,领主程序据此来调用 exit()。因为 exit() 不属于异步信号安全函数,所有通常

不能在信号处理程序中对其发起调用。

注册退出处理程序

atexit();

1.概念:一般内核中每个启动的进程默认都有一个标准的默认终止函数,用于在进程终止时执行的函数,该函数主要用来释放进程所占用的资源,也可以自定义终止函数。按照ISO C规定,一个进程可以注册32个终止函数,这些函数将由exit函数自动调用。

登记的终止函数以栈的形式运行,先注册的后执行。如果自定义注册了进程终止函数,那么内核提供的默认的终止函数将会被覆盖。

原文链接:https://blog.csdn.net/qq_35733751/article/details/82392918

希望进程在结束时,进行一些清理工作,比如某些重要的数据保存到文件中。如果不登记终止函数的话,实现这个操作是有些困难的,因为进程有可能是因为某个函数调用失败,且该函数出错时又调用了exit函数导致进程终止,更糟糕的是出错的函数是不确定的。这个时候就可以通过登记注册终止函数,这样进程在终止时,会自动执行注册的终止函数把数据保存到文件中,方便了许多。

  1. atexit函数语义

atexit函数是用来在内核中注册一个进程终止时执行的函数,通过atexit函数所包含的头文件来看,atexit函数是一个C库函数。

函数原型:

#include <stdlib.h>

int atexit(void(*function)(void));

atexit函数的参数是一个函数指针,表示注册的终止函数,终止函数语法格式为:void(*function)(void) 。

返回值:成功返回0,失败返回不一定是-1

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <fcntl.h>

#include <string.h>

//以下func1,func2,func3都是线程终止函数

void term_func1(void){

puts("term func1");

}

void term_func2(void){

puts("term func2");

}

void term_func3(void){

puts("term func3");

}

int main(int argc , char *args[]){

if(argc < 3){

fprintf(stderr,"use: %s file[exit | _exit | return]\n" ,args[0]);

exit(0);

}

//注册终止函数

atexit(term_func1);

atexit(term_func2);

atexit(term_func3);

//打开文件

FILE *fp = fopen(args[1] , "w");

if(NULL == fp){

perror("fopen");

exit(-1);

}

//向文件写入数据

fprintf(fp , "hello linux");

if(!strcmp(args[2], "exit")){

exit(0); //标准C库函数

复制代码
    }else if(!strcmp(args[2], "_exit")){
            _exit(0);       //系统调用

    }else if(!strcmp(args[2], "return")){
            return 0;       //正常返回
    }else{
            fprintf(stderr,"use: %s file[exit | _exit | return]\n", args[0]);
    }
    exit(0);

}

通过atexit函数注册的终止函数并非在所有情况下都会被调用,是否调用终止函数这取决于进程的终止方式

(base) wannian07@wannian07-PC:~/Desktop/std/linux prog$ gcc -o atexit_process atexit_process.c -lpthread

(base) wannian07@wannian07-PC:~/Desktop/std/linux prog$ ./atexit_process test.txt return

term func3

term func2

term func1

(base) wannian07@wannian07-PC:~/Desktop/std/linux prog$ cat test.txt

hello linux

当前进程以return形式退出,终止函数是以栈的形式执行的,先注册的终止函数后执行。然后通过cat命令查看test.txt文件的内容为hello linux 。

(base) wannian07@wannian07-PC:~/Desktop/std/linux prog$ ./atexit_process test.txt exit

term func3

term func2

term func1

(base) wannian07@wannian07-PC:~/Desktop/std/linux prog$ cat test.txt

hello linux

exit方式和return方式的结果是一样的,也调用了注册的终止函数。

(base) wannian07@wannian07-PC:~/Desktop/std/linux prog$ ./atexit_process test.txt _exit

(base) wannian07@wannian07-PC:~/Desktop/std/linux prog$ cat test.txt

_exit方式和前两种有所不同,之前注册的进程终止函数一个都没有执行,另外,test.txt文件倒是创建了,但是使用cat命令查看文件中并没有内容。

总结

不同的进程终止方式会产生不同的结果,如果我们选择return方式和exit方式,进程在终止之前会调用注册的进程终止函数,但是选择_exit方式,就算注册了终止函数,进程在终止之前也不会调用。

另外,需要注意的是在程序中写入文件数据用的函数是一个标准C库函数,在使用fprintf函数写入数据时,实际上数据并不会写入文件,而是先写入buff缓冲区中,这种方式也叫全缓存,只有当缓冲区写满或者调用fclose函数才会把数据写入文件中。

当程序终止时也会把数据写入文件,但是会有一些区别,如果使用return方式和exit方式让程序退出,会刷新buf中的数据到文件中,但是以_exit方式让程序退出,是不会刷新数据到文件中。

_exit是系统调用,进程终止前,不会调用终止函数,也不会刷新数据到文件中,而是直接进入内核。而exit和return是标准库函数,进程终止前,会调用终止函数,也会刷新数据到文件中。

因此我们可以把进程终止方式简单总结为:

图1-进程终止方式

进程启动和退出------atexit函数

下面这种图也证实了exit函数内部调用了_exit函数。

图2-进程的启动和退出过程

进程的启动和退出过程大概如图2所示:

1.进程在运行时,首先内核会先启动一个例程,这个例程的作用是加载程序运行的参数,环境变量,在内核注册终止函数等这些工作,然后启动例程会调用main函数。

2.main函数调用了exit函数使进程终止退出,在进程终止之前,如果注册了终止函数,那么exit函数会先去依次调用进程终止函数,注册了几个就调用几个,每调用完一个终止函数并返回,调用顺序是以栈的形式来调用,然后调用flush刷新IO缓冲区的数据再返回,最后调用了系统调用_exit或_Exit,然后进程终止退出。

3.值得注意的是main函数也可以通过调用系统调用_exit直接进入内核使进程终止并退出,通过调用系统调用的方式使进程终止的话,并不会调用注册的进程终止函数。也可以通过main函数调用用户函数,然后用户函数调用exit,然后依次调用进程终止函数,再调用标准I/O函数刷新缓冲区,最后调用系统调用_exit使进程终止退出。

4.当然,用户函数也是可以直接通过系统调用_exit()进入内核使进程终止退出的。

原文链接:https://blog.csdn.net/qq_35733751/article/details/82392918

一个操作系统的实现https://blog.csdn.net/chuanwang66/category_8332297.html

on_exit();

用来注册执行exit()函数前执行的终止处理程序。

函数声明

#include <stdlib.h>

int on_exit(void (*function)(int , void *), void *arg);

功能描述

on_exit()用来注册终止处理程序,当程序通过调用exit()或从main 中返回时被调用, 终止处理程序有两个参数,第一个参数是来自最后一个exit()函数调用中的status,第二个参数是来自on_exit()函数中的arg;

同一个函数若注册多次,那它也会被调用多次;

当一个子进程是通过调用fork()函数产生时,它将继承父进程的所有终止处理程序。在成功调用exec系列函数后,所有的终止处理程序都会被删除。

返回值

成功返回0,失败返回非0值。

#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\n");

}

static void

atexitFunc2(void)

{

printf("atexit function 2 called\n");

}

#ifdef HAVE_ON_EXIT

static void

onexitFunc(int exitStatus, void *arg)

{

printf("on_exit function called: status=%d, arg=%ld\n",

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);

}

(base) wannian07@wannian07-PC:~/Desktop/std/linux prog$ gcc exit_handlers.c -o exit_handlers error_functions.c curr_time.c get_num.c

(base) wannian07@wannian07-PC:~/Desktop/std/linux prog$ ./exit_handlers

on_exit function called: status=2, arg=20

atexit function 2 called

atexit function 1 called

on_exit function called: status=2, arg=10

复制代码
25.4 fork()、stdio缓冲区以及_exit()之间的交互 445  

#include "tlpi_hdr.h"

int

main(int argc, char *argv[])

{

printf("Hello world\n");

write(STDOUT_FILENO, "Ciao\n", 5);

复制代码
if (fork() == -1)
    errExit("fork");

/* Both child and parent continue execution here */

exit(EXIT_SUCCESS);

}

(base) wannian07@wannian07-PC:~/Desktop/std/tlpi-dist/procexec$ make fork_stdio_buf

cc -std=c99 -D_XOPEN_SOURCE=600 -D_DEFAULT_SOURCE -g -I.../lib -pedantic -Wall -W -Wmissing-prototypes -Wno-sign-compare -Wno-unused-parameter fork_stdio_buf.c .../libtlpi.a .../libtlpi.a -lm -o fork_stdio_buf

(base) wannian07@wannian07-PC:~/Desktop/std/tlpi-dist/procexec$ ./fork_stdio_buf

Hello world

Ciao

(base) wannian07@wannian07-PC:~/Desktop/std/tlpi-dist/procexec$ ./fork_stdio_buf > a

(base) wannian07@wannian07-PC:~/Desktop/std/tlpi-dist/procexec$ cat a

Ciao

Hello world

Hello world

复制代码
printf() 信息输出了2次,是在进程的用户空间内存维护 stiod 缓冲区的。因此,通过 fork 创建的子进程会复制这些缓冲区。

父子进程调用 exit() 时,会各自刷新 stdio 缓冲区,从而导致重复输出。

可以采用以下2种方法避免:

1.可以在调用 fork 之前,使用 fflush() 来刷新 stdio 缓冲区,作为另外一种选择,使用 setvbuf() 和 setbuf() 来关闭 stdio 流缓冲。

2.子进程可以调用 _exit() 而非 exit(),以便不刷新 stdio 缓冲区。

这一技术例证了更为通用的原则:在创建子进程的应用中,典型情况下仅一个进程(一般为父进程)应通过调用 exit() 终止,而其他进程应调用 _exit()终止,

从而确保一个进程调用退出处理程序刷新 stdio 缓冲区。

write 并未出现2次,是因为 write 会将数据直接传递给内核缓冲区,fork 不会复制这一缓冲区。

相关推荐
路溪非溪12 分钟前
关于Linux内核中头文件问题相关总结
linux
海绵不是宝宝8171 小时前
连接远程服务器上的 jupyter notebook,解放本地电脑
服务器·jupyter·github
三坛海会大神5552 小时前
计算机网络参考模型与子网划分
网络·计算机网络
云卓SKYDROID2 小时前
无人机激光测距技术应用与挑战
网络·无人机·吊舱·高科技·云卓科技
Lovyk3 小时前
Linux 正则表达式
linux·运维
Fireworkitte3 小时前
Ubuntu、CentOS、AlmaLinux 9.5的 rc.local实现 开机启动
linux·ubuntu·centos
sword devil9004 小时前
ubuntu常见问题汇总
linux·ubuntu
ac.char4 小时前
在CentOS系统中查询已删除但仍占用磁盘空间的文件
linux·运维·centos
繁星¹⁸⁹⁵5 小时前
通过update-alternatives可以实现cuda的多版本切换
服务器
淮北也生橘126 小时前
Linux的ALSA音频框架学习笔记
linux·笔记·学习