【问题标题】:How can I schedule some code to run after all '_atexit()' functions are completed在所有 '_atexit()' 函数完成后,如何安排一些代码运行
【发布时间】:2009-11-18 01:28:02
【问题描述】:

我正在编写一个内存跟踪系统,我实际遇到的唯一问题是,当应用程序退出时,任何未在其构造函数中分配但在其解构函数中释放的静态/全局类正在释放在我的内存跟踪工具将分配的数据报告为泄漏之后。

据我所知,正确解决此问题的唯一方法是强制将内存跟踪器的 _atexit 回调放置在堆栈的头部(以便最后调用它)或让它执行在整个 _atexit 堆栈被展开之后。是否真的可以实现这些解决方案中的任何一个,或者是否有另一个我忽略的解决方案。

编辑: 我正在为 Windows XP 开发/开发并使用 VS2005 进行编译。

【问题讨论】:

    标签: c++ allocation atexit


    【解决方案1】:

    我终于想出了如何在 Windows/Visual Studio 下做到这一点。再次查看 crt 启动函数(特别是它调用全局初始化器的位置),我注意到它只是运行包含在某些段之间的“函数指针”。因此,只要对链接器的工作原理有一点了解,我就想到了这个:

    #include <iostream>
    using std::cout;
    using std::endl;
    
    // Typedef for the function pointer
    typedef void (*_PVFV)(void);
    
    // Our various functions/classes that are going to log the application startup/exit
    struct TestClass
    {
        int m_instanceID;
    
        TestClass(int instanceID) : m_instanceID(instanceID) { cout << "  Creating TestClass: " << m_instanceID << endl; }
        ~TestClass() {cout << "  Destroying TestClass: " << m_instanceID << endl; }
    };
    static int InitInt(const char *ptr) { cout << "  Initializing Variable: " << ptr << endl; return 42; }
    static void LastOnExitFunc() { puts("Called " __FUNCTION__ "();"); }
    static void CInit() { puts("Called " __FUNCTION__ "();"); atexit(&LastOnExitFunc); }
    static void CppInit() { puts("Called " __FUNCTION__ "();"); }
    
    // our variables to be intialized
    extern "C" { static int testCVar1 = InitInt("testCVar1"); }
    static TestClass testClassInstance1(1);
    static int testCppVar1 = InitInt("testCppVar1");
    
    // Define where our segment names
    #define SEGMENT_C_INIT      ".CRT$XIM"
    #define SEGMENT_CPP_INIT    ".CRT$XCM"
    
    // Build our various function tables and insert them into the correct segments.
    #pragma data_seg(SEGMENT_C_INIT)
    #pragma data_seg(SEGMENT_CPP_INIT)
    #pragma data_seg() // Switch back to the default segment
    
    // Call create our call function pointer arrays and place them in the segments created above
    #define SEG_ALLOCATE(SEGMENT)   __declspec(allocate(SEGMENT))
    SEG_ALLOCATE(SEGMENT_C_INIT) _PVFV c_init_funcs[] = { &CInit };
    SEG_ALLOCATE(SEGMENT_CPP_INIT) _PVFV cpp_init_funcs[] = { &CppInit };
    
    
    // Some more variables just to show that declaration order isn't affecting anything
    extern "C" { static int testCVar2 = InitInt("testCVar2"); }
    static TestClass testClassInstance2(2);
    static int testCppVar2 = InitInt("testCppVar2");
    
    
    // Main function which prints itself just so we can see where the app actually enters
    void main()
    {
        cout << "    Entered Main()!" << endl;
    }
    

    哪个输出:

    Called CInit();
    Called CppInit();
      Initializing Variable: testCVar1
      Creating TestClass: 1
      Initializing Variable: testCppVar1
      Initializing Variable: testCVar2
      Creating TestClass: 2
      Initializing Variable: testCppVar2
        Entered Main()!
      Destroying TestClass: 2
      Destroying TestClass: 1
    Called LastOnExitFunc();
    

    这是由于 MS 编写其运行时库的方式。基本上,他们在数据段中设置了以下变量:

    (虽然此信息是版权信息,但我认为这是合理使用,因为它不会贬低原件,仅供参考)

    extern _CRTALLOC(".CRT$XIA") _PIFV __xi_a[];
    extern _CRTALLOC(".CRT$XIZ") _PIFV __xi_z[];    /* C initializers */
    extern _CRTALLOC(".CRT$XCA") _PVFV __xc_a[];
    extern _CRTALLOC(".CRT$XCZ") _PVFV __xc_z[];    /* C++ initializers */
    extern _CRTALLOC(".CRT$XPA") _PVFV __xp_a[];
    extern _CRTALLOC(".CRT$XPZ") _PVFV __xp_z[];    /* C pre-terminators */
    extern _CRTALLOC(".CRT$XTA") _PVFV __xt_a[];
    extern _CRTALLOC(".CRT$XTZ") _PVFV __xt_z[];    /* C terminators */
    

    在初始化时,程序简单地从 '__xN_a' 迭代到 '__xN_z'(其中 N 是 {i,c,p,t})并调用它找到的任何非空指针。如果我们只是在段 '.CRT$XnA' 和 '.CRT$XnZ' 之间插入我们自己的段(其中 n 再次是 {I,C,P,T}),它将与其他所有内容一起调用通常会被调用。

    链接器只是按字母顺序连接段。这使得选择何时调用我们的函数变得非常简单。如果您查看defsects.inc(在$(VS_DIR)\VC\crt\src\ 下找到),您会看到MS 已将所有“用户”初始化函数(即在代码中初始化全局变量的函数)放在以“U”结尾的段中.这意味着我们只需要将初始化程序放在早于 'U' 的段中,它们将在任何其他初始化程序之前被调用。

    您必须非常小心,不要使用在您选择的函数指针放置之后才初始化的任何功能(坦率地说,我建议您只使用 .CRT$XCT 这样它只有您的代码没有已初始化。我不确定如果您使用标准的“C”代码链接会发生什么,在这种情况下您可能必须将其放在 .CRT$XIT 块中)。

    我确实发现的一件事是,如果您链​​接到运行时库的 DLL 版本,“预终止符”和“终止符”实际上并没有存储在可执行文件中。因此,您不能真正将它们用作通用解决方案。相反,我让它作为最后一个“用户”函数运行我的特定函数的方式是在“C 初始化程序”中简单地调用atexit(),这样,就不会将其他函数添加到堆栈中(将被调用以相反的顺序添加函数,以及调用全局/静态解构器的方式)。

    最后一个(显而易见的)注释,这是在考虑 Microsoft 的运行时库的情况下编写的。它在其他平台/编译器上的工作方式可能类似(希望您只需将段名称更改为他们使用的任何名称,如果他们使用相同的方案),但不要指望它。

    【讨论】:

      【解决方案2】:

      atexit 由 C/C++ 运行时 (CRT) 处理。它在 main() 已经返回之后运行。可能最好的方法是用您自己的替换标准 CRT。

      在 Windows 上,tlibc 可能是一个不错的起点:http://www.codeproject.com/KB/library/tlibc.aspx

      查看 mainCRTStartup 的代码示例,并在调用 _doexit(); 后运行您的代码; 但在 ExitProcess 之前。

      或者,您可以在调用 ExitProcess 时收到通知。当 ExitProcess 被调用时,会发生以下情况(根据http://msdn.microsoft.com/en-us/library/ms682658%28VS.85%29.aspx):

      1. 进程中的所有线程(调用线程除外)都会终止其执行,而不会收到 DLL_THREAD_DETACH 通知。
      2. 在步骤 1 中终止的所有线程的状态都变为信号状态。
      3. 所有加载的动态链接库 (DLL) 的入口点函数都使用 DLL_PROCESS_DETACH 调用。
      4. 在所有附加的 DLL 都执行了任何进程终止代码后,ExitProcess 函数会终止当前进程,包括调用线程。
      5. 调用线程的状态变为信号状态。
      6. 进程打开的所有对象句柄都已关闭。
      7. 进程的终止状态从 STILL_ACTIVE 变为进程的退出值。
      8. 进程对象的状态变为信号状态,满足所有等待进程终止的线程。

      因此,一种方法是创建一个 DLL 并将该 DLL 附加到进程。进程退出时会收到通知,应该是在处理完atexit之后。

      显然,这一切都相当骇人听闻,请小心进行。

      【讨论】:

        【解决方案3】:

        这取决于开发平台。例如,Borland C++ 有一个#pragma 可以用于此目的。 (来自 Borland C++ 5.0,c. 1995)

        #pragma startup function-name [priority]
        #pragma exit    function-name [priority]
        
        这两个 pragma 允许程序指定应该在程序启动时(在调用 main 函数之前)或程序退出(就在程序通过 _exit 终止之前)调用的函数。 指定的函数名必须是先前声明的函数:
        void function-name(void);
        
        可选优先级应在 64 到 255 范围内,最高优先级为 0;默认值为 100。具有较高优先级的函数在启动时首先调用,最后在退出时调用。 C 库使用从 0 到 63 的优先级,用户不应使用。

        也许您的 C 编译器有类似的功能?

        【讨论】:

        • 我实际上是在 VC++ 和 g++ 上寻找这个,非常巧合。我希望有人能详细说明。
        • 我查看了 VS2008 文档,但什么也没看到。我什至尝试在“#pragma”上使用帮助,但它使帮助工具崩溃。嘘!
        • 感谢您的尝试。 :) 我知道这是可能的,我曾经在某个地方看到过。如果我找到了,我会告诉你的。
        • 如果有请发帖,我在#pragma 帮助下查看了 MSDN 文档,但似乎没有这样的内容。
        【解决方案4】:

        我多次阅读您无法保证全局变量的构造顺序(cite)。我认为由此推断析构函数的执行顺序也不能保证是非常安全的。

        因此,如果您的内存跟踪对象是全局的,那么您几乎可以肯定无法保证您的内存跟踪器对象会最后被破坏(或首先被构造)。如果它没有最后被破坏,并且其他分配未完成,那么是的,它会注意到您提到的泄漏。

        另外,这个 _atexit 函数是为什么平台定义的?

        【讨论】:

        • 您可以保证销毁顺序反映构建。
        • 如果是这样,那么由于构建顺序无法保证,您无法预测镜像销毁顺序。 – Doug T. 0 秒前
        • 您不能强制不同翻译单元中全局变量的构造顺序。 R.Pate 的解决方案是将所有全局变量放在同一个翻译单元中,确实保证了顺序。
        • 3.6.3:“静态存储持续时间(在块范围或命名空间范围内声明)的初始化对象的析构函数被调用......以它们的构造函数完成的相反顺序”。我简化了一点,这与应用于具有初始化而不是构造的事物的相同规则混合在一起。
        • @Caspin:是的确实如此。所有静态存储持续时间对象(包括静态函数局部变量)的析构函数都以构造函数的相反顺序调用(保证)。如果不调用构造函数(对于静态函数局部变量),则不调用析构函数。
        【解决方案5】:

        最后执行内存跟踪器的清理是最好的解决方案。我发现最简单的方法是显式控制所有相关全局变量的初始化顺序。 (一些库将它们的全局状态隐藏在花哨的类或其他方式中,认为它们遵循某种模式,但它们所做的只是阻止这种灵活性。)

        main.cpp 示例:

        #include "global_init.inc"
        int main() {
          // do very little work; all initialization, main-specific stuff
          // then call your application's mainloop
        }
        

        全局初始化文件包括对象定义和#includes 类似的非头文件。按照您希望它们构造的顺序对该文件中的对象进行排序,它们将以相反的顺序被破坏。 C++03 中的 18.3/8 保证销毁顺序镜像构造:“具有静态存储持续时间的非本地对象按照其构造函数完成的相反顺序销毁。” (那部分是在讨论exit(),但是从 main 的返回是一样的,参见 3.6.1/5。)

        作为奖励,您可以保证所有全局变量(在该文件中)在进入 main 之前都已初始化。 (标准中没有保证,但如果实现选择允许。)

        【讨论】:

        • 一些库将其全局状态隐藏在具有静态存储持续时间的本地范围变量中。这还允许您通过决定调用包含这些静态变量的函数的顺序来控制顺序。当然还有线程安全问题。
        • @steve:我喜欢修复全球建筑秩序的技巧。但是它对销毁顺序没有影响。局部静态变量销毁命令对所有人完全免费。
        • 它们的销毁顺序与其构造函数的完成顺序相反(3.6.3)。这适用于所有具有静态存储持续时间的变量,无论其范围如何。因此,当(且仅当)施工订单是时,销毁订单是免费的。当然,如果不是每个人都使用相同的方案,那么命名空间范围的全局变量仍然会导致在 main() 开始之前构建事物,如果它们位于不同的翻译单元中,那么它们会以不可预知的顺序构建,并且您会失去控制。
        • Steve:线程安全是我更喜欢这个解决方案的原因之一:保持全局状态最小化并在定义明确的点初始化——这简洁灵活,使用“在 main 之前”作为那个点.
        • Also 18.3/8:“如果调用 obj3 析构函数的函数在 obj3 构造函数完成时向 atexit 注册,则本地静态对象 obj3 会同时被销毁”。并不是说 C++ 标准会强制要求实现细节。它只是暗示了很多;-)
        【解决方案6】:

        我也遇到过这个问题,也在写一个内存跟踪器。

        一些事情:

        除了破坏之外,您还需要处理构造。在构建内存跟踪器之前准备好调用 malloc/new(假设它是作为一个类编写的)。所以你需要你的类知道它是被构造了还是被破坏了!

        class MemTracker
        {
            enum State
            {
              unconstructed = 0, // must be 0 !!!
              constructed,
              destructed
            };
            State state;
        
            MemTracker()
            {
               if (state == unconstructed)
               {
                  // construct...
                  state = constructed;
               }
            }
        };
        
        static MemTracker memTracker;  // all statics are zero-initted by linker
        

        在调用跟踪器的每个分配上,构造它!

        MemTracker::malloc(...)
        {
            // force call to constructor, which does nothing after first time
            new (this) MemTracker();
            ...
        }
        

        奇怪,但真实。无论如何,走向毁灭:

            ~MemTracker()
            {
                OutputLeaks(file);
                state = destructed;
            }
        

        因此,在销毁时,输出您的结果。然而,我们知道会有更多的电话。该怎么办?嗯……

           MemTracker::free(void * ptr)
           {
              do_tracking(ptr);
        
              if (state == destructed)
              {
                  // we must getting called late
                  // so re-output
                  // Note that this might happen a lot...
                  OutputLeaks(file); // again!
               }
           }
        

        最后:

        • 小心穿线
        • 注意不要在跟踪器中调用 malloc/free/new/delete,或者能够检测到递归等 :-)

        编辑:

        • 我忘了,如果您将跟踪器放在 DLL 中,您可能需要您自己 LoadLibrary()(或 dlopen 等)来增加您的引用计数,这样您就不会过早地从记忆中删除。因为虽然你的类在销毁之后仍然可以被调用,但是如果代码已经被卸载就不能了。

        【讨论】:

        • 您可以通过将静态变量放在随后返回它的函数中来完全避免创建问题。然后在第一次调用该函数时构造它。剩下要处理的是在调用析构函数之后发生的事件。这是该方法的一个很好的概述:entropygames.net/…
        • 嗯,你当然是对的。我一定是把我的记忆和另一个案例混为一谈了——我想在那种情况下我确实不得不担心线程,并且必须更加小心地构造。那是几年前的事了。无论哪种方式,“销毁后,每次调用时输出”绝对是我用于解决所提到的确切问题的技术。也许我应该留在话题上。
        猜你喜欢
        • 2020-12-15
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-08-21
        • 1970-01-01
        • 2014-05-22
        • 1970-01-01
        • 2022-01-10
        相关资源
        最近更新 更多