阅读micropython源码-内存管理组件GC

初探micropython中的内存管理机制
阅读micropython源码,从main()函数入手。在main()函数中,除了初始化功能组件,例如board_init()、tusb_init()和led_init()外,在正式初始化micropython之前,对mp_stack和gc进行了初始化,这两个组件操作了来自链接命令文件的四个保存了内存指针的变量。我猜想mp_stack和gc分别是管理micropython内部的栈和堆内存空间,但同时C运行环境还会使用系统默认的堆栈空间,那么,包含micropython的系统到底是如何管理内存的呢?
 

extern uint8_t _sstack, _estack, _gc_heap_start, _gc_heap_end;

int main(void) {
    board_init();
    tusb_init();
    led_init();

    mp_stack_set_top(&_estack);
    mp_stack_set_limit(&_estack - &_sstack - 1024);

    for (;;) {
        gc_init(&_gc_heap_start, &_gc_heap_end);
        mp_init();
        ...
        gc_sweep_all();
        mp_deinit();
    }

    return 0;
}

分析指定的内存相关参数
原本我是以mimxrt作为具体的移植对象进行阅读,但是mimxrt的linker file描述比较复杂,涉及到片内的好几块内存和外扩内存,所以最终我选择使用samd21q18a_flash.ld文件作为参考分析对象。samd21q18a_flash.ld描述的内存分配模型非常简单,全部使用片内FLASH和片内RAM,这也是一个典型的MCU内存分配系统:
 

/* Memory Spaces Definitions */
MEMORY
{
  rom      (rx)  : ORIGIN = 0x00000000, LENGTH = 0x00040000
  ram      (rwx) : ORIGIN = 0x20000000, LENGTH = 0x00008000
}

其中关于Stack的定义如下:

/* The stack size used by the application. NOTE: you need to adjust according to your application. */
STACK_SIZE = DEFINED(STACK_SIZE) ? STACK_SIZE : DEFINED(__stack_size__) ? __stack_size__ : 0x2000;

/* Section Definitions */
SECTIONS
{
    ...
    /* stack section */
    .stack (NOLOAD):
    {
        . = ALIGN(8);
        _sstack = .;
        . = . + STACK_SIZE;
        . = ALIGN(8);
        _estack = .;
    } > ram
    ...
}

但是“_gc_heap_start”和“_gc_heap_end”是mimxrt所独有的,所以只能硬着头皮看mimxrt的linker file。

MEMORY
{
  m_flash_config (RX) : ORIGIN = flash_config_start,    LENGTH = flash_config_size
  m_ivt          (RX) : ORIGIN = ivt_start,             LENGTH = ivt_size
  m_interrupts   (RX) : ORIGIN = interrupts_start,      LENGTH = interrupts_size
  m_text         (RX) : ORIGIN = text_start,            LENGTH = text_size
  m_vfs          (RX) : ORIGIN = vfs_start,             LENGTH = vfs_size
  /* Teensy uses the last bit of flash for recovery. */
  m_reserved     (RX) : ORIGIN = (vfs_start + vfs_size), LENGTH = reserved_size  
  m_itcm         (RX) : ORIGIN = itcm_start,            LENGTH = itcm_size
  m_dtcm         (RW) : ORIGIN = dtcm_start,            LENGTH = dtcm_size
  m_ocrm         (RW) : ORIGIN = ocrm_start,            LENGTH = ocrm_size
}

其中m_itcm、m_dtcm和m_ocrm是3个独立的片内RAM,itcm和dtcm是高速RAM,从后续的代码中可以看到,itcm专用于存放ram_func,dtcm存放了data段、bss段、heap段(系统堆)、stack段(系统栈),其中stack为dtcm的末尾。ocrm是独立的低速RAM。
 

  __StackTop   = ORIGIN(m_dtcm) + LENGTH(m_dtcm);
  __StackLimit = __StackTop - STACK_SIZE;
  PROVIDE(__stack = __StackTop);

而在“MIMXRT1011.ld”文件中定义了main.c引用的4个关于内存的变量:

ocrm_start          = 0x20200000;
ocrm_size           = 0x00010000;

