Como posso agendar algum código para ser executado após todos os '_atexit()' funções são concluídas

StackOverflow https://stackoverflow.com/questions/1753042

  •  20-09-2019
  •  | 
  •  

Pergunta

Eu estou escrevendo uma memória de sistema de rastreamento e o único problema que eu realmente está quando o aplicativo é encerrado, qualquer static/classes globais que não alocar em seu construtor, mas são desalocando em suas deconstructor são desalocando depois da minha memória rastreamento coisas informou os dados alocados como uma fuga.

Tanto quanto eu posso dizer, a única maneira para mim adequadamente resolver isso seria forçar a colocação da memória do tracker _atexit de retorno de chamada na cabeça da pilha (por isso que é chamado last) ou ter que executar depois de toda a _atexit pilha foi desfeita.É realmente possível para implementar essas soluções, ou existe outra solução que eu tenha esquecido.

Editar:Eu estou a trabalhar/desenvolver para o Windows XP e a compilação com o VS2005.

Foi útil?

Solução

Eu finalmente descobri como fazer isso no Windows/Visual Studio.Olhando através da crt função de inicialização novamente (especificamente onde ele chama os inicializadores para globals), notei que ela estava simplesmente executando a função "ponteiros" que estavam contidas entre determinados segmentos.Assim, com apenas um pouco de conhecimento sobre como o linker funciona, me veio com essa:

#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;
}

saídas:

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

Isso funciona devido à maneira como o MS ter escrito a sua biblioteca de tempo de execução.Basicamente, eles o programa de configuração as seguintes variáveis de segmentos de dados:

(embora esta informação é direitos de autor creio que este é o fair use como não desvalorizar o original e ESTÁ aqui apenas para referência)

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 */

Na inicialização, o programa simplesmente repete o processo de '__xN_' para '__xN_z' (onde N é {i,c,p,t}) e chamadas de qualquer não nulo de ponteiros de encontrar.Se nós apenas inserir o nosso próprio segmento entre os segmentos '.CRT$XnA' e '.CRT$XnZ' (onde, mais uma vez n é {I,C,P,T}), ele será chamado, juntamente com tudo o que normalmente é chamado.

O vinculador simplesmente junta-se os segmentos em ordem alfabética.Isso torna extremamente simples para selecionar quando o nosso funções deve ser chamado.Se você tiver uma olhada em defsects.inc (encontrado em $(VS_DIR)\VC\crt\src\) você pode ver que o MS colocou todos os "usuário" funções de inicialização (isto é, aqueles que inicializar as variáveis globais no seu código) em segmentos que terminam com 'U'.Isso significa que nós só precisamos colocar a nossa inicializadores em um segmento anterior de 'U' e eles vão ser chamada antes de qualquer outra inicializadores.

Você deve ter muito cuidado em não utilizar qualquer funcionalidade que não está inicializado até que, depois de selecionadas para a colocação dos ponteiros de função (francamente, eu recomendo que você apenas use .CRT$XCT de que forma o seu só seu código que ainda não foi inicializado.Eu não tenho certeza do que vai acontecer se você tiver vinculado com o padrão " C " do código, poderá ter de o colocar no .CRT$XIT bloco em que o caso).

Uma coisa que eu descobri foi que o "pré-terminadores" e "terminadores" na verdade, não são armazenados no executável, se você ligar versões de DLL de biblioteca de tempo de execução.Devido a isso, você não pode realmente usá-los como uma solução geral.Em vez disso, a forma que eu fiz ele executar minha função específica, como o último "usuário" função era simplesmente chamada de atexit() dentro do 'C inicializadores', desta forma, nenhuma outra função poderia ter sido adicionados à pilha (os quais serão chamados na ordem inversa a que funções são adicionadas e é como global/static deconstructors são todos chamados).

Apenas um final (óbvio), note que esta é escrito com o Microsoft biblioteca de tempo de execução em mente.Ele pode funcionar semelhantes em outras plataformas/compiladores (espero que você será capaz de fugir com apenas mudando o segmento de nomes para tudo o que eles usam, SE eles usam o mesmo esquema), mas não conte com isso.

Outras dicas

O ATEXIT é processado pelo tempo de execução C/C ++ (CRT). Ele é executado depois que Main () já retornou. Provavelmente, a melhor maneira de fazer isso é substituir o CRT padrão pelo seu.

No Windows, o TLIBC é provavelmente um ótimo lugar para começar: http://www.codeproject.com/kb/library/tlibc.aspx

Veja o exemplo de código para maincrtstartup e basta executar seu código após a chamada para _doexit (); Mas antes de sair do ExitProcess.

