移植MicroPython时的一些思考
WangGaojie Lv4

今年4月份尝试手动将MicroPython移植到自己的平台上,在移植的过程中发现MicroPython的内部模块有大量的底层依赖,这使得移植工作视乎有点庞大。当时基本上仅完成了BASIC_FEATURES等级的移植,相当于完成了最小系统,在REPL可以良好运行,但是缺少了大量的基础库,使得它几乎没有太大的实用性。

最近我又想起了这个以前遗留的工作,我决定将它完善一些,尽量达到EXTRA_FEATURES的移植等级,这意味着需要大量的移植工作,主要是多线程环境、文件系统、网络协议栈、芯片级外设等内容的移植。

大部分移植工作都可以参考官方的ports例子做参考,但在文件系统、网络、多线程上却只能自己努力了,因为这部分内容是我的系统上特有的。这里说明一点,我将Micropython移植到我的系统中是作为一个单独的模块运行,可随时停止和重启,且它作为独立的线程运行不会对其它线程产生任何影响。

首先网络部分的内容,由于我的本机已经运行了网络协议栈,所以在python中不单独运行协议栈了,通过NIC提供一套socket的控制接口到MicroPython内部就行了,这部分MicroPython考虑的比较周到,可以通过宏定义MICROPY_PORT_NETWORK_INTERFACES来导入自己的socket控制器。设计完自己的socket控制器需要通过mod_network_register_nic()注册到MicroPython内部socket模块。

文件系统要稍微麻烦一些,无法直接接入自己的文件系统,最理想的办法是重构MBFS模块,MBFS文件系统本身作为一个待导入的文件系统模块,对它的修改不会对MicroPython的代码产生太多侵略性的修改。文件系统的接口仍然由系统平台提供,python直接向系统申请文件资源。

这里说一个大坑,困扰我许久,在自定义构造文件对象是需要附带finaliser标记,否则GC回收对象时无法关闭系统中的文件。由于这个MicroPython作为一个子模块运行,它退出后系统还要继续运行,这使得它占用的文件未关闭且部分系统级的内存都没有得到释放,这会导致相当严重的问题。

移植文件系统时需要注意到上下文管理器协议的存在,当执行__exit__()方法后文件已经关闭,但是后续在文件对象销毁时会再次执行__del__()方法来关闭文件,在__exit__()方法中关闭也许不是必要的,但是参考源码中实现的FAT、LFS等文件系统来看,它们的__del__()方法都是执行文件关闭的动作。所以这意味这在设计这部分内容时需要特别注意文件关闭多次执行的问题,需要正确处理文件的打开状态以确保文件不会被执行重复关闭的动作。

对多线程模块的支持仍然花费了相当多的调试精力,主要是在GC机制上费了些功夫。MicroPython的GC仅使用标记清除算法,没有引用计数,所以在标记阶段需要特别注意。

GC机制通过搜索处理器栈上的实例对象来进行标记有效内存区域。对于当前线程来说,除了栈区域,还有CPU中的寄存器也需要进行搜索。当搜索到gc内存对象后就进行标记处理。对于多线程来说,每次标记阶段都需要检查所有线程的栈空间,其它线程的寄存器数据已经压入栈了,所以只需要考虑栈空间即可。获取线程的栈指针地址需要依赖于多线程的底层实现,我这里的多线程是由FreeRTOS提供的,所以通过线程句柄可以访问到其它线程的实时栈指针。在GC标记时需要充分遍历所有线程的栈空间,确保所有的root节点都被正确标记,否则会导致严重的运行时问题。每次执行GC时所有未被标记的内存区域都将被回收,所以不要将一个有用的GC内存放到栈以外的区域,如果有必要保存一些全局gc内存变量,最好是将它放到mp_state_ctx内部进行自动管理,或者是在编写gc_collect()时特殊处理。

MicroPython的GC在回收内存时,如果内存被标记了finaliser,则GC会自动调用对象的析构函数来处理对象。如果没有finaliser,即使对象含有析构函数,在内存回收时析构函数也不会执行。在MicroPython中__del__()方法没有特殊地位,它不会在对象删除时自动执行,这是和CPython的一个明显的区别点。在实现内部模块时需要注意一点,主动执行gc_free()也不会执行finaliser的析构函数,这可能导致一些资源泄露的问题,在实现内部模块时需要特别注意。所以对于内存的管理来说,申请的内存最好不要显式的释放,可以放心的交给GC去处理。

外设的移植完成了Pin模块的部分内容,可以实现GPIO的正常读写,中断的支持还未完成。

到目前为止,除外设功能外已经完成了EXTRA_FEATURES的所有功能,编写了一些程序来运行,基本上符合预期,执行效率上明显慢于C代码。受限与运行时上有限的heap内存空间,多创建一些对象后就会导致内存溢出的问题。通过python间接调用系统层面的文件io和hash模块来实现一个文件md5计算器。计算一个2兆字节的文件大约耗时350ms,而直接在系统层面通过C计算同样的文件md5值,耗时约75ms。还测试了一个大数阶乘计算程序,计算10000的阶乘耗时约2.4s,计算30000的阶乘耗时约22秒,MicroPython内置了大整数计算也是它的优势。