ניהול זיכרון

בניגוד לשפות תכנות כגון C/C++, MicroPython מסתירה מהמפתח את פרטי ניהול הזיכרון על ידי תמיכה בניהול זיכרון אוטומטי. ניהול זיכרון אוטומטי הוא טכניקה שבה משתמשות מערכות הפעלה או יישומים כדי לנהל באופן אוטומטי את הקצאת הזיכרון ושחרורו. הדבר מבטל אתגרים כגון שכחה לשחרר את הזיכרון שהוקצה לאובייקט. ניהול זיכרון אוטומטי גם מונע את הבעיה הקריטית של שימוש בזיכרון שכבר שוחרר. ניהול זיכרון אוטומטי מתבצע בצורות רבות, כשאחת מהן היא איסוף אשפה (GC).

אוסף האשפה (garbage collector) ממלא בדרך כלל שני תפקידים;

  1. הקצאת אובייקטים חדשים בזיכרון הפנוי.

  2. שחרור זיכרון שאינו בשימוש.

קיימים אלגוריתמים רבים לאיסוף אשפה, אך MicroPython משתמשת במדיניות Mark and Sweep לניהול הזיכרון. לאלגוריתם זה יש שלב סימון (mark) שעובר על הערימה (heap) ומסמן את כל האובייקטים החיים, בעוד ששלב הטאטוא (sweep) עובר על הערימה ומשחרר את כל האובייקטים הלא מסומנים.

פונקציונליות איסוף האשפה ב-MicroPython זמינה דרך המודול המובנה gc:

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

גם כאשר gc.disable() נקרא, ניתן לעורר איסוף באמצעות gc.collect().

זיכרון MicroPython מקוד C

נדרשת מודעות לאוסף האשפה בעת כתיבת קוד C שמקצה זיכרון מ“ערימת Python“ (כלומר הפונקציות m_malloc(), m_malloc0(), m_free() וכדומה).

שלב הסימון של אוסף האשפה סורק מצביעים חיים לזיכרון הערימה החל מהשורשים הבאים:

  • המחסנית (stack) של זמן הריצה הראשי של Python (או REPL).

  • המחסניות של כל ”חוט (thread) של Python“, עבור פורטים שמיישמים חוטי Python מעל גבי חוטים או משימות נייטיביים של מערכת ההפעלה.

  • ”מצביעי השורש“ המוגדרים בקוד C באמצעות המאקרו MP_REGISTER_ROOT_POINTER. אלו הם הדרך המומלצת להחזיק מצביעים בעלי תחום סטטי לערימת Python.

  • הקצאות מנוטרות שנעשו באמצעות הפונקציות m_tracked_calloc(), m_tracked_realloc ו-m_tracked_free(). פונקציות מיוחדות אלו מאפשרות להקצות בלוק זיכרון שאוסף האשפה תמיד מתייחס אליו כאל חי. בדומה להקצאת זיכרון ב-C, זיכרון זה משוחרר רק על ידי קריאה ל-m_tracked_free() או על ידי איפוס רך (soft reset). יש תקורת זיכרון ותקורת זמן ריצה קטנה לכל הקצאה מנוטרת. תכונה זו אינה מופעלת כברירת מחדל בכל הפורטים.

אוסף האשפה לאחר מכן סורק ומסמן רקורסיבית את כל הזיכרון שאליו מצביעים מצביעי השורש, עד שכל הכתובות מוצו. די בכך כדי למצוא את כל אובייקטי Python שעדיין נמצאים בשימוש על ידי זמן הריצה של MicroPython.

עם זאת, הזיכרון הבא לא ייסרק על ידי אוסף האשפה ועלול להשתחרר בטרם עת:

  • משתני C סטטיים או גלובליים המכילים מצביעים לזיכרון הערימה.

  • מצביעים שאינם מצביעים על ”ראש“ של חוצץ (buffer) מוקצה (כלומר על הכתובת המדויקת שהוחזרה על ידי m_malloc()), אלא על כתובת בתוך החוצץ המוקצה (לדוגמה, מצביע למבנה מקונן). מטעמי ביצועים, אוסף האשפה אינו מסמן את החוצץ העוטף במקרים אלו.

  • המחסנית של כל חוט או משימת RTOS שאינם מריצים קוד Python או שלא נרשמו ידנית כ“חוט Python“ (עבור פורטים שתומכים בחוטים או משימות נייטיביים).

דרכים להימנע משימוש לאחר שחרור (use-after-free) בתרחישים אלו:

  • השתמשו ב-API להקצאה מנוטרת m_tracked_calloc(), m_tracked_realloc() ו-m_tracked_free().

  • רשמו מצביע שורש (ראו לעיל), במקום לאחסן מצביע במשתנה סטטי.

  • בנו מחדש את הקוד, לדוגמה על ידי שימוש ב-API שבו קוד Python מאתחל אובייקט Python יחיד (singleton) (המיושם ב-C) המחזיק את כל המצביעים הרלוונטיים במקום להחזיק אותם במשתנים סטטיים.

הערה

איפוס רך תמיד מנקה את ערימת Python ומשחרר את כל הזיכרון. חשוב לא להחזיק מצביעים כלשהם לערימה לאחר איפוס רך, מכיוון שהם יהפכו למצביעים תלויים (dangling) לזיכרון משוחרר.

