阅读micropython源码-内存管理组件GC
阅读micropython源码-内存管理组件GC
苏勇,2021年8月
文章目录
- 阅读micropython源码-内存管理组件GC
- 初探micropython中的内存管理机制
- 分析指定的内存相关参数
- 通用Python的GC垃圾收集机制
- 从main.c入手分析micropython中的gc组件
- gc_init()
- gc_collect()
- gc_sweep_all()
- 结论
相关源文件:
- ports/mimxrt/main.c
- py/gc.h
- py/gc.c
- lib/utils/gchelper.h
- lib/utils/gchelper_generic.c
- lib/utils/gchelper_m0.s
- lib/utils/gchelper_m3.s
- lib/utils/gchelper_native.c
初探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 = 0x00040000ram (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_sizem_ivt (RX) : ORIGIN = ivt_start, LENGTH = ivt_sizem_interrupts (RX) : ORIGIN = interrupts_start, LENGTH = interrupts_sizem_text (RX) : ORIGIN = text_start, LENGTH = text_sizem_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_sizem_dtcm (RW) : ORIGIN = dtcm_start, LENGTH = dtcm_sizem_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=Noneprint '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.sSTATIC 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 *)®s;gc_collect_root(regs_ptr, ((uintptr_t)MP_STATE_THREAD(stack_top) - (uintptr_t)®s) / 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 childrenTRACE_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 childrenfor (size_t block = 0; block < MP_STATE_MEM(gc_alloc_table_byte_len) * BLOCKS_PER_ATB; block++) {// trace (again) if mark bit setif (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组件的分析,请见下文分解。
阅读micropython源码-内存管理组件GC相关推荐
- 详细讲解Linux内核源码内存管理(值得收藏)
Linux的内存管理是一个非常复杂的过程,主要分成两个大的部分:内核的内存管理和进程虚拟内存.内核的内存管理是Linux内存管理的核心,所以我们先对内核的内存管理进行简介. 一.物理内存模型 物理内存 ...
- 从9个组件开始,教你如何高效的阅读nginx源码?
从9个组件开始,教你如何高效的阅读nginx源码?|内存池.线程池.内存共享组件实现. http处理流程.phase原理.红黑树.配置文件.惊群.原子操作 专注于服务器后台开发,包括C/C++,Lin ...
- 阅读 ANDROID 源码的一些姿势
日常开发中怎么阅读源码 找到正确的源码 IDE是日常经常用的东西,Eclipse就不说了,直接从Android Studio(基于IntelliJ Community版本改造)开始. 我们平时的And ...
- 阅读 Android源码的一些姿势
日常开发中怎么阅读源码 找到正确的源码 IDE 是日常经常用的东西,Eclipse 就不说了,直接从 Android Studio(基于 IntelliJ Community 版本改造)开始. 我们平 ...
- 阅读Android源码的一些姿势
2019独角兽企业重金招聘Python工程师标准>>> 前面吐槽了 有没有必要阅读Android源码,后面觉得只吐槽不太好,还是应该多少弄点干货.需要说明的是,Android每个系统 ...
- 阅读micropyton源码-添加C扩展类模块(2)
阅读micropyton源码-添加C扩展类模块(2) 苏勇,2021年8月 文章目录 阅读micropyton源码-添加C扩展类模块(2) 看看machine_pin_type实例的定义 特别说明 看 ...
- 阅读react-redux源码 - 一
阅读react-redux源码 - 零 阅读react-redux源码 - 一 阅读react-redux源码(二) - createConnect.match函数的实现 阅读react-redux源 ...
- 阅读react-redux源码 - 零
阅读react-redux源码 - 零 阅读react-redux源码 - 一 阅读react-redux源码(二) - createConnect.match函数的实现 react的技术栈一定会遇到 ...
- 手把手带你阅读Mybatis源码(三)缓存篇
点击上方"Java知音",选择"置顶公众号" 技术文章第一时间送达! 前言 大家好,这一篇文章是MyBatis系列的最后一篇文章,前面两篇文章:手把手带你阅读M ...
最新文章
- sepFilter2D函数
- go语言按行读取文件
- js isinteger_在JavaScript中使用示例使用Number isInteger()方法
- asp.net 开发知识小结【转】
- regexbuddy使用记录
- vue中watch数组或者对象
- InteliJ Idea通过maven创建webapp
- 软件工程期末设计(校园教务系统)
- BP 神经网络的非线性系统建模——非线性函数拟合
- 企事业单位 固定资产管理系统
- Web功能测试(邮箱,手机号,验证码,身份证号测试用例)
- Android前景与未来趋势
- 常用的web服务器软件有哪些
- 父子组件传值之(子传父)
- SAP MM 采购申请列表选择条件说明
- mysql sql stuff函数_数据库SQLServer Stuff函数用法
- Oracle数据库,建库建表
- wiki中文语料的word2vec模型构建
- python ctypes详解-CTypes
- Process Scrum
热门文章
- w10电脑c盘满了怎么清理_win10系统如何清理c盘空间容量
- 计算机辅助设计ca,《AutoCA计算机辅助设计》课程标准.doc
- visual studio 2010教程-创建网站项目
- AlexNet分类Fashi-MNIST(Pytorch实现)
- mimikatz免杀过360和火绒
- 2020南京大学软件工程考研上岸感想
- 南大计算机博士黄鑫,南京大学软件学院张贺教授团队在经验软件工程方法学研究中取得重要成果...
- 用 bat 批处理命令启动 Android Studio 自带模拟器
- 科技文献检索(十一)——常用文摘型数据库
- 计算机网络英文论文,计算机网络与因特网论文(英文版).doc