Upravljanje memorijom

Za razliku od programskih jezika poput C/C++, MicroPython skriva detalje upravljanja memorijom od programera podržavajući automatsko upravljanje memorijom. Automatsko upravljanje memorijom je tehnika koju operacijski sustavi ili aplikacije koriste za automatsko upravljanje dodjelom i oslobađanjem memorije. To uklanja izazove poput zaboravljanja oslobađanja memorije dodijeljene nekom objektu. Automatsko upravljanje memorijom također izbjegava ključni problem korištenja memorije koja je već oslobođena. Automatsko upravljanje memorijom poprima mnoge oblike, a jedan od njih je sakupljanje smeća (garbage collection, GC).

Sakupljač smeća obično ima dvije odgovornosti;

  1. Dodjeljivanje novih objekata u dostupnoj memoriji.

  2. Oslobađanje nekorištene memorije.

Postoji mnogo GC algoritama, ali MicroPython za upravljanje memorijom koristi politiku Mark and Sweep. Ovaj algoritam ima fazu označavanja koja prolazi kroz gomilu (heap) označavajući sve žive objekte, dok faza čišćenja prolazi kroz gomilu vraćajući sve neoznačene objekte.

Funkcionalnost sakupljanja smeća u MicroPythonu dostupna je putem ugrađenog modula gc:

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

Čak i kad je pozvan gc.disable(), sakupljanje se može pokrenuti pomoću gc.collect().

MicroPython memorija iz C koda

Svijest o sakupljaču smeća potrebna je pri pisanju C koda koji dodjeljuje memoriju iz „Python gomile” (tj. funkcije m_malloc(), m_malloc0(), m_free() itd.).

Faza označavanja sakupljača smeća pretražuje žive pokazivače na memoriju gomile počevši od sljedećih korijena:

  • Stog glavnog Python runtime-a (ili REPL-a).

  • Stogovi svake „Python dretve”, za portove koji implementiraju Python dretve na vrhu izvornih dretvi ili zadataka operacijskog sustava.

  • „Korijenski pokazivači” definirani u C kodu pomoću makronaredbe MP_REGISTER_ROOT_POINTER. To je preporučeni način za posjedovanje statički obuhvaćenih pokazivača na Python gomilu.

  • Praćene dodjele napravljene funkcijama m_tracked_calloc(), m_tracked_realloc i m_tracked_free(). Te posebne funkcije omogućuju dodjelu bloka memorije koji sakupljač smeća uvijek smatra živim. Slično dodjeli memorije u C-u, ta se memorija oslobađa samo pozivom m_tracked_free() ili mekim resetiranjem. Svaka praćena dodjela ima mali režijski trošak u korištenju memorije i izvođenju. Ova značajka nije omogućena prema zadanim postavkama na svim portovima.

Sakupljač smeća zatim rekurzivno pretražuje i označava svu memoriju na koju pokazuju korijenski pokazivači, sve dok se ne iscrpe sve adrese. To je dovoljno za pronalaženje svih Python objekata koje MicroPython runtime još uvijek koristi.

Međutim, sljedeću memoriju sakupljač smeća neće pretraživati i mogla bi biti prijevremeno oslobođena:

  • Statičke ili globalne C varijable koje sadrže pokazivače na memoriju gomile.

  • Pokazivači koji ne pokazuju na „početak” dodijeljenog međuspremnika (tj. na točnu adresu koju vraća m_malloc()), nego na adresu unutar dodijeljenog međuspremnika (na primjer, pokazivač na ugniježđenu strukturu). Iz razloga performansi, sakupljač smeća u tim slučajevima ne označava obuhvaćajući međuspremnik.

  • Stog bilo koje dretve ili RTOS zadatka koji ne izvršava Python kod niti je ručno registriran kao „Python dretva” (za portove koji podržavaju izvorne dretve ili zadatke).

Načini izbjegavanja korištenja nakon oslobađanja (use-after-free) u ovim scenarijima:

  • Koristite API za praćenu dodjelu m_tracked_calloc(), m_tracked_realloc() i m_tracked_free().

  • Registrirajte korijenski pokazivač (vidi gore) umjesto pohranjivanja pokazivača u statičkoj varijabli.

  • Restrukturirajte kod, na primjer tako da imate API u kojem Python kod inicijalizira singleton Python objekt (implementiran u C-u) koji drži sve relevantne pokazivače umjesto da ih drži u statičkim varijablama.

Napomena

Meko resetiranje uvijek čisti Python gomilu i oslobađa svu memoriju. Važno je ne držati nikakve pokazivače na gomilu nakon mekog resetiranja, jer će postati viseći pokazivači na oslobođenu memoriju.

