Minneshantering¶
Till skillnad från programmeringsspråk som C/C++ döljer MicroPython detaljerna kring minneshantering för utvecklaren genom att tillhandahålla automatisk minneshantering. Automatisk minneshantering är en teknik som används av operativsystem eller applikationer för att automatiskt hantera allokering och frigöring av minne. Detta eliminerar problem som att glömma att frigöra det minne som allokerats till ett objekt. Automatisk minneshantering undviker även det kritiska problemet med att använda minne som redan har frigjorts. Automatisk minneshantering förekommer i många former, varav en är skräpsamling (garbage collection, GC).
Skräpsamlaren har vanligtvis två ansvarsområden;
Allokera nya objekt i tillgängligt minne.
Frigöra oanvänt minne.
Det finns många GC-algoritmer, men MicroPython använder principen Mark and Sweep för att hantera minne. Denna algoritm har en markeringsfas som går igenom heapen och markerar alla levande objekt, medan svepfasen går igenom heapen och återvinner alla omarkerade objekt.
Funktionalitet för skräpsamling i MicroPython är tillgänglig via den inbyggda modulen gc:
>>> x = 5
>>> x
5
>>> import gc
>>> gc.enable()
>>> gc.mem_alloc()
1312
>>> gc.mem_free()
2071392
>>> gc.collect()
19
>>> gc.disable()
>>>
Även när gc.disable() har anropats kan en samling utlösas med gc.collect().
MicroPython-minne från C-kod¶
Medvetenhet om skräpsamlaren krävs när man skriver C-kod som allokerar minne från ”Python-heapen” (dvs. funktionerna m_malloc(), m_malloc0(), m_free() osv.).
Skräpsamlarens markeringsfas söker efter levande pekare till heap-minne med utgångspunkt i följande rötter:
Stacken för det huvudsakliga Python-körtidssystemet (eller REPL).
Stackarna för varje ”Python-tråd”, för portar som implementerar Python-trådar ovanpå inbyggda operativsystemstrådar eller -uppgifter.
De ”rotpekare” som definieras i C-kod med hjälp av makrot
MP_REGISTER_ROOT_POINTER. Detta är det rekommenderade sättet att ha statiskt avgränsade pekare till Python-heapen.Spårade allokeringar gjorda med funktionerna
m_tracked_calloc(),m_tracked_reallocochm_tracked_free(). Dessa specialfunktioner gör det möjligt att allokera ett minnesblock som alltid betraktas som levande av skräpsamlaren. På samma sätt som minnesallokering i C frigörs detta minne endast genom att anropam_tracked_free()eller genom en mjuk omstart. Det medför en liten minnesanvändning och körtidskostnad för varje spårad allokering. Denna funktion är inte aktiverad som standard på alla portar.
Skräpsamlaren skannar och markerar sedan rekursivt allt minne som rotpekarna pekar på, tills alla adresser är uttömda. Detta är tillräckligt för att hitta alla Python-objekt som fortfarande används av MicroPython-körtidssystemet.
Följande minne kommer dock inte att skannas av skräpsamlaren och skulle kunna frigöras i förtid:
Statiska eller globala C-variabler som innehåller pekare till heap-minne.
Pekare som inte pekar på ”huvudet” av en allokerad buffert (dvs. på den exakta adress som returneras av
m_malloc()), utan i stället på en adress inuti den allokerade bufferten (till exempel en pekare till en nästlad struct). Av prestandaskäl markerar skräpsamlaren inte den omslutande bufferten i dessa fall.Stacken för någon tråd eller RTOS-uppgift som inte kör Python-kod eller som inte manuellt registrerats som en ”Python-tråd” (för portar som stöder inbyggda trådar eller uppgifter).
Sätt att undvika use-after-free i dessa scenarier:
Använd API:et för spårad allokering
m_tracked_calloc(),m_tracked_realloc()ochm_tracked_free().Registrera en rotpekare (se ovan) i stället för att lagra en pekare i en statisk variabel.
Strukturera om koden, till exempel genom att ha ett API där Python-kod initierar ett singleton-Python-objekt (implementerat i C) som håller alla relevanta pekare, i stället för att ha dem i statiska variabler.
Anteckning
Mjuk återställning rensar alltid Python-heapen och frigör allt minne. Det är viktigt att inte hålla kvar några pekare till heapen efter en mjuk omstart, eftersom de då blir dinglande pekare till frigjort minne.
Vissa portar stöder även en ”C-heap” (se Dynamisk minnesallokering i C), och i så fall kan du allokera minne som förblir giltigt över en mjuk omstart genom att anropa standardfunktionerna i C malloc osv.
Objektmodellen¶
Alla MicroPython-objekt refereras till via datatypen mp_obj_t. Denna är vanligtvis ordstor (dvs. samma storlek som en pekare på målarkitekturen) och kan typiskt vara 32-bitars (STM32, RP2, nRF, Unix x86) eller 64-bitars (Unix x64). Den kan även vara större än en ordstorlek för vissa objektrepresentationer; till exempel har OBJ_REPR_D en 64-bitars mp_obj_t på en 32-bitars arkitektur.
En mp_obj_t representerar ett MicroPython-objekt, till exempel ett heltal, ett flyttal, en typ, en dict eller en klassinstans. Vissa objekt, som booleska värden och små heltal, har sitt värde lagrat direkt i mp_obj_t-värdet och kräver inget ytterligare minne. Andra objekt har sitt värde lagrat någon annanstans i minnet (till exempel på den skräpsamlade heapen), och deras mp_obj_t innehåller en pekare till det minnet. En del av mp_obj_t är taggen, som anger vilken typ av objekt det är.
Se py/mpconfig.h för de specifika detaljerna kring de tillgängliga representationerna.
Pekartaggning
Eftersom pekare är ordjusterade kommer de lägre bitarna i detta objekthandtag att vara noll när de lagras i en mp_obj_t. På till exempel en 32-bitars arkitektur kommer de lägre 2 bitarna att vara noll:
********|********|********|******00
Dessa bitar är reserverade för att lagra en tagg. Taggen lagrar extra information i stället för att införa ett nytt fält för att lagra den informationen i objektet, vilket kan vara ineffektivt. I MicroPython anger taggen om vi har att göra med ett litet heltal, en internerad (liten) sträng eller ett konkret objekt, och olika semantik gäller för var och en av dessa.
För små heltal är mappningen denna:
********|********|********|*******1
Där asteriskerna innehåller det faktiska heltalsvärdet. För en internerad sträng eller ett omedelbart objekt (t.ex. True) är layouten för mp_obj_t-värdet respektive:
********|********|********|*****010
********|********|********|*****110
Medan ett konkret objekt som inte är något av ovanstående har formen:
********|********|********|******00
Stjärnorna här motsvarar adressen till det konkreta objektet i minnet.
Allokering av objekt¶
Värdet för ett litet heltal lagras direkt i mp_obj_t och allokeras på plats, inte på heapen eller någon annanstans. Skapandet av små heltal påverkar därmed inte heapen. På samma sätt gäller för internerade strängar som redan har sina textdata lagrade någon annanstans, och för omedelbara värden som None, False och True.
Allt annat som är ett konkret objekt allokeras på heapen, och dess objektstruktur är sådan att ett fält reserveras i objekthuvudet för att lagra objektets typ.
+++++++++++
+ +
+ type + object header
+ +
+++++++++++
+ + object items
+ +
+ +
+++++++++++
Heapens minsta allokeringsenhet är ett block, som är fyra maskinord stort (16 byte på en 32-bitars maskin, 32 byte på en 64-bitars maskin). En annan struktur som också allokeras på heapen håller reda på allokeringen av objekt i varje block. Denna struktur kallas en bitmap.
Bitmappen håller reda på om ett block är ”ledigt” eller ”i bruk” och använder två bitar för att spåra detta tillstånd för varje block.
Mark-sweep-skräpsamlaren hanterar de objekt som allokerats på heapen och använder även bitmappen för att markera objekt som fortfarande används. Se py/gc.c för den fullständiga implementeringen av dessa detaljer.
Allokering: heap-layout
Heapen är arrangerad så att den består av block i pooler. Ett block kan ha olika egenskaper:
ATB(allocation table byte): Om satt är blocket ett normalt block
FREE: Ledigt block
HEAD: Huvud i en kedja av block
TAIL: I svansen av en kedja av block
MARK : Markerat huvudblock
FTB(finaliser table byte): Om satt har blocket en finaliserare