☰
TobudOS 中的 MicroPython 内存管理:gc 模块、mp_obj_t 对象模型与 Mark-Sweep 回收机制
2026/10/10 1:25:09 网站建设 项目流程

【免费下载链接】TobudOS

TobudOS 是面向物联网领域开发的实时操作系统,早期版本基于腾讯自研的物联网操作系统TencentOS Tiny,2020年由腾讯捐赠到开放原子开源基金会进行孵化,2023年正式更名为TobudOS,TobudOS具有低功耗,低资源占用,模块化,安全可靠等特点,可有效提升物联网终端产品开发效率,提供精简的 RTOS 内核,内核组件可裁剪可配置,可快速移植到多种主流 MCU (如 STM32 全系列) 及模组芯片上。而且,基于 RTOS 内核提供了丰富的物联网组件,内部集成主流物联网协议栈(如 CoAP/MQTT/TLS/DTLS/LoRaWAN/NB-IoT 等),可助力物联网终端设备及业务快速接入物联网云平台。

项目地址:https://gitcode.com/openatomfoundation/TobudOS
点击查看免费下载

TobudOS 作为面向物联网领域的轻量级实时操作系统,在语言组件中集成了 MicroPython,为 MCU 应用提供了解释型 Python 能力。MicroPython 与 C/C++ 的最大差异之一在于它向开发者隐藏了内存管理细节——通过自动内存管理(Automatic Memory Management)消除"忘记释放"和"使用已释放内存"这两类经典缺陷。本文以 TobudOS 仓库中 MicroPython 官方开发文档 memorymgt 为主体,完整讲解其垃圾回收策略、对象模型与堆分配机制,并结合本仓库中 MicroPython 的移植源码,说明这些机制在 TobudOS 目标平台上是如何落地配置的。

一、垃圾回收:Mark and Sweep 策略

自动内存管理有诸多形态,其中一种即垃圾回收(GC)。GC 通常承担两项职责:

  1. 在可用内存中分配新对象;
  2. 释放不再使用的内存。

MicroPython 采用的是Mark and Sweep(标记-清除)策略:

  • 标记阶段(mark phase):遍历堆,标记所有仍然存活的对象;
  • 清除阶段(sweep phase):再次遍历堆,回收所有未被标记的对象。

在 TobudOS 仓库的 MicroPython 移植中,该实现位于 py/gc.c。从源码结构看,其分配表字节(ATB, allocation table byte)用两个 bit 跟踪每个 block 的状态,与文档描述一一对应:

// ATB = allocation table byte // 0b00 = FREE -- free block // 0b01 = HEAD -- head of a chain of blocks // 0b10 = TAIL -- in the tail of a chain of blocks // 0b11 = MARK -- marked head block #define AT_FREE (0) #define AT_HEAD (1) #define AT_TAIL (2) #define AT_MARK (3)