/* 20kiB stack. */
__stack_size__ = 0x5000;
_estack = __StackTop;
_sstack = __StackLimit;

/* Do not use the traditional C heap. */
__heap_size__ = 0;

/* Use second OCRAM bank for GC heap. */
_gc_heap_start = ORIGIN(m_ocrm);
_gc_heap_end = ORIGIN(m_ocrm) + LENGTH(m_ocrm);

_estack和_sstack就是系统栈,_gc_heap_start和_gc_heap_end代表了整个ocrm的空间。现在看起来mp_stack就是接管了系统栈,而gc是用了系统堆之外额外的一块独立空间。此处猜测,mp_stack仅仅是观察系统栈的使用情况,不大可能直接操作系统栈,因为系统栈中还会包含micropython之外的硬件自动压栈的信息,这些内容是不能让micropython随便操作的,但可以让micropython读,以获取某些程序运行时的信息。gc可能对应micropython的动态内存分配机制,用掉了整块ocrm的64KB内存,可能对应某些malloc()和free()函数的实现。

关于mp_stack的分析,将在另一篇文章中详述,本文将重点追溯gc的实现代码。

通用Python的GC垃圾收集机制
实际上,关于GC,经过多次试探性地阅读micropython源码,我已经有了一知半解的概念。GC的全名是垃圾收集器Garbage Collector,是Python标准实现的一个概念,是Python中管理内存的一个功能组件。

参考《python 垃圾回收器_Python 垃圾回收机制》文章的介绍:
https://blog.csdn.net/weixin_35853363/article/details/112933690

Python中的垃圾回收是以引用计数为主,分代收集为辅。
在Python中,如果一个对象的引用数为0,Python虚拟机就会回收这个对象的内存。
 

classA:
    def __init__(self):
        self.t=None
        print 'new obj, id is %s' %str(hex(id(self)))

    def __del__(self):
        print 'del obj, id id %s' %str(hex(id(self)))

if __name__ == '__main__':
    while True:
    a1=A()
    del a1

运行如上代码,进程占用的内存基本不会变动:

new obj, id is 0x2a79d48L
del obj, id id 0x2a79d48L

a1 = A() 会创建一个对象,在0x2a79d48L内存中,a1变量指向这个内存,这时候这个内存的引用计数是1。del a1后,a1变量不再指向0x2a79d48L内存,所以这块内存的引用计数减一,等于0,所以就销毁了这个对象,然后释放内存。

导致引用计数+1的情况:

对象被创建,例如a=23
对象被引用,例如b=a
对象被作为参数,传入到一个函数中,例如func(a)
对象作为一个元素,存储在容器中,例如list1=[a,a]
导致引用计数-1的情况:

对象的别名被显式销毁,例如del a
对象的别名被赋予新的对象,例如a=24
一个对象离开它的作用域,例如f函数执行完毕时,func函数中的局部变量(全局变量不会)
对象所在的容器被销毁,或从容器中删除对象
从main.c入手分析micropython中的gc组件
在main()函数中有两处调用gc的函数:
gc_init(&_gc_heap_start, &_gc_heap_end);
gc_sweep_all();

在main.c文件中还有一个单独的gc_collect(),谁会调用这个函数?

gc_init()
先跟到gc_init()函数中,gc接管的整块gc_heap被分为了gc_alloc_table、gc_finaliser_table和gc_pool。
 

void gc_init(void *start, void *end) {
    // calculate parameters for GC (T=total, A=alloc table, F=finaliser table, P=pool; all in bytes):
    // T = A + F + P
    //     F = A * BLOCKS_PER_ATB / BLOCKS_PER_FTB
    //     P = A * BLOCKS_PER_ATB * BYTES_PER_BLOCK
    // => T = A * (1 + BLOCKS_PER_ATB / BLOCKS_PER_FTB + BLOCKS_PER_ATB * BYTES_PER_BLOCK)
    ...
    MP_STATE_MEM(gc_alloc_table_start) = (byte *)start;
    ...
    MP_STATE_MEM(gc_finaliser_table_start) = MP_STATE_MEM(gc_alloc_table_start) + MP_STATE_MEM(gc_alloc_table_byte_len);
    ...
    MP_STATE_MEM(gc_pool_start) = (byte *)end - gc_pool_block_len * BYTES_PER_BLOCK;
    MP_STATE_MEM(gc_pool_end) = end;
    ...
    assert(MP_STATE_MEM(gc_pool_start) >= MP_STATE_MEM(gc_finaliser_table_start) + gc_finaliser_table_byte_len);
}

