本文主要探讨一下Linux下exit函数是怎么实现的,以及C++中的全局对象的析构函数是如何注册到exit函数的执行过程中。
exit(3)函数的声明及实现
C语言标准库中的exit(3)函数用于终止当前进程。
NAME
exit - cause normal process termination
SYNOPSIS
#include <stdlib.h>
void exit(int status);
DESCRIPTION
The exit() function causes normal process termination and the value of status & 0377 is returned to the parent (see
wait(2)).
All functions registered with atexit(3) and on_exit(3) are called, in thereverseorder of their registration.
以Linux为例,main函数是被glibc的LIBC_START_MAIN函数调用。LIBC_START_MAIN内部大概是这样:
result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);
exit (result);
在main函数返回后立刻调用exit函数。而exit函数中会处理那些析构函数。
实际上,每个C/C++程序内部都有一个全局的链表叫:__exit_funcs。
它的类型是:
struct exit_function_list { struct exit_function_list *next; size_t idx; struct exit_function fns[32]; }; exit_function_list* __exit_funcs;
这个链表稍有点复杂。它是把exit_function每32个组成一个块,然后把这些块用单向链表串起来。这个链表的头指针是__exit_funcs。每当要构造一个新块(exit_function_list对象)时,它总是被插入在这个链表的头部。往每个exit_function_list里写入function时,是按照index 0、1、2、3这样的方式顺序写入。idx代表下一个空闲位的index,所以它的初始值是0,每写入一个function就加1。如果idx等于sizeof (fns)/sizeof (fns[0]),就代表写满了,得申请要给新的exit_function_list插在头部。
在exit函数中会从前往后遍历这个链表并挨个调用exit_function。遍历的顺序必须与插入的顺序相反。所以对于这个链表来讲,是从头向后遍历(依靠next指针)。对于每个exit_function_list对象来说,是按照idx、idx-1… 2,1,0 这样的顺序遍历fns。
向exit函数中注册handler有3种方式:
- inton_exit(void (*function)(int , void *), void *arg); //历史古老函数
- intatexit(void (*function)(void)); //C89, C99, POSIX.1-2001.标准
- int __cxa_atexit(void (*func) (void *), void * arg, void * dso_handle); //gcc的__attribute__((destructor))同属于此类。
为了兼容这三种不同的接口,每个exit_function结构体内部有一个名为flavor的字段来标明它的类型。有如下几种
enum { ef_free, ef_us, ef_on, ef_at, ef_cxa };
其中ef_free代表这是一个空闲槽。ef_us代表这个槽刚刚被分配,但是类型未知(之后应该被赋值为ef_on, ef_at, ef_cxa之一)。ef_on就是on_exit注册的;ef_at就是at_exit注册的;ef_cxa就是__cxa_atexit注册的。
除此flavor之外,exit_function还要用一个union把这三种不同函数指针及传入的参数保存下来。具体可见glibc的stdlib/exit.h。
析构函数的注册:
__cxa_atexit是为C++和动态链接库特别新加的。C++中全局对象的析构函数应该在exit函数执行中被调用。
“Destructors (12.4) for initialized objects (that is, objects whose lifetime (3.8) has begun) with static storage duration are called as a result of returning from main and as a result of calling std::exit (18.5).” —ISO_IEC-14882-2011
根据posix标准,用atexit函数注册的handlers的数量是有上限的。这个限制可以通过sysconf函数获得。
#include <stdio.h> #include <unistd.h> int main(){ long a = sysconf(_SC_ATEXIT_MAX); printf("ATEXIT_MAX = %ld\n", a); return 0; }
posix标准规定,这个上限至少为32。
出于以下两个原因,
1. _SC_ATEXIT_MAX可能很小,不够用。(实际上,近代的Linux上,exit函数的handlers是用链表实现的,近乎无限长,所以atexit不存在个数限制。)
2. 原标准没有考虑到动态链接库卸载的问题。析构函数应该在动态链接库卸载的时候调用,而不是直到整个进程要退出(exit)的时候。
所以Linux又定义了一个新函数 __cxa_atexit,并规定析构函数必须用__cxa_atexit函数注册,并且在动态链接库卸载时(dlclose),链接器应使用__cxa_finalize函数来清理(调用那些析构函数)。具体可参见 Itanium C++ ABI (http://refspecs.linuxfoundation.org/cxxabi-1.86.html)
__cxa_atexit与exit函数最大的不同是,__cxa_finalize只调用类型为ef_cxa并且dso相符的,并把它们从__exit_funcs链表中删除。
由 udpwork.com 聚合
|
评论: 0
|
要! 要! 即刻! Now!