إدارة الذاكرة

على عكس لغات البرمجة مثل C/C++، تُخفي MicroPython تفاصيل إدارة الذاكرة عن المطوّر من خلال دعم الإدارة التلقائية للذاكرة. الإدارة التلقائية للذاكرة هي تقنية تستخدمها أنظمة التشغيل أو التطبيقات لإدارة تخصيص الذاكرة وإلغاء تخصيصها تلقائياً. وهذا يقضي على تحديات مثل نسيان تحرير الذاكرة المخصصة لكائن ما. كما تتجنب الإدارة التلقائية للذاكرة المشكلة الحرجة المتمثلة في استخدام ذاكرة سبق تحريرها. تتخذ الإدارة التلقائية للذاكرة أشكالاً عديدة، أحدها هو جمع المهملات (GC).

عادةً ما يتحمّل جامع المهملات مسؤوليتين؛

  1. تخصيص كائنات جديدة في الذاكرة المتاحة.

  2. تحرير الذاكرة غير المستخدمة.

هناك العديد من خوارزميات جمع المهملات، لكن MicroPython تستخدم سياسة التحديد والكنس لإدارة الذاكرة. تتضمن هذه الخوارزمية مرحلة تحديد تجتاز الكومة (heap) لتحديد جميع الكائنات الحية، بينما تمرّ مرحلة الكنس عبر الكومة لاسترجاع جميع الكائنات غير المحدّدة.

تتوفر وظيفة جمع المهملات في 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() وما إلى ذلك).

تفحص مرحلة التحديد في جامع المهملات بحثاً عن المؤشرات الحية إلى ذاكرة الكومة بدءاً من الجذور التالية:

  • مكدّس وقت تشغيل Python الرئيسي (أو REPL).

  • مكدّسات كل "خيط Python"، بالنسبة للمنافذ التي تنفّذ خيوط Python فوق خيوط أو مهام نظام التشغيل الأصلية.

  • "المؤشرات الجذرية" المعرّفة في شيفرة C باستخدام الماكرو MP_REGISTER_ROOT_POINTER. هذه هي الطريقة الموصى بها للحصول على مؤشرات ذات نطاق ساكن إلى كومة Python.

  • التخصيصات المتعقَّبة التي تُنفَّذ بالدوال m_tracked_calloc() و m_tracked_realloc و m_tracked_free(). تتيح هذه الدوال الخاصة تخصيص كتلة من الذاكرة يعتبرها جامع المهملات دائماً حية. على غرار تخصيص الذاكرة في C، لا يتم تحرير هذه الذاكرة إلا باستدعاء m_tracked_free() أو عبر إعادة التعيين الناعمة. هناك قدر صغير من استهلاك الذاكرة والعبء أثناء وقت التشغيل لكل تخصيص متعقَّب. هذه الميزة غير مفعّلة افتراضياً على جميع المنافذ.

ثم يفحص جامع المهملات بشكل تعاودي ويحدّد كل الذاكرة التي تشير إليها المؤشرات الجذرية، حتى تُستنفد جميع العناوين. وهذا كافٍ للعثور على جميع كائنات Python التي لا تزال قيد الاستخدام من قِبل وقت تشغيل MicroPython.

ومع ذلك، لن يفحص جامع المهملات الذاكرة التالية، وقد يتم تحريرها قبل الأوان:

  • متغيرات C الساكنة أو العامة التي تحتوي على مؤشرات إلى ذاكرة الكومة.

  • المؤشرات التي لا تشير إلى "رأس" مخزن مؤقت مخصّص (أي إلى العنوان الدقيق الذي تُعيده m_malloc())، بل تشير بدلاً من ذلك إلى عنوان داخل المخزن المؤقت المخصّص (على سبيل المثال، مؤشر إلى بنية متداخلة). لأسباب تتعلق بالأداء، لا يحدّد جامع المهملات المخزن المؤقت المحيط في هذه الحالات.

  • مكدّس أي خيط أو مهمة نظام تشغيل في الوقت الحقيقي (RTOS) لا يشغّل شيفرة Python أو لم يُسجَّل يدوياً بوصفه "خيط Python" (بالنسبة للمنافذ التي تدعم الخيوط أو المهام الأصلية).

طرق تجنّب الاستخدام بعد التحرير في هذه السيناريوهات:

  • استخدم واجهة برمجة التطبيقات للتخصيص المتعقَّب m_tracked_calloc() و m_tracked_realloc() و m_tracked_free().

  • سجّل مؤشراً جذرياً (انظر أعلاه)، بدلاً من تخزين مؤشر في متغير ساكن.

  • أعد هيكلة الشيفرة، على سبيل المثال بإنشاء واجهة برمجة تطبيقات حيث تهيّئ شيفرة Python كائن Python مفرداً (مُنفَّذاً في C) يحتفظ بجميع المؤشرات ذات الصلة بدلاً من وجودها في متغيرات ساكنة.

