The exit(3) function and destructors

  • Post author:
  • Post category:IT
  • Post comments:0评论

本文主要探讨一下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种方式:

  1. inton_exit(void (*function)(int , void *), void *arg); //历史古老函数
  2. intatexit(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链表中删除。

发表回复