Neki portovi podržavaju i „C gomilu” (vidi Dinamičko zauzimanje memorije u jeziku C), u kojem slučaju možete dodijeliti memoriju koja će ostati valjana tijekom mekog resetiranja pozivom standardnih C funkcija malloc itd.

Model objekta

Na sve MicroPython objekte upućuje se podatkovnim tipom mp_obj_t. To je obično veličine riječi (tj. iste veličine kao pokazivač na ciljnoj arhitekturi), i može tipično biti 32-bitno (STM32, RP2, nRF, Unix x86) ili 64-bitno (Unix x64). Također može biti veće od veličine riječi za određene reprezentacije objekata, na primjer OBJ_REPR_D ima mp_obj_t veličine 64 bita na 32-bitnoj arhitekturi.

mp_obj_t predstavlja MicroPython objekt, na primjer cijeli broj, broj s pomičnim zarezom, tip, rječnik ili instancu klase. Neki objekti, poput logičkih vrijednosti i malih cijelih brojeva, imaju svoju vrijednost pohranjenu izravno u vrijednosti mp_obj_t i ne zahtijevaju dodatnu memoriju. Drugi objekti imaju svoju vrijednost pohranjenu negdje drugdje u memoriji (na primjer na gomili koju sakuplja sakupljač smeća), a njihov mp_obj_t sadrži pokazivač na tu memoriju. Dio mp_obj_t je oznaka koja govori o kojem se tipu objekta radi.

Pogledajte py/mpconfig.h za konkretne detalje o dostupnim reprezentacijama.

Označavanje pokazivača (Pointer tagging)

Budući da su pokazivači poravnati na riječ, kad se pohrane u mp_obj_t niži bitovi tog rukovatelja objektom bit će nula. Na primjer, na 32-bitnoj arhitekturi niža 2 bita bit će nula:

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

Ti su bitovi rezervirani za pohranjivanje oznake. Oznaka pohranjuje dodatne informacije umjesto uvođenja novog polja za pohranu tih informacija u objektu, što bi moglo biti neučinkovito. U MicroPythonu oznaka govori radi li se o malom cijelom broju, internaliziranom (malom) nizu znakova ili konkretnom objektu, a na svaki od njih primjenjuje se drugačija semantika.

Za male cijele brojeve mapiranje je sljedeće:

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

Gdje zvjezdice sadrže stvarnu vrijednost cijelog broja. Za internalizirani niz znakova ili neposredni objekt (npr. True) raspored vrijednosti mp_obj_t je, redom:

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

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

Dok konkretan objekt koji nije nijedan od navedenih ima oblik:

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

Zvjezdice ovdje odgovaraju adresi konkretnog objekta u memoriji.

Dodjela objekata

Vrijednost malog cijelog broja pohranjuje se izravno u mp_obj_t i dodjeljuje se na licu mjesta, a ne na gomili ili negdje drugdje. Stoga stvaranje malih cijelih brojeva ne utječe na gomilu. Slično vrijedi za internalizirane nizove znakova koji već imaju svoje tekstualne podatke pohranjene negdje drugdje te za neposredne vrijednosti poput None, False i True.

Sve ostalo što je konkretan objekt dodjeljuje se na gomili, a njegova struktura objekta je takva da je u zaglavlju objekta rezervirano polje za pohranu tipa objekta.

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

Najmanja jedinica dodjele gomile je blok, koji je veličine četiri strojne riječi (16 bajtova na 32-bitnom stroju, 32 bajta na 64-bitnom stroju). Druga struktura koja se također dodjeljuje na gomili prati dodjelu objekata u svakom bloku. Ta se struktura naziva bitmapa.

../_images/bitmap.png

Bitmapa prati je li blok „slobodan” ili „u upotrebi” i koristi dva bita za praćenje tog stanja za svaki blok.

Mark-sweep sakupljač smeća upravlja objektima dodijeljenim na gomili, a također koristi bitmapu za označavanje objekata koji su još u upotrebi. Pogledajte py/gc.c za potpunu implementaciju ovih detalja.

Dodjela: raspored gomile

Gomila je organizirana tako da se sastoji od blokova u skupovima (pools). Blok može imati različita svojstva:

  • ATB(allocation table byte): Ako je postavljen, tada je blok normalan blok

  • FREE: Slobodan blok

  • HEAD: Početak lanca blokova

  • TAIL: U repu lanca blokova

  • MARK : Označeni početni blok

  • FTB(finaliser table byte): Ako je postavljen, tada blok ima finalizator