gc_alloc_table和gc_finaliser_table记录内存块使用的情况,gc_pool就是内存块的物理存放空间。

再看gc.h文件,果不其然,这里还定义了典型的内存管理API:

void *gc_alloc(size_t n_bytes, unsigned int alloc_flags);
void gc_free(void *ptr); // does not call finaliser
size_t gc_nbytes(const void *ptr);
void *gc_realloc(void *ptr, size_t n_bytes, bool allow_move);

在程序中可以使用gc_alloc()和gc_free(),以及另外的内存分配函数,在gc管辖的内存区域中申请和释放内存。若gc中的某些个内存块的引用数为0时(变成了“野指针”),在系统调用gc_collect()时,即可自动回收内存,而不用显式调用gc_free()。这种case是为了应对Python中能够实现动态创建内存并在自动回收已经不再使用的内存,防止过早地出现内存溢出。

gc_collect()
至于gc_collect()函数的实现,这里也有一个trick。
 

// A given port must implement gc_collect by using the other collect functions.
void gc_collect(void);
void gc_collect_start(void);
void gc_collect_root(void **ptrs, size_t len);
void gc_collect_end(void);

正如代码中的注释说明,gc_collect()函数需要在移植代码中实现,并且在其中调用另外三个gc_collect_xxx()函数。gc_collect_start()和gc_collect_end()传入和传出的参数都是void,猜测是要根据具体的移植情况,通过gc_collect_root()函数传入符合不同策略的参数。例如,在mimxrt移植实现的main.c函数中,就有如下代码:
 

void gc_collect(void) {
    gc_collect_start();
    gc_helper_collect_regs_and_stack();
    gc_collect_end();
}

咦,这个gc_helper_collect_regs_and_stack()是什么鬼,竟然没有参数,看起来也是一个通用实现。跟进去看一下。gc_helper_collect_regs_and_stack()函数在“lib/utils/gchelper.h”文件中声明,跟arm cortex-m架构相关的代码如下:
 

typedef uintptr_t gc_helper_regs_t[10];

void gc_helper_collect_regs_and_stack(void);

在lib/utils/gchelper_generic.c文件中关于arm cortex-m架构的相关代码如下:

// Fallback implementation, prefer gchelper_m0.s or gchelper_m3.s

STATIC void gc_helper_get_regs(gc_helper_regs_t arr) {
    register long r4 asm ("r4");
    register long r5 asm ("r5");
    register long r6 asm ("r6");
    register long r7 asm ("r7");
    register long r8 asm ("r8");
    register long r9 asm ("r9");
    register long r10 asm ("r10");
    register long r11 asm ("r11");
    register long r12 asm ("r12");
    register long r13 asm ("r13");
    arr[0] = r4;
    arr[1] = r5;
    arr[2] = r6;
    arr[3] = r7;
    arr[4] = r8;
    arr[5] = r9;
    arr[6] = r10;
    arr[7] = r11;
    arr[8] = r12;
    arr[9] = r13;
}

// Explicitly mark this as noinline to make sure the regs variable
// is effectively at the top of the stack: otherwise, in builds where
// LTO is enabled and a lot of inlining takes place we risk a stack
// layout where regs is lower on the stack than pointers which have
// just been allocated but not yet marked, and get incorrectly sweeped.
MP_NOINLINE void gc_helper_collect_regs_and_stack(void) {
    gc_helper_regs_t regs;
    gc_helper_get_regs(regs);
    // GC stack (and regs because we captured them)
    void **regs_ptr = (void **)(void *)&regs;
    gc_collect_root(regs_ptr, ((uintptr_t)MP_STATE_THREAD(stack_top) - (uintptr_t)&regs) / sizeof(uintptr_t));
}

