Geheugenbeheer¶
In tegenstelling tot programmeertalen zoals C/C++ verbergt MicroPython de details van geheugenbeheer voor de ontwikkelaar door automatisch geheugenbeheer te ondersteunen. Automatisch geheugenbeheer is een techniek die door besturingssystemen of applicaties wordt gebruikt om de toewijzing en vrijgave van geheugen automatisch te beheren. Hierdoor verdwijnen uitdagingen zoals het vergeten om het aan een object toegewezen geheugen vrij te geven. Automatisch geheugenbeheer voorkomt ook het kritieke probleem van het gebruiken van geheugen dat al is vrijgegeven. Automatisch geheugenbeheer kent vele vormen, waarvan er een garbage collection (GC) is.
De garbage collector heeft doorgaans twee verantwoordelijkheden;
Nieuwe objecten toewijzen in beschikbaar geheugen.
Ongebruikt geheugen vrijgeven.
Er bestaan veel GC-algoritmen, maar MicroPython gebruikt het Mark and Sweep-beleid voor het beheren van geheugen. Dit algoritme heeft een markeerfase die de heap doorloopt en alle levende objecten markeert, terwijl de veegfase de heap doorloopt en alle ongemarkeerde objecten terugvordert.
De functionaliteit voor garbage collection in MicroPython is beschikbaar via de ingebouwde module gc:
>>> x = 5
>>> x
5
>>> import gc
>>> gc.enable()
>>> gc.mem_alloc()
1312
>>> gc.mem_free()
2071392
>>> gc.collect()
19
>>> gc.disable()
>>>
Zelfs wanneer gc.disable() is aangeroepen, kan collection worden geactiveerd met gc.collect().
MicroPython-geheugen vanuit C-code¶
Bewustzijn van de garbage collector is nodig bij het schrijven van C-code die geheugen toewijst vanaf de “Python-heap” (d.w.z. de functies m_malloc(), m_malloc0(), m_free(), enz.).
De markeerfase van de garbage collector zoekt naar levende pointers naar heap-geheugen, te beginnen bij de volgende wortels:
De stack van de hoofd-Python-runtime (of REPL).
De stacks van elke “Python-thread”, voor ports die Python-threads bovenop native threads of taken van het besturingssysteem implementeren.
De “wortelpointers” die in C-code zijn gedefinieerd met de macro
MP_REGISTER_ROOT_POINTER. Dit is de aanbevolen manier om statisch gescopete pointers naar de Python-heap te hebben.Getraceerde toewijzingen die zijn gemaakt met de functies
m_tracked_calloc(),m_tracked_reallocenm_tracked_free(). Met deze speciale functies kan een geheugenblok worden toegewezen dat door de garbage collector altijd als levend wordt beschouwd. Net als bij geheugentoewijzing in C wordt dit geheugen alleen vrijgegeven doorm_tracked_free()aan te roepen of door een soft reset. Elke getraceerde toewijzing brengt een klein gebruik van geheugen en runtime-overhead met zich mee. Deze functie is niet op alle ports standaard ingeschakeld.
De garbage collector scant en markeert vervolgens recursief al het geheugen waarnaar de wortelpointers verwijzen, totdat alle adressen zijn afgewerkt. Dit volstaat om alle Python-objecten te vinden die nog in gebruik zijn door de MicroPython-runtime.
Het volgende geheugen wordt echter niet gescand door de garbage collector en kan voortijdig worden vrijgegeven:
Statische of globale C-variabelen die pointers naar heap-geheugen bevatten.
Pointers die niet naar de “kop” van een toegewezen buffer wijzen (d.w.z. naar het exacte adres dat door
m_malloc()wordt geretourneerd), maar in plaats daarvan naar een adres binnen de toegewezen buffer (bijvoorbeeld een pointer naar een geneste struct). Om prestatieredenen markeert de garbage collector in deze gevallen niet de omsluitende buffer.De stack van elke thread of RTOS-taak die geen Python-code uitvoert of handmatig is geregistreerd als een “Python-thread” (voor ports die native threads of taken ondersteunen).
Manieren om use-after-free in deze scenario’s te vermijden:
Gebruik de API voor getraceerde toewijzing
m_tracked_calloc(),m_tracked_realloc()enm_tracked_free().Registreer een wortelpointer (zie hierboven), in plaats van een pointer in een statische variabele op te slaan.
Herstructureer de code, bijvoorbeeld door een API te hebben waarbij Python-code een singleton Python-object (geïmplementeerd in C) initialiseert dat alle relevante pointers bevat, in plaats van ze in statische variabelen te hebben.
Notitie
Zachte reset wist altijd de Python-heap en geeft al het geheugen vrij. Het is belangrijk om na een soft reset geen pointers naar de heap vast te houden, aangezien deze loshangende pointers naar vrijgegeven geheugen worden.
Sommige ports ondersteunen ook een “C-heap” (zie Dynamische geheugenallocatie in C), in welk geval u geheugen kunt toewijzen dat geldig blijft over een soft reset door standaard C-functies zoals malloc aan te roepen, enz.
Het objectmodel¶
Naar alle MicroPython-objecten wordt verwezen via het gegevenstype mp_obj_t. Dit heeft doorgaans de grootte van een woord (d.w.z. dezelfde grootte als een pointer op de doelarchitectuur) en kan typisch 32-bit (STM32, RP2, nRF, Unix x86) of 64-bit (Unix x64) zijn. Het kan voor bepaalde objectrepresentaties ook groter zijn dan de woordgrootte; OBJ_REPR_D heeft bijvoorbeeld een 64-bit mp_obj_t op een 32-bit-architectuur.
Een mp_obj_t vertegenwoordigt een MicroPython-object, bijvoorbeeld een integer, float, type, dict of class-instantie. Sommige objecten, zoals booleans en kleine integers, hebben hun waarde rechtstreeks opgeslagen in de mp_obj_t-waarde en vereisen geen extra geheugen. Andere objecten hebben hun waarde elders in het geheugen opgeslagen (bijvoorbeeld op de garbage-collected heap) en hun mp_obj_t bevat een pointer naar dat geheugen. Een deel van mp_obj_t is de tag die aangeeft welk type object het is.
Zie py/mpconfig.h voor de specifieke details van de beschikbare representaties.
Pointer-tagging
Omdat pointers woord-uitgelijnd zijn, zullen de lagere bits van deze object-handle nul zijn wanneer ze worden opgeslagen in een mp_obj_t. Op een 32-bit-architectuur zullen de lagere 2 bits bijvoorbeeld nul zijn:
********|********|********|******00
Deze bits zijn gereserveerd voor het opslaan van een tag. De tag slaat extra informatie op in plaats van een nieuw veld in te voeren om die informatie in het object op te slaan, wat inefficiënt kan zijn. In MicroPython geeft de tag aan of we te maken hebben met een kleine integer, een interned (kleine) string of een concreet object, en op elk daarvan zijn andere semantiek van toepassing.
Voor kleine integers is de mapping als volgt:
********|********|********|*******1
Waarbij de sterretjes de werkelijke integerwaarde bevatten. Voor een interned string of een immediate object (bijv. True) is de indeling van de mp_obj_t-waarde respectievelijk:
********|********|********|*****010
********|********|********|*****110
Terwijl een concreet object dat geen van bovenstaande is de volgende vorm aanneemt:
********|********|********|******00
De sterren komen hier overeen met het adres van het concrete object in het geheugen.
Toewijzing van objecten¶
De waarde van een kleine integer wordt rechtstreeks opgeslagen in de mp_obj_t en wordt ter plekke toegewezen, niet op de heap of elders. Daardoor heeft het aanmaken van kleine integers geen invloed op de heap. Hetzelfde geldt voor interned strings die hun tekstuele gegevens al elders hebben opgeslagen, en voor immediate waarden zoals None, False en True.
Al het andere, dat een concreet object is, wordt op de heap toegewezen en de objectstructuur is zodanig dat in de object-header een veld is gereserveerd om het type van het object op te slaan.
+++++++++++
+ +
+ type + object header
+ +
+++++++++++
+ + object items
+ +
+ +
+++++++++++
De kleinste toewijzingseenheid van de heap is een block, dat vier machinewoorden groot is (16 bytes op een 32-bit-machine, 32 bytes op een 64-bit-machine). Een andere structuur die ook op de heap wordt toegewezen, houdt de toewijzing van objecten in elk block bij. Deze structuur wordt een bitmap genoemd.
De bitmap houdt bij of een block “vrij” of “in gebruik” is en gebruikt twee bits om deze status voor elk block bij te houden.
De mark-sweep garbage collector beheert de objecten die op de heap zijn toegewezen, en gebruikt ook de bitmap om objecten te markeren die nog in gebruik zijn. Zie py/gc.c voor de volledige implementatie van deze details.
Toewijzing: heap-indeling
De heap is zo ingericht dat deze bestaat uit blocks in pools. Een block kan verschillende eigenschappen hebben:
ATB(allocation table byte): Indien ingesteld, dan is het block een normaal block
FREE: Vrij block
HEAD: Kop van een keten van blocks
TAIL: In de staart van een keten van blocks
MARK : Gemarkeerd kop-block
FTB(finaliser table byte): Indien ingesteld, dan heeft het block een finaliser