Quản lý Bộ nhớ

Khác với các ngôn ngữ lập trình như C/C++, MicroPython ẩn các chi tiết quản lý bộ nhớ khỏi nhà phát triển bằng cách hỗ trợ quản lý bộ nhớ tự động. Quản lý bộ nhớ tự động là một kỹ thuật được hệ điều hành hoặc ứng dụng sử dụng để tự động quản lý việc cấp phát và giải phóng bộ nhớ. Điều này giúp loại bỏ những vấn đề như quên giải phóng bộ nhớ đã cấp phát cho một đối tượng. Quản lý bộ nhớ tự động cũng tránh được vấn đề nghiêm trọng là sử dụng bộ nhớ đã được giải phóng. Quản lý bộ nhớ tự động có nhiều hình thức, một trong số đó là thu gom rác (GC).

Bộ thu gom rác thường có hai trách nhiệm;

  1. Cấp phát các đối tượng mới trong bộ nhớ khả dụng.

  2. Giải phóng bộ nhớ không còn sử dụng.

Có nhiều thuật toán GC nhưng MicroPython sử dụng chính sách Mark and Sweep để quản lý bộ nhớ. Thuật toán này có giai đoạn đánh dấu (mark) duyệt qua heap để đánh dấu tất cả các đối tượng còn sống, trong khi giai đoạn quét (sweep) duyệt qua heap để thu hồi tất cả các đối tượng chưa được đánh dấu.

Chức năng thu gom rác trong MicroPython được cung cấp qua module tích hợp gc:

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

Ngay cả khi gc.disable() được gọi, quá trình thu gom vẫn có thể được kích hoạt bằng gc.collect().

Bộ nhớ MicroPython từ mã C

Cần có hiểu biết về bộ thu gom rác khi viết mã C cấp phát bộ nhớ từ "Python heap" (tức là các hàm m_malloc(), m_malloc0(), m_free(), v.v.).

Giai đoạn đánh dấu của bộ thu gom rác quét để tìm các con trỏ trực tiếp đến bộ nhớ heap bắt đầu từ các root sau:

  • Stack của runtime Python chính (hoặc REPL).

  • Stack của mỗi "luồng Python", đối với các cổng triển khai luồng Python trên các luồng hoặc tác vụ của hệ điều hành gốc.

  • Các "con trỏ gốc" được định nghĩa trong mã C bằng macro MP_REGISTER_ROOT_POINTER. Đây là cách khuyến nghị để có các con trỏ có phạm vi tĩnh đến Python heap.

  • Các cấp phát được theo dõi thực hiện bằng các hàm m_tracked_calloc(), m_tracked_reallocm_tracked_free(). Những hàm đặc biệt này cho phép cấp phát một khối bộ nhớ luôn được bộ thu gom rác coi là còn sống. Tương tự như cấp phát bộ nhớ trong C, bộ nhớ này chỉ được giải phóng bằng cách gọi m_tracked_free() hoặc bằng soft reset. Mỗi lần cấp phát được theo dõi đều có chi phí nhỏ về bộ nhớ và thời gian chạy. Tính năng này không được bật theo mặc định trên tất cả các cổng.

Sau đó, bộ thu gom rác đệ quy quét và đánh dấu tất cả bộ nhớ được trỏ bởi các con trỏ gốc, cho đến khi tất cả các địa chỉ được xử lý hết. Điều này đủ để tìm tất cả các đối tượng Python vẫn đang được runtime MicroPython sử dụng.

Tuy nhiên, bộ nhớ sau đây sẽ không được bộ thu gom rác quét và có thể bị giải phóng sớm:

  • Các biến C tĩnh hoặc toàn cục chứa con trỏ đến bộ nhớ heap.

  • Các con trỏ không trỏ đến "đầu" của một bộ đệm được cấp phát (tức là đến địa chỉ chính xác được trả về bởi m_malloc()), mà thay vào đó trỏ đến một địa chỉ bên trong bộ đệm được cấp phát (ví dụ: con trỏ đến một struct lồng nhau). Vì lý do hiệu năng, bộ thu gom rác không đánh dấu bộ đệm bao ngoài trong các trường hợp này.

  • Stack của bất kỳ luồng hoặc tác vụ RTOS nào không chạy mã Python hoặc không được đăng ký thủ công là "luồng Python" (đối với các cổng hỗ trợ luồng hoặc tác vụ gốc).

Các cách để tránh use-after-free trong các tình huống này:

  • Sử dụng API cấp phát được theo dõi m_tracked_calloc(), m_tracked_realloc()m_tracked_free().

  • Đăng ký một con trỏ gốc (xem ở trên), thay vì lưu trữ con trỏ trong một biến tĩnh.

  • Tái cấu trúc mã, ví dụ bằng cách có một API mà mã Python khởi tạo một đối tượng Python singleton (được triển khai trong C) giữ tất cả các con trỏ liên quan thay vì lưu chúng trong các biến tĩnh.

Ghi chú

