Gestione della memoria¶
A differenza di linguaggi di programmazione come C/C++, MicroPython nasconde allo sviluppatore i dettagli della gestione della memoria supportando la gestione automatica della memoria. La gestione automatica della memoria è una tecnica utilizzata dai sistemi operativi o dalle applicazioni per gestire automaticamente l’allocazione e la deallocazione della memoria. Questo elimina problemi come dimenticarsi di liberare la memoria allocata a un oggetto. La gestione automatica della memoria evita inoltre il problema critico dell’utilizzo di memoria già rilasciata. La gestione automatica della memoria assume molte forme, una delle quali è la garbage collection (GC).
Il garbage collector ha solitamente due responsabilità;
Allocare nuovi oggetti nella memoria disponibile.
Liberare la memoria inutilizzata.
Esistono molti algoritmi di GC, ma MicroPython utilizza la politica Mark and Sweep per gestire la memoria. Questo algoritmo prevede una fase di marcatura (mark) che percorre lo heap marcando tutti gli oggetti vivi, mentre la fase di pulizia (sweep) attraversa lo heap recuperando tutti gli oggetti non marcati.
La funzionalità di garbage collection in MicroPython è disponibile tramite il modulo integrato gc:
>>> x = 5
>>> x
5
>>> import gc
>>> gc.enable()
>>> gc.mem_alloc()
1312
>>> gc.mem_free()
2071392
>>> gc.collect()
19
>>> gc.disable()
>>>
Anche quando viene invocato gc.disable(), la collection può essere attivata con gc.collect().
La memoria di MicroPython dal codice C¶
È necessario tenere conto del garbage collector quando si scrive codice C che alloca memoria dallo «heap Python» (ovvero le funzioni m_malloc(), m_malloc0(), m_free(), ecc.).
La fase di marcatura del garbage collector cerca i puntatori vivi alla memoria dello heap partendo dalle seguenti radici:
Lo stack del runtime Python principale (o REPL).
Gli stack di ciascun «thread Python», per le porte che implementano i thread Python sopra i thread o le task native del sistema operativo.
I «puntatori radice» definiti nel codice C utilizzando la macro
MP_REGISTER_ROOT_POINTER. Questo è il modo consigliato per avere puntatori a scope statico verso lo heap Python.Le allocazioni tracciate effettuate con le funzioni
m_tracked_calloc(),m_tracked_reallocem_tracked_free(). Queste funzioni speciali consentono di allocare un blocco di memoria che è sempre considerato vivo dal garbage collector. Analogamente all’allocazione di memoria in C, questa memoria viene liberata solo chiamandom_tracked_free()o tramite soft reset. Ogni allocazione tracciata comporta un piccolo sovraccarico in termini di utilizzo della memoria e di runtime. Questa funzionalità non è abilitata per impostazione predefinita su tutte le porte.
Il garbage collector quindi scansiona e marca ricorsivamente tutta la memoria puntata dai puntatori radice, finché tutti gli indirizzi non sono esauriti. Questo è sufficiente per trovare tutti gli oggetti Python ancora in uso dal runtime di MicroPython.
Tuttavia, la memoria seguente non verrà scansionata dal garbage collector e potrebbe essere liberata prematuramente:
Variabili C statiche o globali che contengono puntatori alla memoria dello heap.
Puntatori che non puntano alla «testa» di un buffer allocato (ovvero all’esatto indirizzo restituito da
m_malloc()), ma invece a un indirizzo all’interno del buffer allocato (ad esempio, un puntatore a una struct annidata). Per ragioni di prestazioni, in questi casi il garbage collector non marca il buffer che lo contiene.Lo stack di qualsiasi thread o task RTOS che non sta eseguendo codice Python o che non è stato registrato manualmente come «thread Python» (per le porte che supportano thread o task native).
Modi per evitare use-after-free in questi scenari:
Utilizzare l’API di allocazione tracciata
m_tracked_calloc(),m_tracked_realloc()em_tracked_free().Registrare un puntatore radice (vedi sopra), invece di memorizzare un puntatore in una variabile statica.
Ristrutturare il codice, ad esempio adottando un’API in cui il codice Python inizializza un oggetto Python singleton (implementato in C) che contiene tutti i puntatori rilevanti, anziché averli in variabili statiche.
Nota
Soft reset cancella sempre lo heap Python e libera tutta la memoria. È importante non mantenere alcun puntatore allo heap dopo un soft reset, poiché diventerebbero puntatori pendenti verso memoria liberata.
Alcune porte supportano anche uno «heap C» (vedi Allocazione dinamica della memoria C), nel qual caso è possibile allocare memoria che rimarrà valida attraverso il soft reset chiamando le funzioni C standard malloc, ecc.
Il modello a oggetti¶
Tutti gli oggetti MicroPython sono riferiti tramite il tipo di dato mp_obj_t. Questo ha solitamente la dimensione di una parola (ovvero la stessa dimensione di un puntatore sull’architettura di destinazione) e può essere tipicamente a 32 bit (STM32, RP2, nRF, Unix x86) o a 64 bit (Unix x64). Può anche essere più grande di una parola per determinate rappresentazioni degli oggetti; ad esempio OBJ_REPR_D ha un mp_obj_t di dimensione 64 bit su un’architettura a 32 bit.
Un mp_obj_t rappresenta un oggetto MicroPython, ad esempio un intero, un float, un tipo, un dict o un’istanza di classe. Alcuni oggetti, come i booleani e i piccoli interi, hanno il loro valore memorizzato direttamente nel valore mp_obj_t e non richiedono memoria aggiuntiva. Altri oggetti hanno il loro valore memorizzato altrove in memoria (ad esempio nello heap gestito dal garbage collector) e il loro mp_obj_t contiene un puntatore a quella memoria. Una porzione di mp_obj_t è il tag che indica di che tipo di oggetto si tratta.
Consulta py/mpconfig.h per i dettagli specifici sulle rappresentazioni disponibili.
Tagging dei puntatori
Poiché i puntatori sono allineati alla parola, quando vengono memorizzati in un mp_obj_t i bit meno significativi di questo handle dell’oggetto saranno zero. Ad esempio, su un’architettura a 32 bit i 2 bit meno significativi saranno zero:
********|********|********|******00
Questi bit sono riservati allo scopo di memorizzare un tag. Il tag memorizza informazioni aggiuntive anziché introdurre un nuovo campo per memorizzare tale informazione nell’oggetto, cosa che potrebbe essere inefficiente. In MicroPython il tag indica se abbiamo a che fare con un piccolo intero, una stringa internata (piccola) o un oggetto concreto, e a ciascuno di essi si applicano semantiche differenti.
Per i piccoli interi la mappatura è la seguente:
********|********|********|*******1
Dove gli asterischi contengono il valore intero effettivo. Per una stringa internata o un oggetto immediato (ad es. True) la disposizione del valore mp_obj_t è, rispettivamente:
********|********|********|*****010
********|********|********|*****110
Mentre un oggetto concreto che non rientra in nessuno dei casi precedenti assume la forma:
********|********|********|******00
Le stelle qui corrispondono all’indirizzo dell’oggetto concreto in memoria.
Allocazione degli oggetti¶
Il valore di un piccolo intero è memorizzato direttamente nel mp_obj_t e verrà allocato in-place, non sullo heap o altrove. Pertanto, la creazione di piccoli interi non influisce sullo heap. Lo stesso vale per le stringhe internate che hanno già i loro dati testuali memorizzati altrove e per i valori immediati come None, False e True.
Tutto il resto, che è un oggetto concreto, viene allocato sullo heap e la sua struttura è tale che un campo è riservato nell’intestazione dell’oggetto per memorizzare il tipo dell’oggetto.
+++++++++++
+ +
+ type + object header
+ +
+++++++++++
+ + object items
+ +
+ +
+++++++++++
L’unità di allocazione più piccola dello heap è un blocco, che ha la dimensione di quattro parole macchina (16 byte su una macchina a 32 bit, 32 byte su una macchina a 64 bit). Un’altra struttura, anch’essa allocata sullo heap, traccia l’allocazione degli oggetti in ciascun blocco. Questa struttura è chiamata bitmap.
La bitmap traccia se un blocco è «libero» o «in uso» e utilizza due bit per tracciare questo stato per ciascun blocco.
Il garbage collector mark-sweep gestisce gli oggetti allocati sullo heap e utilizza inoltre la bitmap per marcare gli oggetti ancora in uso. Consulta py/gc.c per l’implementazione completa di questi dettagli.
Allocazione: disposizione dello heap
Lo heap è organizzato in modo da essere costituito da blocchi raggruppati in pool. Un blocco può avere diverse proprietà:
ATB(allocation table byte): Se impostato, allora il blocco è un blocco normale
FREE: Blocco libero
HEAD: Testa di una catena di blocchi
TAIL: Nella coda di una catena di blocchi
MARK : Blocco di testa marcato
FTB(finaliser table byte): Se impostato, allora il blocco ha un finalizzatore