Como alternativa, você pode ser notificado quando o Exitprocess for chamado. Quando o ExitProcess é chamado, ocorre o seguinte (de acordo com http://msdn.microsoft.com/en-us/library/ms682658%28vs.85%29.aspx):

  1. Todos os threads do processo, exceto o encadeamento de chamada, encerrarem sua execução sem receber uma notificação DLL_THREAD_DETACH.
  2. Os estados de todos os threads terminados na etapa 1 são sinalizados.
  3. As funções de ponto de entrada de todas as bibliotecas de link dinâmico carregadas (DLLs) são chamadas com dll_process_detach.
  4. Depois que todas as DLLs anexadas executam qualquer código de terminação do processo, a função ExitProcess encerra o processo atual, incluindo o encadeamento de chamada.
  5. O estado do segmento de chamada é sinalizado.
  6. Todas as alças de objeto abertas pelo processo estão fechadas.
  7. O status de terminação do processo muda de Still_Active para o valor de saída do processo.
  8. O estado do objeto do processo é sinalizado, satisfazendo quaisquer encadeamentos que estavam esperando o terminação do processo.

Portanto, um método seria criar uma DLL e ter essa DLL anexada ao processo. Ele será notificado quando o processo sair, que deve ser processado após o ATEXIT.

Obviamente, tudo isso é bastante hackish, prossiga com cuidado.

Isso depende da plataforma de desenvolvimento. Por exemplo, o Borland C ++ possui um #Pragma que pode ser usado exatamente para isso. (De Borland C ++ 5.0, c. 1995)

#pragma startup function-name [priority]
#pragma exit    function-name [priority]
Esses dois Pragmas permitem que o programa especifique funções que devem ser chamadas na inicialização do programa (antes que a função principal seja chamada) ou a saída do programa (pouco antes do término do programa através do _Exit). O nome da função especificado deve ser uma função declarada anteriormente como:
void function-name(void);
A prioridade opcional deve estar na faixa de 64 a 255, com maior prioridade em 0; O padrão é 100. As funções com prioridades mais altas são chamadas primeiro na inicialização e a última na saída. As prioridades de 0 a 63 são usadas pelas bibliotecas C e não devem ser usadas pelo usuário.

Talvez o seu compilador C tenha uma instalação semelhante?

Eu li várias vezes que não pode garantir a ordem de construção de variáveis globais (cite).Eu acho que é bastante seguro inferir a partir disso que o processo de destruição ordem de execução também não é garantido.

Portanto, se a sua memória de rastreamento de objeto é global, é quase certo que você não consiga quaisquer garantias de que a sua memória do objeto controlador terá destruídos última (ou construídos em primeiro lugar).Se não destruídos último, e outras atribuições são excelentes, aí sim ele vai notar a vazamentos que você menciona.

Também, de qual plataforma é este _atexit função definida para?

Tendo a memória do tracker de limpeza executado por último é a melhor solução.A maneira mais fácil que encontrei para fazer isso é para controlar explicitamente todas as variáveis globais' ordem de inicialização.(Algumas bibliotecas de ocultar o seu estado global em fantasia classes ou de outra forma, pensando que eles estão seguindo um padrão, mas tudo o que eles fazem é evitar que este tipo de flexibilidade.)

Exemplo main.cpp:

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

Onde o global-arquivo de inicialização inclui definições de objeto e #inclui semelhantes não-arquivos de cabeçalho.Ordem os objetos neste arquivo, na ordem em que você deseja que eles construídos, e eles vão ser destruídos em ordem inversa.18.3/8 em C++03 garantias de que a destruição da ordem espelhos construção:"Não-locais, objetos com duração de armazenamento estático são destruídos na ordem inversa da conclusão de seus construtor". (Esta seção está falando exit(), mas um retorno a partir principal é a mesma, veja 3.6.1/5.)

Como um bônus, você tem a garantia de que todas as variáveis globais (no arquivo) são inicializados antes de entrar principal.(Algo não está garantida na norma, mas permitiu que se implementações de escolher.)

Eu tive esse problema exato, escrevendo também um rastreador de memória.

Algumas coisas:

Junto com a destruição, você também precisa lidar com a construção. Esteja preparado para que o MALLOC/NOVO seja chamado antes que seu rastreador de memória seja construído (supondo que seja escrito como uma classe). Então, você precisa da sua classe para saber se ela foi construída ou destruída ainda!

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

Em todas as alocação que chama seu rastreador, construa -o!

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

Estranho mas verdade. De qualquer forma, na destruição:

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

Então, na destruição, produza seus resultados. No entanto, sabemos que haverá mais ligações. O que fazer? Nós iremos,...

   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!
       }
   }

E por fim:

  • Tenha cuidado com a rosca
  • Cuidado para não ligar para Malloc/Free/New/Excluir dentro do seu rastreador ou ser capaz de detectar a recursão, etc :-)

EDITAR:

  • E eu esqueci, se você colocar seu rastreador em uma DLL, provavelmente precisará carregar o carregamento () (ou dlopen, etc.) você mesma Para aumentar sua contagem de referência, para que você não seja removido da memória prematuramente. Porque, embora sua classe ainda possa ser chamada após a destruição, ela não pode se o código foi descarregado.
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top