Đặt lại mềm luôn xóa Python heap và giải phóng tất cả bộ nhớ. Điều quan trọng là không được giữ bất kỳ con trỏ nào đến heap sau khi soft reset, vì chúng sẽ trở thành các con trỏ lơ lửng đến bộ nhớ đã được giải phóng.

Một số cổng cũng hỗ trợ "C heap" (xem Cấp phát bộ nhớ động C), trong trường hợp này bạn có thể cấp phát bộ nhớ sẽ vẫn hợp lệ sau soft reset bằng cách gọi các hàm C chuẩn như malloc, v.v.

Mô hình đối tượng

Tất cả các đối tượng MicroPython đều được tham chiếu bằng kiểu dữ liệu mp_obj_t. Đây thường là kích thước word (tức là cùng kích thước với một con trỏ trên kiến trúc đích), và thường có thể là 32-bit (STM32, RP2, nRF, Unix x86) hoặc 64-bit (Unix x64). Nó cũng có thể lớn hơn kích thước word đối với một số biểu diễn đối tượng nhất định, ví dụ OBJ_REPR_Dmp_obj_t kích thước 64-bit trên kiến trúc 32-bit.

Một mp_obj_t đại diện cho một đối tượng MicroPython, ví dụ một số nguyên, số thực, kiểu, dict hoặc instance của lớp. Một số đối tượng, như boolean và số nguyên nhỏ, có giá trị được lưu trực tiếp trong giá trị mp_obj_t và không cần bộ nhớ bổ sung. Các đối tượng khác có giá trị được lưu ở nơi khác trong bộ nhớ (ví dụ trên heap được thu gom rác) và mp_obj_t của chúng chứa một con trỏ đến bộ nhớ đó. Một phần của mp_obj_t là tag cho biết loại đối tượng là gì.

Xem py/mpconfig.h để biết chi tiết cụ thể về các biểu diễn có sẵn.

Gắn thẻ con trỏ

Vì các con trỏ được căn chỉnh theo word, khi chúng được lưu trong một mp_obj_t, các bit thấp hơn của handle đối tượng này sẽ bằng không. Ví dụ trên kiến trúc 32-bit, 2 bit thấp hơn sẽ bằng không:

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

Các bit này được dành riêng cho mục đích lưu trữ tag. Tag lưu trữ thông tin bổ sung thay vì giới thiệu một trường mới để lưu thông tin đó trong đối tượng, vốn có thể kém hiệu quả. Trong MicroPython, tag cho biết chúng ta đang xử lý một số nguyên nhỏ, chuỗi được intern (nhỏ) hay một đối tượng cụ thể, và các ngữ nghĩa khác nhau áp dụng cho mỗi loại.

Đối với số nguyên nhỏ, ánh xạ là như sau:

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

Trong đó các dấu hoa thị giữ giá trị số nguyên thực tế. Đối với một chuỗi được intern hoặc một đối tượng tức thì (ví dụ: True), bố cục của giá trị mp_obj_t lần lượt là:

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

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

Trong khi một đối tượng cụ thể không phải là bất kỳ loại nào ở trên có dạng:

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

Các dấu hoa thị ở đây tương ứng với địa chỉ của đối tượng cụ thể trong bộ nhớ.

Cấp phát đối tượng

Giá trị của một số nguyên nhỏ được lưu trực tiếp trong mp_obj_t và sẽ được cấp phát tại chỗ, không phải trên heap hay nơi khác. Do đó, việc tạo số nguyên nhỏ không ảnh hưởng đến heap. Tương tự với các chuỗi được intern đã có dữ liệu văn bản được lưu ở nơi khác, và các giá trị tức thì như None, FalseTrue.

Mọi thứ khác là đối tượng cụ thể đều được cấp phát trên heap và cấu trúc đối tượng của nó như vậy là một trường được dành riêng trong header đối tượng để lưu kiểu của đối tượng.

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

Đơn vị cấp phát nhỏ nhất của heap là một khối, có kích thước bốn word máy (16 byte trên máy 32-bit, 32 byte trên máy 64-bit). Một cấu trúc khác cũng được cấp phát trên heap theo dõi việc cấp phát đối tượng trong mỗi khối. Cấu trúc này được gọi là bitmap.

../_images/bitmap.png

Bitmap theo dõi liệu một khối là "rảnh" hay "đang sử dụng" và dùng hai bit để theo dõi trạng thái này cho mỗi khối.

Bộ thu gom rác đánh dấu-quét quản lý các đối tượng được cấp phát trên heap, và cũng sử dụng bitmap để đánh dấu các đối tượng vẫn đang được sử dụng. Xem py/gc.c để biết toàn bộ triển khai các chi tiết này.

Cấp phát: bố cục heap

Heap được sắp xếp sao cho nó bao gồm các khối trong các pool. Một khối có thể có các thuộc tính khác nhau:

  • ATB(allocation table byte): Nếu được đặt, thì khối đó là khối bình thường

  • FREE: Khối rảnh

  • HEAD: Đầu của một chuỗi các khối

  • TAIL: Trong phần đuôi của một chuỗi các khối

  • MARK : Khối đầu được đánh dấu

  • FTB(finaliser table byte): Nếu được đặt, thì khối đó có một finaliser