Správa paměti

Na rozdíl od programovacích jazyků jako C/C++ skrývá MicroPython detaily správy paměti před vývojářem tím, že podporuje automatickou správu paměti. Automatická správa paměti je technika používaná operačními systémy nebo aplikacemi k automatickému řízení alokace a dealokace paměti. To eliminuje problémy, jako je zapomenutí uvolnit paměť alokovanou pro objekt. Automatická správa paměti také předchází kritickému problému použití paměti, která již byla uvolněna. Automatická správa paměti má mnoho podob, jednou z nich je uvolňování paměti (garbage collection, GC).

Garbage collector má obvykle dvě odpovědnosti:

  1. Alokovat nové objekty v dostupné paměti.

  2. Uvolnit nepoužívanou paměť.

Existuje mnoho GC algoritmů, ale MicroPython používá ke správě paměti politiku Mark and Sweep. Tento algoritmus má fázi značení (mark), která prochází haldu a označuje všechny živé objekty, zatímco fáze úklidu (sweep) prochází haldu a uvolňuje všechny neoznačené objekty.

Funkcionalita uvolňování paměti je v MicroPythonu dostupná prostřednictvím vestavěného modulu gc:

>>> x = 5
>>> x
5
>>> import gc
>>> gc.enable()
>>> gc.mem_alloc()
1312
>>> gc.mem_free()
2071392
>>> gc.collect()
19
>>> gc.disable()
>>>

I když je zavoláno gc.disable(), lze uvolňování spustit pomocí gc.collect().

Paměť MicroPythonu z kódu v C

Při psaní kódu v C, který alokuje paměť z „Python haldy“ (tj. funkce m_malloc(), m_malloc0(), m_free() atd.), je nutné mít na paměti garbage collector.

Fáze značení garbage collectoru vyhledává živé ukazatele do paměti haldy počínaje následujícími kořeny:

  • Zásobník hlavního běhového prostředí Pythonu (nebo REPL).

  • Zásobníky jednotlivých „Python vláken“ u portů, které implementují vlákna Pythonu nad nativními vlákny nebo úlohami operačního systému.

  • „Kořenové ukazatele“ definované v kódu v C pomocí makra MP_REGISTER_ROOT_POINTER. To je doporučený způsob, jak mít staticky platné ukazatele do Python haldy.

  • Sledované alokace provedené pomocí funkcí m_tracked_calloc(), m_tracked_realloc a m_tracked_free(). Tyto speciální funkce umožňují alokovat blok paměti, který je garbage collectorem vždy považován za živý. Podobně jako u alokace paměti v C je tato paměť uvolněna pouze voláním m_tracked_free() nebo měkkým resetem. Každá sledovaná alokace má malou režii ve využití paměti i v době běhu. Tato funkce není ve výchozím nastavení povolena na všech portech.

Garbage collector poté rekurzivně prochází a označuje veškerou paměť, na kterou kořenové ukazatele odkazují, dokud nejsou vyčerpány všechny adresy. To stačí k nalezení všech objektů Pythonu, které jsou stále používány běhovým prostředím MicroPythonu.

Následující paměť však nebude garbage collectorem procházena a mohla by být předčasně uvolněna:

  • Statické nebo globální proměnné v C, které obsahují ukazatele do paměti haldy.

  • Ukazatele, které neukazují na „začátek“ alokovaného bufferu (tj. na přesnou adresu vrácenou funkcí m_malloc()), ale na adresu uvnitř alokovaného bufferu (například ukazatel na vnořenou strukturu). Z výkonnostních důvodů garbage collector v těchto případech neoznačuje obklopující buffer.

  • Zásobník jakéhokoli vlákna nebo RTOS úlohy, které neprovádí kód Pythonu ani není ručně zaregistrováno jako „Python vlákno“ (u portů, které podporují nativní vlákna nebo úlohy).

Způsoby, jak se v těchto scénářích vyhnout použití paměti po jejím uvolnění (use-after-free):

  • Použijte API pro sledované alokace m_tracked_calloc(), m_tracked_realloc() a m_tracked_free().

  • Zaregistrujte kořenový ukazatel (viz výše) namísto ukládání ukazatele do statické proměnné.

  • Přestrukturujte kód, například tak, že vytvoříte API, kde kód Pythonu inicializuje jedináčkový objekt Pythonu (implementovaný v C), který drží všechny relevantní ukazatele, místo aby byly ve statických proměnných.

Poznámka