gc_helper_collect_regs_and_stack()函数内部首先定义了一个指向寄存器组的指针,然后通过汇编语言实现的gc_helper_get_regs()函数拿到当前CPU内部寄存器的值。这里注意,gchelper_m0.s和gchelper_m3.s实现的函数是gc_helper_get_regs_and_sp(),并未在此处调用。然后把这些(压入系统栈内的)寄存器一并送入到gc_collect_root()函数中。
 

void gc_collect_root(void **ptrs, size_t len) {
    for (size_t i = 0; i < len; i++) {
        void *ptr = gc_get_ptr(ptrs, i);
        if (VERIFY_PTR(ptr)) {
            size_t block = BLOCK_FROM_PTR(ptr);
            if (ATB_GET_KIND(block) == AT_HEAD) {
                // An unmarked head: mark it, and mark all its children
                TRACE_MARK(block, ptr);
                ATB_HEAD_TO_MARK(block);
                gc_mark_subtree(block);
            }
        }
    }
}

此时再看gc_collect_root()函数的实现,顾名思义,只是一个执行collect的入口。首先遍历栈中的每个指针,通过VERIFY_PTR()函数查验其是否为gc管理的内存资源(位于gc_pool中),若是,则找到这块内存所对应的block,若这个block被标记为“AT_HEAD”,说明目前定位到一串已经分配但不再使用的内存块链表,则在gc_alloc_table表中标记该块和挂在它后面的字块,表示它们可以被重新分配使用。

gc_sweep_all()
这里顺便还看到一个常用的函数gc_deal_with_stack_overflow(),这个函数在gc_sweep_all()中被调用了,也顺便分析一下。这个函数的功能在于,当出现内存溢出的情况下(堆溢出而不是字面上的栈溢出,gc只能管理堆不能管理栈),从头开始逐个扫描整个gc_alloc_table,看能不能找到碎片的内存(没有子块的内存块),然后试图把它们重新组织起来。之后可由调用环境再试图通过gc_alloc()申请gc_pool中的内存,由一定记录之前申请不到的(指定较大尺寸的)内存块现在就能申请到了。
 

STATIC void gc_deal_with_stack_overflow(void) {
    while (MP_STATE_MEM(gc_stack_overflow)) {
        MP_STATE_MEM(gc_stack_overflow) = 0;

        // scan entire memory looking for blocks which have been marked but not their children
        for (size_t block = 0; block < MP_STATE_MEM(gc_alloc_table_byte_len) * BLOCKS_PER_ATB; block++) {
            // trace (again) if mark bit set
            if (ATB_GET_KIND(block) == AT_MARK) {
                gc_mark_subtree(block);
            }
        }
    }
}

结论
到此,对micropython中的gc已经有了一个大体的了解:

main()函数调用的gc_init()之后,将一大块内存器交给gc管理,实际存放用户数据的存储区在gc_pool,gc通过gc_alloc_table管理gc_pool中以block组织起来的内存块的使用情况。
micropython内核可以通过gc_alloc()和gc_free()等函数从gc_pool中申请使用内存块
micropython内核可以在合适的时机调用gc_collect()函数回收已分配但不再使用的内存块。至于内存如何变成已分配但未使用的状态,可参见上文中摘要的Python垃圾回收机制。
gc_collect()函数的实现依赖于具体的CPU架构,需要根据移植平台实现,中间涉及到获取系统栈中CPU寄存器值及监管范围的操作,不同的处理器有所区别,所以需要用户实现,但大部分通用操作已经由gc_collect_xxx()的其它函数实现了,所以用户在具体的移植中实现gc_collect()时,可调用其它gc_collect_xxx()函数完成大部分功能。
在有必要的情况下,如果要了解gc中的内存分配机制,可再具体详读gc_alloc()和gc_free()函数及相关函数的实现代码。

另外,关于micropython工程的内存管理,此处也可以有一个结论。micropython工程有三块内存:系统栈、系统堆和micropython堆(gc)。系统堆是C编译器管理,micropython堆是micropython通过gc组件管理,而系统栈是由C编译器管理,但仍被micropython监视。

关于可能与系统栈有关的mp_stack组件的分析,请见下文分解。
 

  • 20
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值