(见 py/gc.c#L55-L64)

此外,在启用 finaliser 时(MICROPY_ENABLE_FINALISER),还会有一张FTB(finaliser table byte)位图,用于标记"可能带有析构器的 block",这正是文档末尾提到的 FTB 属性:

// FTB = finaliser table byte // if set, then the corresponding block may have a finaliser #define BLOCKS_PER_FTB (8)

(见 py/gc.c#L89-L98)

二、gc 内建模块:开发者可用的内存接口

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()触发一次回收——disable()只是关闭"自动"触发。

对照 TobudOS 仓库中该模块的实现 py/modgc.c,gc模块的完整函数表为:

函数作用实现说明
gc.collect()手动执行一次回收返回本次回收的对象数(受MICROPY_PY_GC_COLLECT_RETVAL控制,否则返回None)
gc.enable()开启自动回收置位gc_auto_collect_enabled
gc.disable()关闭自动回收清零gc_auto_collect_enabled
gc.isenabled()查询自动回收状态返回布尔值
gc.mem_free()返回堆中可用字节数通过gc_info()读取info.free
gc.mem_alloc()返回堆中已分配字节数通过gc_info()读取info.used
gc.threshold()查询/设置自动回收阈值以 block 为单位换算成字节

gc.collect()的核心实现非常直接(见 py/modgc.c#L34-L41):

STATIC mp_obj_t py_gc_collect(void) { gc_collect(); #if MICROPY_PY_GC_COLLECT_RETVAL return MP_OBJ_NEW_SMALL_INT(MP_STATE_MEM(gc_collected)); #else return mp_const_none; #endif }

其中gc.collect()的返回值(示例中的19)就是本轮 sweep 阶段实际清除的对象数量。

gc.threshold()则控制自动回收的触发时机:无参调用返回当前阈值(字节),传入负值表示取消阈值限制。它在内部把字节阈值除以MICROPY_BYTES_PER_GC_BLOCK换算为 block 数(见 py/modgc.c#L79-L96)。这意味着在资源受限的 MCU 上,开发者可以在大对象分配前手动gc.disable()批量分配、结束后一次性gc.collect(),从而摊薄多次回收的开销。

三、对象模型:mp_obj_t 与指针标签

3.1 所有对象都是 mp_obj_t

MicroPython 中所有对象都由mp_obj_t数据类型表示。它通常是字长大小(与目标架构上指针同宽),因此常见为 32 位(如 STM32、nRF、ESP32、Unix x86)或 64 位(Unix x64)。在 TobudOS 的 MicroPython 移植中,这一类型定义位于 py/obj.h:

#if MICROPY_OBJ_REPR == MICROPY_OBJ_REPR_D typedef uint64_t mp_obj_t; #else typedef void *mp_obj_t; #endif

值得注意的是,某些对象表示下mp_obj_t可以大于字长——例如OBJ_REPR_D在 32 位架构上就是 64 位类型。具体可用的表示形式定义在 py/mpconfig.h 中。

mp_obj_t可以代表整数、浮点数、类型、字典或类实例等对象。其中布尔值、小整数等对象的值直接存放在mp_obj_t里,不需要额外内存;其他对象的值存放在别处(例如 GC 堆上),mp_obj_t中存的是指向该内存的指针。mp_obj_t的一部分 bit 是tag(标签),用于标识对象类型。

3.2 指针标签(Pointer tagging)

由于指针都是字对齐的,存储在mp_obj_t中的低位必然为零。以 32 位架构为例,低 2 位为零:

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

这些零位被挪用为 tag 来携带附加信息——比新增一个字段更高效。tag 告诉解释器:当前是小整数、interned 字符串还是具体对象,三者各有不同的语义:

对象种类低位 tag
小整数 small int*******1
interned 字符串 / qstr*****010
立即数对象(如True)*****110
具体对象(堆上对象指针)******00

小整数的完整映射为********|********|********|*******1,星号部分是实际的整数值。

TobudOS 移植使用的是REPR_A表示,可在 port/mpconfigport.h 中看到:

// Object representation #define MICROPY_OBJ_REPR (MICROPY_OBJ_REPR_A)

REPR_A 下各类型的判定与构造宏正对应文档中的 tag 布局(见 py/obj.h#L84-L127):

static inline bool mp_obj_is_small_int(mp_const_obj_t o) { return (((mp_int_t)(o)) & 1) != 0; // tag = 1 } #define MP_OBJ_NEW_SMALL_INT(small_int) ((mp_obj_t)((((mp_uint_t)(small_int)) << 1) | 1)) static inline bool mp_obj_is_qstr(mp_const_obj_t o) { return (((mp_int_t)(o)) & 7) == 2; // tag = 010 } #define MP_OBJ_NEW_QSTR(qst) ((mp_obj_t)((((mp_uint_t)(qst)) << 3) | 2)) static inline bool mp_obj_is_immediate_obj(mp_const_obj_t o) { return (((mp_int_t)(o)) & 7) == 6; // tag = 110 } static inline bool mp_obj_is_obj(mp_const_obj_t o) { return (((mp_int_t)(o)) & 3) == 0; // tag = 00,堆上具体对象 }

可以看到,文档中抽象描述的 tag 位(1、010、110、00)在源码里就是这些低位的按位运算。

3.3 对象的分配规则

  • 小整数的值直接存放在mp_obj_t中,属于原地分配,不落在堆上,因此创建小整数不影响堆;
  • 已经在其文本数据处存有内容的 interned 字符串,以及None、False、True这类立即数值同理;
  • 其余所有具体对象都分配在堆上,且对象结构在头部(header)中保留一个字段用于存放对象类型。
+++++++++++ + + + type + object header + + +++++++++++ + + object items + + + + +++++++++++

对应地,所有具体对象都必须以mp_obj_base_t作为首成员(见 py/obj.h):

struct _mp_obj_base_t { const mp_obj_type_t *type MICROPY_OBJ_BASE_ALIGNMENT; };

小整数、qstr 对象和内联浮点数则不属于"具体对象",不携带该头。

四、堆布局:block、pool 与 bitmap

4.1 最小分配单元是 block

堆的最小分配单元是block,大小为 4 个机器字(32 位机上 16 字节,64 位机上 32 字节)。这一默认值由 py/mpconfig.h 定义:

#ifndef MICROPY_BYTES_PER_GC_BLOCK #define MICROPY_BYTES_PER_GC_BLOCK (4 * MP_BYTES_PER_OBJ_WORD) #endif

py/gc.c 在此基础上定义:

#define WORDS_PER_BLOCK ((MICROPY_BYTES_PER_GC_BLOCK) / MP_BYTES_PER_OBJ_WORD) #define BYTES_PER_BLOCK (MICROPY_BYTES_PER_GC_BLOCK)

4.2 bitmap 跟踪 block 占用

堆上还会分配一个专门结构来跟踪每个 block 的对象分配状态,称为bitmap(位图)。位图用两个 bit 记录每个 block 是"空闲"还是"在用"(即上文 ATB 的 FREE/HEAD/TAIL/MARK 四种状态,每个 ATB 字节管理 4 个 block:BLOCKS_PER_ATB (4))。

Mark-Sweep GC 管理堆上对象的同时也利用这张位图来标记仍在使用的对象,完整实现见 py/gc.c。

4.3 堆布局中的 block 属性

堆被组织为"由 block 组成的 pool(池)",每个 block 可能具有以下属性之一(与文档列表对应):

  • ATB(allocation table byte):若置位,则该 block 是普通 block;
  • FREE:空闲 block;
  • HEAD:一段 block 链的头部;
  • TAIL:处于 block 链的尾部;
  • MARK:被标记的头部 block;
  • FTB(finaliser table byte):若置位,则该 block 带有 finaliser。

从gc_init()的源码(py/gc.c#L109-L138)可以进一步确认池的构成:总内存 = 分配表 + finaliser 表 + pool,且 pool 的结束指针会先对齐到 block 边界:

void gc_init(void *start, void *end) { // align end pointer on block boundary end = (void *)((uintptr_t)end & (~(BYTES_PER_BLOCK - 1))); ... MP_STATE_MEM(gc_pool_start) = (byte *)end - gc_pool_block_len * BYTES_PER_BLOCK; ... }

也就是说,GC 池并非简单占据整个传入区间——它要为分配表和 finaliser 表预留空间,实际可用的 pool 大小由gc_pool_block_len * BYTES_PER_BLOCK决定。这也是gc.mem_free()读数总是小于物理 RAM 的原因。

五、在 TobudOS 中验证内存行为

TobudOS 中 MicroPython 作为独立语言组件编译进 RTOS 应用(参见 examples/micropython_demo 与 board/BearPi_STM32L431RC/KEIL/micropython_basic_demo)。在该移植中,GC 与相关特性均处于开启状态,见 port/mpconfigport.h:

#define MICROPY_ENABLE_GC (1)

在 TobudOS 目标板上运行 MicroPython 后,可以按以下方式验证本文所述的内存管理行为:

>>> import gc >>> gc.isenabled() # 确认自动回收处于开启状态 True >>> free0 = gc.mem_free() # 记录回收前可用字节 >>> big = [list(range(1000)) for _ in range(50)] # 制造一批堆对象 >>> gc.mem_free() # 观察可用内存明显下降 >>> gc.collect() # 手动回收,返回清除的对象数 >>> gc.mem_free() # 再观察内存回升

排查内存问题时的一般方法论:

  1. 用gc.mem_free()/gc.mem_alloc()在关键路径前后取样,定位内存增长位置;
  2. 大对象批量操作前gc.disable(),操作完成后gc.collect()一次性回收,减少回收频次对实时性的影响;
  3. 利用gc.threshold()查看或调整自动回收阈值,避免默认阈值在小堆场景下触发过频。

小结

MicroPython 的自动内存管理建立在三个层次之上:

  • API 层:gc模块(实现于 py/modgc.c)提供 enable/disable/collect/mem_free/mem_alloc/threshold 等运维接口;
  • 对象层:mp_obj_t借助指针低位的 tag 区分小整数、qstr、立即数与堆对象,小整数和立即数免堆分配;
  • 堆层:以 4 字长的 block 为最小单元,由 ATB/FTB 位图跟踪 block 状态,Mark-Sweep GC(实现于 py/gc.c)完成标记与清除。

TobudOS 的 MicroPython 移植(components/language/micropython)选用 REPR_A 对象表示并默认开启 GC,使得上述机制可以直接在 STM32 等 32 位 MCU 上工作。理解这些内部细节后,开发者既能更合理地编写低内存消耗的 Python 应用,也能在嵌入式资源紧张时精确控制回收时机。

【免费下载链接】TobudOS

TobudOS 是面向物联网领域开发的实时操作系统,早期版本基于腾讯自研的物联网操作系统TencentOS Tiny,2020年由腾讯捐赠到开放原子开源基金会进行孵化,2023年正式更名为TobudOS,TobudOS具有低功耗,低资源占用,模块化,安全可靠等特点,可有效提升物联网终端产品开发效率,提供精简的 RTOS 内核,内核组件可裁剪可配置,可快速移植到多种主流 MCU (如 STM32 全系列) 及模组芯片上。而且,基于 RTOS 内核提供了丰富的物联网组件,内部集成主流物联网协议栈(如 CoAP/MQTT/TLS/DTLS/LoRaWAN/NB-IoT 等),可助力物联网终端设备及业务快速接入物联网云平台。

项目地址:https://gitcode.com/openatomfoundation/TobudOS
点击查看免费下载

相关推荐

上一篇:GPT Researcher完整指南:5分钟跑通第一个研究代理,从安装到混合检索实战
下一篇:freeCodeCamp 课程设计解析:用递归分治实现快速排序(Quick Sort)

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询