Gerenciamento de Memória¶
Diferentemente de linguagens de programação como C/C++, o MicroPython oculta os detalhes do gerenciamento de memória do desenvolvedor ao oferecer suporte ao gerenciamento automático de memória. O gerenciamento automático de memória é uma técnica usada por sistemas operacionais ou aplicações para gerenciar automaticamente a alocação e a desalocação de memória. Isso elimina desafios como esquecer de liberar a memória alocada a um objeto. O gerenciamento automático de memória também evita o problema crítico de usar memória que já foi liberada. O gerenciamento automático de memória assume muitas formas, sendo uma delas a coleta de lixo (GC, garbage collection).
O coletor de lixo geralmente tem duas responsabilidades;
Alocar novos objetos na memória disponível.
Liberar memória não utilizada.
Existem muitos algoritmos de GC, mas o MicroPython usa a política Mark and Sweep para gerenciar a memória. Esse algoritmo tem uma fase de marcação que percorre o heap marcando todos os objetos vivos, enquanto a fase de varredura percorre o heap recuperando todos os objetos não marcados.
A funcionalidade de coleta de lixo no MicroPython está disponível por meio do módulo embutido gc:
>>> x = 5
>>> x
5
>>> import gc
>>> gc.enable()
>>> gc.mem_alloc()
1312
>>> gc.mem_free()
2071392
>>> gc.collect()
19
>>> gc.disable()
>>>
Mesmo quando gc.disable() é invocado, a coleta pode ser disparada com gc.collect().
Memória do MicroPython a partir de código C¶
É necessário ter consciência do coletor de lixo ao escrever código C que aloca memória do “heap do Python” (ou seja, funções m_malloc(), m_malloc0(), m_free(), etc).
A fase de marcação do coletor de lixo procura por ponteiros vivos para a memória do heap a partir das seguintes raízes:
A pilha do runtime principal do Python (ou do REPL).
As pilhas de cada “thread do Python”, para portas que implementam threads do Python sobre threads ou tarefas nativas do sistema operacional.
Os “ponteiros raiz” definidos em código C usando a macro
MP_REGISTER_ROOT_POINTER. Esta é a maneira recomendada de ter ponteiros com escopo estático para o heap do Python.Alocações rastreadas feitas com as funções
m_tracked_calloc(),m_tracked_reallocem_tracked_free(). Essas funções especiais permitem alocar um bloco de memória que é sempre considerado vivo pelo coletor de lixo. Semelhante à alocação de memória em C, essa memória só é liberada ao chamarm_tracked_free()ou por meio de soft reset. Há um pequeno custo de uso de memória e de tempo de execução para cada alocação rastreada. Esse recurso não é habilitado por padrão em todas as portas.
O coletor de lixo então varre e marca recursivamente toda a memória apontada pelos ponteiros raiz, até que todos os endereços sejam esgotados. Isso é suficiente para encontrar todos os objetos Python que ainda estão em uso pelo runtime do MicroPython.
No entanto, a seguinte memória não será varrida pelo coletor de lixo e poderia ser liberada prematuramente:
Variáveis C estáticas ou globais que contêm ponteiros para a memória do heap.
Ponteiros que não apontam para a “cabeça” de um buffer alocado (ou seja, para o endereço exato retornado por
m_malloc()), mas em vez disso para um endereço dentro do buffer alocado (por exemplo, um ponteiro para uma struct aninhada). Por motivos de desempenho, o coletor de lixo não marca o buffer envolvente nesses casos.A pilha de qualquer thread ou tarefa do RTOS que não esteja executando código Python ou registrada manualmente como uma “thread do Python” (para portas que oferecem suporte a threads ou tarefas nativas).
Maneiras de evitar use-after-free nesses cenários:
Usar a API de alocação rastreada
m_tracked_calloc(),m_tracked_realloc()em_tracked_free().Registrar um ponteiro raiz (veja acima), em vez de armazenar um ponteiro em uma variável estática.
Reestruturar o código, por exemplo, tendo uma API onde o código Python inicializa um objeto Python singleton (implementado em C) que contém todos os ponteiros relevantes, em vez de mantê-los em variáveis estáticas.
Nota
Soft Reset sempre limpa o heap do Python e libera toda a memória. É importante não manter nenhum ponteiro para o heap após um soft reset, pois eles se tornarão ponteiros pendentes (dangling) para memória liberada.
Algumas portas também oferecem suporte a um “heap de C” (veja Alocação dinâmica de memória em C), caso em que você pode alocar memória que permanecerá válida ao longo de um soft reset chamando funções C padrão como malloc, etc.
O modelo de objetos¶
Todos os objetos do MicroPython são referenciados pelo tipo de dado mp_obj_t. Esse tipo geralmente tem o tamanho de uma palavra (word), ou seja, o mesmo tamanho de um ponteiro na arquitetura de destino, e normalmente pode ser de 32 bits (STM32, RP2, nRF, Unix x86) ou de 64 bits (Unix x64). Ele também pode ser maior que o tamanho de uma palavra para certas representações de objetos; por exemplo, OBJ_REPR_D tem um mp_obj_t de 64 bits em uma arquitetura de 32 bits.
Um mp_obj_t representa um objeto do MicroPython, por exemplo um inteiro, float, tipo, dict ou instância de classe. Alguns objetos, como booleanos e inteiros pequenos, têm seu valor armazenado diretamente no valor de mp_obj_t e não exigem memória adicional. Outros objetos têm seu valor armazenado em outro lugar na memória (por exemplo, no heap gerenciado pela coleta de lixo) e seu mp_obj_t contém um ponteiro para essa memória. Uma parte do mp_obj_t é a tag, que indica qual é o tipo do objeto.
Veja py/mpconfig.h para os detalhes específicos das representações disponíveis.
Tagging de ponteiros
Como os ponteiros estão alinhados a palavras (word-aligned), quando são armazenados em um mp_obj_t os bits inferiores desse identificador de objeto serão zero. Por exemplo, em uma arquitetura de 32 bits os 2 bits inferiores serão zero:
********|********|********|******00
Esses bits são reservados para o propósito de armazenar uma tag. A tag armazena informações extras, em vez de introduzir um novo campo para armazenar essas informações no objeto, o que poderia ser ineficiente. No MicroPython a tag indica se estamos lidando com um inteiro pequeno, uma string internada (pequena) ou um objeto concreto, e semânticas diferentes se aplicam a cada um deles.
Para inteiros pequenos o mapeamento é o seguinte:
********|********|********|*******1
Onde os asteriscos contêm o valor inteiro propriamente dito. Para uma string internada ou um objeto imediato (por exemplo, True) o layout do valor de mp_obj_t é, respectivamente:
********|********|********|*****010
********|********|********|*****110
Já um objeto concreto que não é nenhum dos anteriores assume a forma:
********|********|********|******00
Os asteriscos aqui correspondem ao endereço do objeto concreto na memória.
Alocação de objetos¶
O valor de um inteiro pequeno é armazenado diretamente no mp_obj_t e será alocado no próprio local (in-place), não no heap nem em outro lugar. Dessa forma, a criação de inteiros pequenos não afeta o heap. O mesmo vale para strings internadas que já têm seus dados textuais armazenados em outro lugar, e para valores imediatos como None, False e True.
Todo o resto que é um objeto concreto é alocado no heap, e sua estrutura de objeto é tal que um campo é reservado no cabeçalho do objeto para armazenar o tipo do objeto.
+++++++++++
+ +
+ type + object header
+ +
+++++++++++
+ + object items
+ +
+ +
+++++++++++
A menor unidade de alocação do heap é um bloco, que tem o tamanho de quatro palavras de máquina (16 bytes em uma máquina de 32 bits, 32 bytes em uma máquina de 64 bits). Outra estrutura também alocada no heap rastreia a alocação de objetos em cada bloco. Essa estrutura é chamada de bitmap.
O bitmap rastreia se um bloco está “livre” ou “em uso” e usa dois bits para rastrear esse estado para cada bloco.
O coletor de lixo mark-sweep gerencia os objetos alocados no heap e também utiliza o bitmap para marcar objetos que ainda estão em uso. Veja py/gc.c para a implementação completa desses detalhes.
Alocação: layout do heap
O heap é organizado de modo que consiste em blocos dentro de pools. Um bloco pode ter diferentes propriedades:
ATB(allocation table byte): Se definido, então o bloco é um bloco normal
FREE: Bloco livre
HEAD: Cabeça de uma cadeia de blocos
TAIL: Na cauda de uma cadeia de blocos
MARK : Bloco de cabeça marcado
FTB(finaliser table byte): Se definido, então o bloco tem um finalizador