Měkký reset vždy vyprázdní Python haldu a uvolní veškerou paměť. Je důležité nedržet po měkkém resetu žádné ukazatele do haldy, protože se z nich stanou visící ukazatele na uvolněnou paměť.

Některé porty podporují také „C haldu“ (viz Dynamická alokace paměti v C), v takovém případě můžete alokovat paměť, která zůstane platná i po měkkém resetu, voláním standardních funkcí C malloc atd.

Objektový model

Na všechny objekty MicroPythonu se odkazuje pomocí datového typu mp_obj_t. Ten má obvykle velikost slova (tj. stejnou velikost jako ukazatel na cílové architektuře) a může být typicky 32bitový (STM32, RP2, nRF, Unix x86) nebo 64bitový (Unix x64). Pro určité reprezentace objektů může být i větší než velikost slova, například OBJ_REPR_D má na 32bitové architektuře mp_obj_t o velikosti 64 bitů.

mp_obj_t představuje objekt MicroPythonu, například celé číslo, číslo s pohyblivou řádovou čárkou, typ, slovník nebo instanci třídy. Některé objekty, jako booleovské hodnoty a malá celá čísla, mají svou hodnotu uloženou přímo v hodnotě mp_obj_t a nevyžadují žádnou další paměť. Jiné objekty mají svou hodnotu uloženou jinde v paměti (například na haldě spravované garbage collectorem) a jejich mp_obj_t obsahuje ukazatel na tuto paměť. Část mp_obj_t tvoří značka (tag), která určuje, o jaký typ objektu se jedná.

Konkrétní podrobnosti o dostupných reprezentacích najdete v py/mpconfig.h.

Značkování ukazatelů

Protože ukazatele jsou zarovnány na velikost slova, budou při uložení do mp_obj_t spodní bity tohoto objektového handlu nulové. Například na 32bitové architektuře budou spodní 2 bity nulové:

********|********|********|******00

Tyto bity jsou vyhrazeny pro účely uložení značky. Značka uchovává dodatečné informace namísto zavádění nového pole pro uložení těchto informací do objektu, což by mohlo být neefektivní. V MicroPythonu značka říká, zda máme co do činění s malým celým číslem, internovaným (malým) řetězcem nebo konkrétním objektem, a na každý z nich se vztahuje jiná sémantika.

Pro malá celá čísla je mapování následující:

********|********|********|*******1

Kde hvězdičky drží skutečnou celočíselnou hodnotu. Pro internovaný řetězec nebo bezprostřední objekt (např. True) je rozložení hodnoty mp_obj_t po řadě následující:

********|********|********|*****010

********|********|********|*****110

Zatímco konkrétní objekt, který není žádným z výše uvedených, má tvar:

********|********|********|******00

Hvězdičky zde odpovídají adrese konkrétního objektu v paměti.

Alokace objektů

Hodnota malého celého čísla je uložena přímo v mp_obj_t a bude alokována na místě, nikoli na haldě nebo jinde. Vytvoření malých celých čísel tedy nemá vliv na haldu. Podobně je tomu u internovaných řetězců, které již mají svá textová data uložena jinde, a u bezprostředních hodnot jako None, False a True.

Vše ostatní, co je konkrétním objektem, je alokováno na haldě a jeho objektová struktura je taková, že v hlavičce objektu je vyhrazeno pole pro uložení typu objektu.

+++++++++++
+         +
+ type    + object header
+         +
+++++++++++
+         + object items
+         +
+         +
+++++++++++

Nejmenší jednotkou alokace na haldě je blok, který má velikost čtyř strojových slov (16 bajtů na 32bitovém stroji, 32 bajtů na 64bitovém stroji). Další struktura, rovněž alokovaná na haldě, sleduje alokaci objektů v každém bloku. Tato struktura se nazývá bitmapa.

../_images/bitmap.png

Bitmapa sleduje, zda je blok „volný“ nebo „používaný“, a k uchování tohoto stavu pro každý blok používá dva bity.

Garbage collector typu mark-sweep spravuje objekty alokované na haldě a využívá také bitmapu k označení objektů, které jsou stále používány. Úplnou implementaci těchto detailů najdete v py/gc.c.

Alokace: rozložení haldy

Halda je uspořádána tak, že se skládá z bloků v poolech. Blok může mít různé vlastnosti:

  • ATB(allocation table byte): Pokud je nastaven, pak je blok normálním blokem

  • FREE: Volný blok

  • HEAD: Hlava řetězce bloků

  • TAIL: V ocasu řetězce bloků

  • MARK : Označený hlavní blok

  • FTB(finaliser table byte): Pokud je nastaven, pak má blok finalizátor