ملاحظة

تقوم إعادة التعيين الناعمة دائماً بمسح كومة Python وتحرير كل الذاكرة. من المهم عدم الاحتفاظ بأي مؤشرات إلى الكومة بعد إعادة التعيين الناعمة، لأنها ستصبح مؤشرات معلّقة إلى ذاكرة محرَّرة.

تدعم بعض المنافذ "كومة 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، على سبيل المثال عدد صحيح أو عدد عشري أو نوع أو قاموس أو نسخة من صنف. تخزَّن قيمة بعض الكائنات، مثل القيم المنطقية والأعداد الصحيحة الصغيرة، مباشرة في قيمة mp_obj_t ولا تتطلب ذاكرة إضافية. أما الكائنات الأخرى فتخزَّن قيمها في مكان آخر من الذاكرة (على سبيل المثال في الكومة الخاضعة لجمع المهملات) ويحتوي mp_obj_t الخاص بها على مؤشر إلى تلك الذاكرة. جزء من mp_obj_t هو الوسم الذي يخبر بنوع الكائن.

انظر py/mpconfig.h للاطلاع على التفاصيل المحددة للتمثيلات المتاحة.

وسم المؤشرات

لأن المؤشرات تكون محاذاة على حدود الكلمة، فعند تخزينها في mp_obj_t ستكون البتات الدنيا لمقبض هذا الكائن أصفاراً. على سبيل المثال، على معمارية 32 بت ستكون البتّان الأدنى صفرين:

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

هذه البتات محجوزة لأغراض تخزين الوسم. يخزّن الوسم معلومات إضافية بدلاً من إدخال حقل جديد لتخزين تلك المعلومات في الكائن، وهو ما قد يكون غير فعّال. في MicroPython يخبر الوسم ما إذا كنا نتعامل مع عدد صحيح صغير أو سلسلة (صغيرة) مدمجة (interned) أو كائن محسوس، وتُطبَّق دلالات مختلفة على كل من هذه.

بالنسبة للأعداد الصحيحة الصغيرة يكون التعيين كالتالي:

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

حيث تحمل النجوم قيمة العدد الصحيح الفعلية. بالنسبة لسلسلة مدمجة (interned) أو كائن مباشر (مثل True) فإن تخطيط قيمة mp_obj_t يكون، على التوالي:

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

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

بينما يأخذ الكائن المحسوس الذي ليس أياً مما سبق الشكل التالي:

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

تتوافق النجوم هنا مع عنوان الكائن المحسوس في الذاكرة.

تخصيص الكائنات

تخزَّن قيمة العدد الصحيح الصغير مباشرة في mp_obj_t وستُخصَّص في مكانها، وليس على الكومة أو في مكان آخر. وبناءً على ذلك، لا يؤثر إنشاء الأعداد الصحيحة الصغيرة على الكومة. وبالمثل بالنسبة للسلاسل المدمجة (interned) التي سبق تخزين بياناتها النصية في مكان آخر، وللقيم المباشرة مثل None و False و True.

أما كل ما عدا ذلك مما يُعدّ كائناً محسوساً فيُخصَّص على الكومة، وتكون بنية كائنه بحيث يُحجَز حقل في ترويسة الكائن لتخزين نوع الكائن.

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

أصغر وحدة تخصيص في الكومة هي الكتلة (block)، وحجمها أربع كلمات آلة (16 بايت على آلة 32 بت، و32 بايت على آلة 64 بت). وهناك بنية أخرى مخصّصة أيضاً على الكومة تتعقّب تخصيص الكائنات في كل كتلة. تُسمّى هذه البنية الخريطة النقطية (bitmap).

../_images/bitmap.png

تتعقّب الخريطة النقطية ما إذا كانت الكتلة "حرة" أم "قيد الاستخدام"، وتستخدم بتّين لتعقّب هذه الحالة لكل كتلة.

يدير جامع المهملات بأسلوب التحديد والكنس الكائنات المخصّصة على الكومة، ويستفيد أيضاً من الخريطة النقطية لتحديد الكائنات التي لا تزال قيد الاستخدام. انظر py/gc.c للاطلاع على التنفيذ الكامل لهذه التفاصيل.

التخصيص: تخطيط الكومة

تُرتَّب الكومة بحيث تتكون من كتل ضمن مجمّعات (pools). يمكن أن تتمتع الكتلة بخصائص مختلفة:

  • ATB (بايت جدول التخصيص): إذا كان مضبوطاً، فإن الكتلة كتلة عادية

  • FREE: كتلة حرة

  • HEAD: رأس سلسلة من الكتل

  • TAIL: في ذيل سلسلة من الكتل

  • MARK : كتلة رأس محدَّدة

  • FTB (بايت جدول المُنهي): إذا كان مضبوطاً، فإن الكتلة تحتوي على مُنهٍ (finaliser)