Speicherverwaltung¶
Im Gegensatz zu Programmiersprachen wie C/C++ verbirgt MicroPython die Details der Speicherverwaltung vor dem Entwickler, indem es eine automatische Speicherverwaltung unterstützt. Die automatische Speicherverwaltung ist eine Technik, die von Betriebssystemen oder Anwendungen verwendet wird, um die Zuweisung und Freigabe von Speicher automatisch zu verwalten. Dadurch entfallen Probleme wie das Vergessen, den einem Objekt zugewiesenen Speicher freizugeben. Die automatische Speicherverwaltung vermeidet außerdem das kritische Problem, bereits freigegebenen Speicher zu verwenden. Die automatische Speicherverwaltung tritt in vielen Formen auf, eine davon ist die Garbage Collection (GC).
Der Garbage Collector hat in der Regel zwei Aufgaben:
Neue Objekte im verfügbaren Speicher zuweisen.
Ungenutzten Speicher freigeben.
Es gibt viele GC-Algorithmen, aber MicroPython verwendet zur Speicherverwaltung die Mark-and-Sweep-Strategie. Dieser Algorithmus besitzt eine Markierungsphase, die den Heap durchläuft und alle lebenden Objekte markiert, während die Sweep-Phase den Heap durchgeht und alle nicht markierten Objekte wieder freigibt.
Die Garbage-Collection-Funktionalität ist in MicroPython über das eingebaute Modul gc verfügbar:
>>> x = 5
>>> x
5
>>> import gc
>>> gc.enable()
>>> gc.mem_alloc()
1312
>>> gc.mem_free()
2071392
>>> gc.collect()
19
>>> gc.disable()
>>>
Selbst wenn gc.disable() aufgerufen wurde, kann eine Sammlung mit gc.collect() ausgelöst werden.
MicroPython-Speicher aus C-Code¶
Beim Schreiben von C-Code, der Speicher aus dem „Python-Heap“ zuweist (d. h. die Funktionen m_malloc(), m_malloc0(), m_free() usw.), ist die Kenntnis des Garbage Collectors erforderlich.
Die Markierungsphase des Garbage Collectors sucht ausgehend von den folgenden Wurzeln (Roots) nach lebenden Zeigern auf Heap-Speicher:
Der Stack der Haupt-Python-Laufzeitumgebung (oder REPL).
Die Stacks der einzelnen „Python-Threads“, bei Ports, die Python-Threads auf Basis nativer Betriebssystem-Threads oder -Tasks implementieren.
Die im C-Code mit dem Makro
MP_REGISTER_ROOT_POINTERdefinierten „Root-Zeiger“. Dies ist die empfohlene Methode, um statisch sichtbare Zeiger auf den Python-Heap zu halten.Verfolgte Zuweisungen (Tracked Allocations), die mit den Funktionen
m_tracked_calloc(),m_tracked_reallocundm_tracked_free()erstellt werden. Diese speziellen Funktionen ermöglichen die Zuweisung eines Speicherblocks, der vom Garbage Collector stets als lebend betrachtet wird. Ähnlich wie bei der Speicherzuweisung in C wird dieser Speicher nur durch den Aufruf vonm_tracked_free()oder durch einen Soft-Reset freigegeben. Jede verfolgte Zuweisung verursacht einen geringen Speicher- und Laufzeit-Overhead. Diese Funktion ist nicht standardmäßig auf allen Ports aktiviert.
Der Garbage Collector durchsucht und markiert dann rekursiv den gesamten Speicher, auf den die Root-Zeiger verweisen, bis alle Adressen abgearbeitet sind. Dies genügt, um alle Python-Objekte zu finden, die noch von der MicroPython-Laufzeitumgebung verwendet werden.
Der folgende Speicher wird vom Garbage Collector jedoch nicht durchsucht und könnte vorzeitig freigegeben werden:
Statische oder globale C-Variablen, die Zeiger auf Heap-Speicher enthalten.
Zeiger, die nicht auf den „Anfang“ eines zugewiesenen Puffers verweisen (d. h. auf die exakte von
m_malloc()zurückgegebene Adresse), sondern stattdessen auf eine Adresse innerhalb des zugewiesenen Puffers (zum Beispiel ein Zeiger auf eine verschachtelte Struktur). Aus Performancegründen markiert der Garbage Collector in diesen Fällen den umschließenden Puffer nicht.Der Stack eines beliebigen Threads oder einer RTOS-Task, die keinen Python-Code ausführt oder nicht manuell als „Python-Thread“ registriert wurde (bei Ports, die native Threads oder Tasks unterstützen).
Möglichkeiten, Use-after-free in diesen Szenarien zu vermeiden:
Verwenden Sie die Tracked-Allocation-API
m_tracked_calloc(),m_tracked_realloc()undm_tracked_free().Registrieren Sie einen Root-Zeiger (siehe oben), anstatt einen Zeiger in einer statischen Variable zu speichern.
Strukturieren Sie den Code um, zum Beispiel durch eine API, bei der Python-Code ein Singleton-Python-Objekt (in C implementiert) initialisiert, das alle relevanten Zeiger enthält, anstatt sie in statischen Variablen zu halten.
Bemerkung
Soft-Reset leert immer den Python-Heap und gibt den gesamten Speicher frei. Es ist wichtig, nach einem Soft-Reset keine Zeiger auf den Heap zu halten, da diese zu hängenden Zeigern (Dangling Pointers) auf freigegebenen Speicher werden.
Einige Ports unterstützen zusätzlich einen „C-Heap“ (siehe Dynamische C-Speicherzuweisung). In diesem Fall können Sie Speicher zuweisen, der über einen Soft-Reset hinweg gültig bleibt, indem Sie die Standard-C-Funktionen malloc usw. aufrufen.
Das Objektmodell¶
Auf alle MicroPython-Objekte wird über den Datentyp mp_obj_t verwiesen. Dieser ist üblicherweise wortgroß (d. h. so groß wie ein Zeiger auf der Zielarchitektur) und kann typischerweise 32-Bit (STM32, RP2, nRF, Unix x86) oder 64-Bit (Unix x64) sein. Für bestimmte Objektdarstellungen kann er auch größer als ein Wort sein; zum Beispiel besitzt OBJ_REPR_D auf einer 32-Bit-Architektur ein 64-Bit-großes mp_obj_t.
Ein mp_obj_t repräsentiert ein MicroPython-Objekt, zum Beispiel eine Ganzzahl, einen Float, einen Typ, ein dict oder eine Klasseninstanz. Einige Objekte, wie boolesche Werte und kleine Ganzzahlen, speichern ihren Wert direkt im mp_obj_t-Wert und benötigen keinen zusätzlichen Speicher. Andere Objekte speichern ihren Wert an anderer Stelle im Speicher (zum Beispiel auf dem Garbage-Collected-Heap), und ihr mp_obj_t enthält einen Zeiger auf diesen Speicher. Ein Teil des mp_obj_t ist das Tag, das angibt, um welchen Objekttyp es sich handelt.
Die genauen Details der verfügbaren Darstellungen finden Sie in py/mpconfig.h.
Pointer-Tagging
Da Zeiger wortausgerichtet sind, sind die unteren Bits dieses Objekt-Handles null, wenn sie in einem mp_obj_t gespeichert werden. Auf einer 32-Bit-Architektur sind beispielsweise die unteren 2 Bits null:
********|********|********|******00
Diese Bits sind dazu reserviert, ein Tag zu speichern. Das Tag speichert zusätzliche Informationen, anstatt ein neues Feld im Objekt einzuführen, um diese Informationen abzulegen, was ineffizient sein könnte. In MicroPython gibt das Tag an, ob wir es mit einer kleinen Ganzzahl, einer internierten (kleinen) Zeichenkette oder einem konkreten Objekt zu tun haben, wobei für jedes davon unterschiedliche Semantiken gelten.
Für kleine Ganzzahlen lautet die Zuordnung wie folgt:
********|********|********|*******1
Wobei die Sternchen den tatsächlichen Ganzzahlwert enthalten. Für eine internierte Zeichenkette oder ein unmittelbares Objekt (z. B. True) lautet das Layout des mp_obj_t-Werts jeweils:
********|********|********|*****010
********|********|********|*****110
Während ein konkretes Objekt, das keines der oben genannten ist, die folgende Form annimmt:
********|********|********|******00
Die Sterne entsprechen hier der Adresse des konkreten Objekts im Speicher.
Zuweisung von Objekten¶
Der Wert einer kleinen Ganzzahl wird direkt im mp_obj_t gespeichert und an Ort und Stelle zugewiesen, nicht auf dem Heap oder anderswo. Daher wirkt sich die Erzeugung kleiner Ganzzahlen nicht auf den Heap aus. Gleiches gilt für internierte Zeichenketten, deren Textdaten bereits an anderer Stelle gespeichert sind, sowie für unmittelbare Werte wie None, False und True.
Alles andere, was ein konkretes Objekt ist, wird auf dem Heap zugewiesen, und seine Objektstruktur ist so beschaffen, dass im Objekt-Header ein Feld reserviert ist, um den Typ des Objekts zu speichern.
+++++++++++
+ +
+ type + object header
+ +
+++++++++++
+ + object items
+ +
+ +
+++++++++++
Die kleinste Zuweisungseinheit des Heaps ist ein Block, der vier Maschinenwörter groß ist (16 Byte auf einer 32-Bit-Maschine, 32 Byte auf einer 64-Bit-Maschine). Eine weitere, ebenfalls auf dem Heap zugewiesene Struktur verfolgt die Zuweisung von Objekten in jedem Block. Diese Struktur wird als Bitmap bezeichnet.
Die Bitmap verfolgt, ob ein Block „frei“ oder „in Benutzung“ ist, und verwendet zwei Bits, um diesen Zustand für jeden Block zu erfassen.
Der Mark-Sweep-Garbage-Collector verwaltet die auf dem Heap zugewiesenen Objekte und nutzt zudem die Bitmap, um Objekte zu markieren, die noch in Benutzung sind. Die vollständige Implementierung dieser Details finden Sie unter py/gc.c.
Zuweisung: Heap-Layout
Der Heap ist so aufgebaut, dass er aus Blöcken in Pools besteht. Ein Block kann verschiedene Eigenschaften haben:
ATB (allocation table byte): Wenn gesetzt, dann ist der Block ein normaler Block
FREE: Freier Block
HEAD: Kopf einer Kette von Blöcken
TAIL: Im Schwanz einer Kette von Blöcken
MARK : Markierter Kopfblock
FTB (finaliser table byte): Wenn gesetzt, dann besitzt der Block einen Finalizer