The exit(3) function and destructors


本文主要探讨一下Linux下exit函数是怎么实现的,以及C++中的全局对象的析构函数是如何注册到exit函数的执行过程中。

exit(3)函数的声明及实现

C语言标准库中的exit(3)函数用于终止当前进程。
Linux为例,main函数是被glibc的LIBC_START_MAIN函数调用。LIBC_START_MAIN内部大概是这样:
result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);
exit (result);
在main函数返回后立刻调用exit函数。而exit函数中会处理那些析构函数。下面是glibc中exit函数的实现:
void exit (int status) {
  __run_exit_handlers (status, &__exit_funcs, true);
}

__exit_funcs是个全局变量。它是一个链表,
类型声明在stdlib/cxa_atexit.h:

struct exit_function_list
  {
    struct exit_function_list *next;
    size_t idx;
    struct exit_function fns[32];
  };
变量定义在stdlib/cxa_atexit.c:
static struct exit_function_list initial;
struct exit_function_list *__exit_funcs = &initial;
这个链表稍有点复杂。它是把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种方式

  1. int on_exit(void (*function)(int , void *), void *arg); //历史古老函数
  2. int atexit(void (*function)(void)); //C89, C99, POSIX.1-2001.标准
  3. 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链表中删除。
后记: 后来我在Y司工作的时候,遇到一次变量重定义导致运行时崩溃的问题,就是依靠查找exit函数的析构列表找出的重名变量。

此博客中的热门博文

在windows下使用llvm+clang

少写代码,多读别人写的代码

tensorflow distributed runtime初窥