蔡富强,郭兵,沈艳
(1.四川大学 计算机学院,成都610065;2.电子科技大学)
蔡富强(硕士生),主要研究方向为嵌入式实时系统;郭兵(教授),主要研究方向为嵌入式实时系统、SoC和中间件;沈艳(副教授),主要研究方向为虚拟仪器、分布式测试。
嵌入式系统是一个专用的计算机系统,它在现实生活中有着极其广泛的用途,大到飞机、宇宙飞船,小到各种手持设备(如手机),都是嵌入式系统的具体应用。可以说,嵌入式系统已经渐渐融入我们的生活,变得不可或缺。这种趋势将随着物联网[1]的推广变得更加明显。
嵌入式系统的一个比较关键的技术就是嵌入式操作系统。目前常用的嵌入式操作系统有 Linux、μC/OS[2]、WinCE、VxWorks、Symbian等,其中μC/OS以其开放源码、高效而小巧,同时又兼具实时性等特点得到了比较广泛的应用。
μC/OS中只实现了操作系统的一些基本功能(带优先级可抢占的进程管理和一个比较简单的内存管理方案),这是它只有几千行代码的基本原因。正因为如此,它并不适用于所有嵌入式应用,尤其是面向高端的应用。为了拓展μC/OS的应用范围,很多改进措施被提出。一些学者为μC/OS添加功能模块,以拓展其应用,如TCP/ⅠP协议栈、GUⅠ、文件系统等,参考文献[3]为μC/OS添加内存文件系统,参考文献[4]为μC/OS添加基于FAT的Flash文件系统;也有学者改进μC/OS的基本模块,以满足新的需求,参考文献[5]改进了μC/OS的任务调度模块,参考文献[6]解决μC/OS消息队列数据通信安全问题;参考文献[7]扩展μC/OS到可重构系统,使其支持软硬件任务的统一调度。
本文也以拓展μC/OS应用为目的,为μC/OS添加加载外部可执行程序支持,同时也提出一种内存分配方案,以实现为程序映像和栈高效分配内存。由于目前在嵌入式领域应用最多的处理器为ARM系列,本功能的实现针对ARM平台。
μC/OS内核实现的内存分配方案支持多种大小的内存块,但不同大小内存块之间独立管理,这种实现使用不方便,不能实现动态而高效的内存分配。本文提出一种独立的内存管理机制,以实现为程序映像和栈高效分配内存。
实现参考了Linux内核的分区页面管理算法[8]。将全局内存划分为以512字节为单位的块,并为每个内存块建立描述符,通过描述符来管理内存块。内存块按块数的几何级数组织成多级链表,第一级用于分配1个块,第二级用于分配地址连续的2个块,第三级用于分配地址连续的4个块,依次类推。由连续内存块的首块内存的描述符参与链表链接。将实现下面几个函数,具体描述见1.2小节。
实现该内存管理,需要一些全局变量和数据结构定义。ⅠNT8U*pmem指向静态分配的全局内存。块描述符的定义如下:
BLOCK_DESC表示内存块描述符,每个内存块都有一个内存描述符,并且从前到后一一对应。next和prev用于多级链表的链接;order的最高位表示该连续的内存块序列是否被使用,其余位表示所属链表级数。链表级数根据全局内存大小自动调整,在嵌入式应用中一般不超过10(能分配最大512KB的连续内存),用ⅠNT8Ugorder表示链表的级数。BLOCK_DESC*desc_begin表示块描述符的首地址。void*block_begin表示内存块的首地址。BLOCK_DESC*desc_list[11]用于链接多级链表。
DescToBlock函数实现从内存块描述符地址到内存块地址的转换。由于块描述符与内存块依次对应,只要有两者的内存首地址,就很容易实现。具体实现只需要下面的表达式:return(char*)block_begin+ (desc-desc_begin)<<9。BlockToDesc函数功能正好相反,实现方式类似。
BlockMemⅠnit函数完成该内存管理方案的初始化。首先,根据提供的全局内存的大小确定多级链表级数,最多为10级,并初始化全局变量gorder,内存块数为size/(512+sizeof(BLOCK_DESC));然后,根据全局内存能够容纳的内存块数将其分为两部分,前面用于存储所有的块描述符(将块描述符清零),后面部分就是用于分配的内存块,并初始化全局变量block_begin和desc_begin;最后,将内存块按最大化的原则分配于多级链表中,理想情况下所有的内存块都分配于所支持的最大级链表中,大小不满足的则分配到低级别的链表中(仍须按最大化原则)。
图1表示初始化后的内存布局(虚线表示不一定存在)。
GetBlocks函数用于分配(1<<order)个连续的内存块。算法描述如下:
① 检查参数order是否有效,无效返回null,有效继续;
②检查order级链表是否有空闲内存,没有转下一步,否则从链表中取出第一块,修改描述符的使用标志位,通过函数DescToBlock获取内存地址并返回;
③ 从更高级别获取空闲内存,如果获取失败,返回null,否则,逐级分裂直到order级链表,并修改首个描述符的所属链表级数,转第二步。
图1 初始化后的内存布局
FreeBlocks函数用于释放指定地址处开始的(1<<order)个连续的内存块。该函数是实现高效内存分配的关键,为了保证内存不会被分得过碎,需要迭代检查释放内存是否可以同相邻的内存合并(看描述符order字段是否相同)。算法描述如下:
①通过函数BlockToDesc获取块描述符的地址,将描述符设置为未使用;
② 检查order级链表中是否有与待释放内存相邻的内存(前后都有相邻的选前面的参与合并)。如果没有相邻的,转第③步。否则,将相邻内存的首个内存块描述符从链表中删除,修改相关的两个块描述符的order字段(在前面的加1,后面的清零),参数order加1,继续执行第②步;
③ 将内存插入order级链表中。
要实现外部程序加载,需要操作系统和编译器的密切配合,整个过程非常复杂。为了使程序加载器尽可能简单和高效,采用下面的措施:
① 修改编译μC/OS内核的Makefile文件,使内核链接地址和加载地址相同,通过NM工具导出内核函数地址表。将地址表以函数指针的形式组织在头文件中,另外还须在头文件中加入内核中关于数据类型和结构的定义,供外部程序使用。以本文第一部分中的BlockMemⅠnit函数为例,假设函数地址位于0x00008000,在头文件中作如下声明即可:
② 使main函数的代码位于程序映像的入口处(为简单起见,main函数不支持参数)。只需采用下面措施即可。编程时保证main函数是所在文件中第一个被实现的函数,修改LD链接脚本使包含main函数的目标文件的代码段位于整个可执行程序的开始。
③通过OBJCOPY工具将链接生成的ELF格式的可执行程序转化为二进制映像。
④ 为μC/OS添加函数void Exec(char*name),用于加载二进制映像,并为其创建进程。
通过以上措施,加载器的实现非常简单,只需为二进制映像分配内存并将其加载进内存,然后调用函数OSTaskCreate(二进制映像加载地址(main),null,通过函数 GetBlocks分配的栈空间,优先级)即可,由于ARM平台没有直接内存寻址模式,二进制映像不需要重定位就可执行。图2为Exec函数的流程。
图2 Exec函数执行流程
测试时采用S3C2440系列开发板作为目标平台。首先,修改u-boot,以使其能够加载μC/OS到指定地址处,并完成内核的引导工作;其次,移植μC/OS,使其能够在S3C2440系列开发板上运行;然后,按参考文献[3]或[4]中提供的方法为μC/OS实现文件系统支持;最后,按本文中的方法实现二进制映像加载功能。经测试,该方案是可行的。当然,该方案也存在一定的局限性,如只针对ARM平台,内存分配方案还不能通用(对小内存块分配利用率低),又由于μC/OS内核不支持保护模式,安全性也是一个问题。
[1]王保云.物联网技术研究综述[J].电子测量与仪器学报,2009,23(12):1-7.
[2]Labrosse Jean J.嵌入式实时操作系统μC/OS-ⅠⅠ[M].邵贝贝,等译.2版.北京航空航天大学出版社,2003.
[3]张红兵.大容量内存文件系统设计及μC/OS下的实现[J].单片机与嵌入式系统应用,2004(3):15-20.
[4]王命延.一种加载在μC/OS-ⅠⅠ内核上的嵌入式文件系统[J].南昌大学学报:理科版,2005,29(2):197-204.
[5]张旭.μC/OS ⅠⅠ内核任务调度模块的分析与改进[J].单片机与嵌入式系统应用,2005(4):71-76.
[6]曾蜀芳.μC/OS-ⅠⅠ中消息队列通信的数据安全问题[J].计算机技术与发展,2009,19(8):151-154.
[7]周博.SHUM-UCOS:基于统一多任务模型可重构系统的实时操作系统[J].计算机学报,2006,29(2):208-218.
[8]Bovet Daniel P,Cesati Marco.深入理解Linux内核[M].陈莉君,等译.2版.北京:中国电力出版社,2004:223-268.