חלק מהפורטים תומכים גם ב“ערימת C“ (ראו הקצאת זיכרון דינמית ב-C), ובמקרה זה ניתן להקצות זיכרון שיישאר תקף לאורך איפוס רך על ידי קריאה לפונקציות C סטנדרטיות malloc וכדומה.

מודל האובייקטים

כל אובייקטי MicroPython מיוצגים על ידי טיפוס הנתונים mp_obj_t. בדרך כלל זהו בגודל מילה (כלומר באותו גודל כמו מצביע בארכיטקטורת היעד), ויכול להיות בדרך כלל 32 סיביות (STM32, RP2, nRF, Unix x86) או 64 סיביות (Unix x64). הוא יכול גם להיות גדול מגודל מילה עבור ייצוגי אובייקטים מסוימים, לדוגמה ל-OBJ_REPR_D יש mp_obj_t בגודל 64 סיביות בארכיטקטורה של 32 סיביות.

mp_obj_t מייצג אובייקט MicroPython, לדוגמה מספר שלם, מספר ממשי (float), טיפוס, מילון (dict) או מופע מחלקה. אובייקטים מסוימים, כמו ערכים בוליאניים ומספרים שלמים קטנים, מאחסנים את ערכם ישירות בערך mp_obj_t ואינם דורשים זיכרון נוסף. אובייקטים אחרים מאחסנים את ערכם במקום אחר בזיכרון (לדוגמה בערימה המנוהלת על ידי אוסף האשפה) וה-mp_obj_t שלהם מכיל מצביע לאותו זיכרון. חלק מ-mp_obj_t הוא התגית (tag) שמציינת מאיזה סוג אובייקט מדובר.

ראו py/mpconfig.h לפרטים הספציפיים של הייצוגים הזמינים.

תיוג מצביעים (Pointer tagging)

מכיוון שמצביעים מיושרים לגבול מילה, כאשר הם מאוחסנים ב-mp_obj_t הסיביות הנמוכות של ידית האובייקט הזו יהיו אפס. לדוגמה בארכיטקטורת 32 סיביות 2 הסיביות הנמוכות יהיו אפס:

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

סיביות אלו שמורות למטרות אחסון של תגית. התגית מאחסנת מידע נוסף במקום הוספת שדה חדש לאחסון מידע זה באובייקט, דבר שעלול להיות לא יעיל. ב-MicroPython התגית מציינת אם אנו מתמודדים עם מספר שלם קטן, מחרוזת מוטמעת (interned) (קטנה) או אובייקט ממשי, וסמנטיקה שונה חלה על כל אחד מאלו.

עבור מספרים שלמים קטנים המיפוי הוא זה:

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

כאשר הכוכביות מחזיקות את ערך המספר השלם בפועל. עבור מחרוזת מוטמעת או אובייקט מיידי (לדוגמה True) המבנה של הערך mp_obj_t הוא, בהתאמה:

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

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

בעוד שאובייקט ממשי שאינו אף אחד מהאמורים לעיל מקבל את הצורה:

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

הכוכביות כאן מתאימות לכתובת האובייקט הממשי בזיכרון.

הקצאת אובייקטים

ערכו של מספר שלם קטן מאוחסן ישירות ב-mp_obj_t ויוקצה במקום, לא בערימה או במקום אחר. ככזה, יצירת מספרים שלמים קטנים אינה משפיעה על הערימה. באופן דומה עבור מחרוזות מוטמעות שכבר מאחסנות את נתוני הטקסט שלהן במקום אחר, וערכים מיידיים כמו None, False ו-True.

כל דבר אחר שהוא אובייקט ממשי מוקצה בערימה ומבנה האובייקט שלו הוא כזה ששדה נשמר בכותרת האובייקט כדי לאחסן את סוג האובייקט.

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

יחידת ההקצאה הקטנה ביותר של הערימה היא בלוק, שגודלו ארבע מילות מכונה (16 בייטים במכונת 32 סיביות, 32 בייטים במכונת 64 סיביות). מבנה נוסף שגם מוקצה בערימה עוקב אחר הקצאת האובייקטים בכל בלוק. מבנה זה נקרא מפת סיביות (bitmap).

../_images/bitmap.png

מפת הסיביות עוקבת אחר האם בלוק ”פנוי“ או ”בשימוש“ ומשתמשת בשתי סיביות כדי לעקוב אחר מצב זה עבור כל בלוק.

אוסף האשפה מסוג mark-sweep מנהל את האובייקטים המוקצים בערימה, וגם מנצל את מפת הסיביות כדי לסמן אובייקטים שעדיין בשימוש. ראו py/gc.c למימוש המלא של פרטים אלו.

הקצאה: פריסת הערימה

הערימה מסודרת כך שהיא מורכבת מבלוקים בתוך מאגרים (pools). לבלוק יכולות להיות תכונות שונות:

  • ATB(allocation table byte): אם מוגדר, אז הבלוק הוא בלוק רגיל

  • FREE: בלוק פנוי

  • HEAD: ראש של שרשרת בלוקים

  • TAIL: בזנב של שרשרת בלוקים

  • MARK : בלוק ראש מסומן

  • FTB(finaliser table byte): אם מוגדר, אז לבלוק יש מסיים (finaliser)