你永远不懂我伤悲,就像白天不懂夜的黑
分类: LINUX
2013-08-19 15:25:37
首相要明确,这里我们需要迁移的系统并非一个应用系统(如通讯网关),而是一个真实的实时操作系统(名称暂时称为 OLDOS)。它有自己的完整的系统构架——内存管理、进程管理、异常处理、系统调用、设备驱动、甚至还有共享库、shell等。总的来说,它麻雀虽小,但五藏据全。作为承载平台,在其上运行着诸多服务和应用程序。
我们的老系统开发于20年前,在那个时代它无疑非常出色,但随着底层硬件越来越丰富和上层应用程序要求也越来越高。老系统对硬件的管理和对进程的管理都逐渐显的捉襟见肘了——因为原先的设计对硬件支持编码缺乏分层结构,体系结构有关和无关代码未能清晰划分,这使得后期换板卡所带来的移植工作量很大;另外系统的进程管理功能和调试功能现在看来比较弱,不能有效管理进程,以及调试进程。
除了技术原因一外,对于维护OLDOS的费用相比维护Linux系统要高许多,原因很简单――Linux已经很普及,因此培养或寻找Linux开发人员要比培养OLDOS人员容易,费用低。纵上所述,选择Linux平台作为操作系统是大势所趋。
注释:曾经也选择过vxworks做操作系统,vxworks在实时控制领域有着良好口碑,但是由于其是商业产品,因此每销售一个我们装有vxworks的板卡,都需要给风河(vxworks的厂商)付费。这点和使用开源操作系统Linux相比很不划算。
本文将下来主要就移植要求和移植基本策略进行分析,希望读者能从中体会系统架构的变换,进而增进对Linux系统的认识。
我们依系统架构层次自下向上开始介绍移植过程吧。
首先是驱动程序部分。OLDOS操作系统原来的架构与Linux相似,也是分为用户空间和内核空间,驱动程序处于内核空间中,控制硬件。还好我们是专用通用系统,没有丰富多彩的外设,因此驱动紧紧限于串口,Flash,以及几种专用的PCI外设。架构迁移到Linux操作系统上后,所有驱动必须进行移植,不过任务量不大,因为串口、Flash等驱动Linux都已经支持得很好,至于几种专有设备必须自己动手移植到Linux下了。并且以后需作为特殊设备文件被应用访问。这里没什么值得多说了。
OLDOS操作系统并不包含文件系统,但它支持检索可执行对象(因此也可理解为一个最简单的文件系统)。因为可执行镜像是直接被存储在内存中,因此执行时不需要向Linux那样载入内存,只需要做动态分配空间,然后建立进程内存映射,便可跳转到入口点执行(这点和vxworks有点像)。
在我们移植到Linux系统后,对象执行方式发生了一些变化——Linux系统下可执行镜像需要以ELF等格式存储在在磁盘(或模拟的块设备,如ramdisk),执行时由加载程序(ld)载入,然后才能为其建立映射,开始执行。因此我们需要将从前的可执行文件做一定修改(需要添加main函数等),并编译成ELF格式存储于磁盘。执行时动态载入并从入口函数(默认为main)执行(利用exec系统调用)。
OLDOS以前的异常处理机制在移植后会不再使用了,因为Linux系统对异常已经做了处理。但是由于OLDOS系统原先支持软件错误管理――所谓错误管理其中最主要的功能便是在出错时刻能将系统状态进行跟踪和保存,以便调试和修正。
在Linux系统中也存在错误管理机制,比如我们常看到的内核错误oops等(是由内核die()函数产生的)便能将错误现场(各种寄存器值,调用链等)反映出来。在OLDOS移植到Linux后,成为用户空间的OLDOS API后(其实就是一个动态库,这点我们以后讲述),仍然需要支持原先的错误管理机制,也就是说仍然要能在上层应用出错误时将其现场保存。(这点和Linux内核中的oops类似,但是实现在用户层)。为了满足这个目的,我们必须能在系统出现异常时,取得系统的现场信息。
而现在问题是,如何在用户空间获得这些寄存器信息(以前可是在内核空间获取的),并在致命错误(不可修复)时,能终止当前应用任务(因为此刻系统继续运行就或造成错误)。第一个要求我们利用了Linux系统(其实所有posix系统几乎都这么做)在发生异常时会向用户空间发送特殊信号(如SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGSEGV,SIGQUIT,SIGINT,SIGTERM)这一桥梁来获取系统现场信息。具体做法是从信号处理函数的sigcontext参数中提取系统寄存器信息(这个参数一般在信号处理函数使用中忽略)。第二个要求则要求我们建立一个高优先级别的守护进程来接收上述信号,之所以用高优先级的进程接收是因为必须抢在出错应用进程运行前处理信号——打印出错现场,终止出错应用进程。 具体实现不进行展开了,点到为止。
原先的OLDOS系统为上层应用提供了很多系统调用,不过在整体被迁移到Linux系统之上后,系统资源不再由OLDOS的系统调用直接操作了,但是为了保持接口访问不便,我们仍需要保留原来的系统调用接口。我们最简单的做法便是将对OLDOS 的系统调用转换成库函数调用。
为了能让所有的应用程序都能访问,且尽量节约系统的存储空间,我们将OLDOS系统调用集体转换成动态库函数。而这些库函数底层实际上又是调用Linux的系统调用或系统库函数。
但这里有一个问题,共享库的数据段是私有的,也就是说如果一个进程改变共享库中的全局变量,那么其它进程将无法看到改变。这是因为共享库的数据段的写时复制(write-on-copy)行为。但是我们的OLDOS中的系统调用却共享了许多数据结构(以前在内核里共享这些数据结构,如进程控制块TCB等),这些数据结构必须在迁移后必须仍能被各个进程访问。
为此,我们借助了共享内存存储这些需全局访问的共享变量。具体做法是――系统初起时,用一个辅助进程创建一段共享内存区存储全局共享变量,然后将其挂接到我们的应用程序空间中。如此一来,所有的进程便都可访问到同一个变量拷贝了。当然为了同步访问,共享变量需要加锁保护。
库的移植方法和OLDOS系统调用类似,最后都被封装成为Linux的共享库(*.so)。这里有许多细节问题,限于篇幅,不作介绍了。
我们系统主要考核的性能要求是系统的实时性。原先的OLDOS操作系统为满足实时性在设计上体现出很强的“受限环境”,所谓受限环境指的是对系统的运行环境进行限制――比如每个进程使用的堆栈大小固定,进程内存分配都在创建时刻分配,不允许动态分配。而我们知道Linux系统的设计基于通用系统,任何程序都可运行,因此进程运行方式对实时性来讲有几点不适合:
u 进程镜像按需动态载入——运行过程的载入难免影响任务执行的确定性。
u 进程堆栈分配大小不固定,按需动态分配。
u 进程可动态分配内存。
为此,我们采取了以下措施纠正上述问题。
进程镜像一次载入――要使用mlockall调用载入且锁住内存)
进程栈大小固定―― 通过rlimit限制进程栈大小,且在进程创建时就初运行一段会占用堆栈的代码来扩展栈空间(由于锁住了内存,所以在进程运行期间堆栈空间都不会释放)
进程堆空间分配一次完成――在进程创建时刻分配一固定大小的内存池用于所有进程的堆分配。即每个任务都有自己固定大小的堆空间,且已经分配就绪。
除了上述改进外,还要给Linux内核大上实时补丁(主要时内核抢占补丁)。如此依赖系统的实时性将获得保证。
Linux系统由于设计时主要考虑的是服务器应用,因此对桌面应用和嵌入应用某些方面显得有些先天不足,比如实时性不很强,占用资源比较大,界面不够优美等等。但是随着越来越多的开发者加入和越来越广的应用,Linux系统的发展如今开始向桌面和嵌入领域倾斜,尤其是嵌入领域。很多性能补丁被不断加入到内核,系统的实时性和稳定性稳步提高,相信在不久嵌入通讯领域必将有Linux一席之地。