Bill Weinberg, MontaVista 软件
Linux暴风雨般地占领了嵌入式系统市场。根据工业分析家分析,大约1/3到1/2的新的32位和64位嵌入式系统设计采用了Linux。嵌入式 Linux 已经在很多应用领域显示出优势,比如SOHO家庭网络和成像/多功能外设,并在以下几方面具备巨大的跨越式发展前景:(NAS/SAN)存储,家庭数字娱乐(HDTV/PVR/DVR/STB)和手持设备/无线设备,特别是数字移动电话。
新的嵌入式Linux应用不会象掌握在智慧和工艺之神-罗神手中那样,会突然从开发者的头脑中爆发出来。大量的项目必须采用数千行的,甚至数百万行的过去的现成代码。成百上千的嵌入式项目已经成功地将其它平台的现成代码移植到Linux之上,比如Wind River VxWorks 和 pSOS, VRTX, Nucleus 和其它RTOS ,这些移植工作现在仍然有价值和现实意义。
到目前为止,大多数的关于移植旧的RTOS应用到嵌入式Linux的文献,已经在关注RTOS 接口(API),任务,调度模式和怎样将他们映射到相应的用户空间去。 在嵌入式程序的密集I/O空间中,同样重要的是,将RTOS的应用硬件接口代码向具有更加规范化模式的Linux设备启动程序的移植。
本文将纵览几种常用的内存映射I/O方法,它们经常出现于旧的嵌入式应用中。它们涵盖的范围,包括从对中断服务例程的特殊使用和用户线程对硬件访问,到出现于有些ROTS中的半规范化驱动程序模型。它对于移植RTOS 代码到规范化模式的Linux设备启动程序具有启发性,并且介绍了一些方法。特别地,本文会重点讨论和比较RTOS代码中的内存映射,Linux基于I/O调度队列的移植,和重新定义RTOS I/O,以便在本地Linux 驱动程序和守护进程里应用。
RTOS I/O 概念
“不规范”是能够描述大多数在基于RTOS系统里的I/O的最佳词语。大多数RTOS针对较早的无MMU的CPU而设计,忽略了内存管理,即使当MMU问世也是这样,不区分物理地址和逻辑地址。大多数 RTOS还全部在特权态(系统模式)运行,表面上看增强了性能。像这样,全部的RTOS 应用和系统代码都能够访问整个机器地址空间,内存映射设备和I/O指令。实际上,将RTOS应用程序代码同驱动程序代码区分开非常困难,即使它们是有差别的。
这个不规范的结构导致了I/O的特殊实现。在很多情况下,完全缺乏对一种设备驱动程序模型的认同。根据这种工作的平等和没有分层的特性,回顾在基于RTOS软件中使用的一些重要概念和实践非常有指导意义:
在线内存映射访问
当在上个世纪八十年代中期商业化的RTOS产品可以买到的时候,大多数嵌入式软件包含巨大的主循环,主循环带有针对严格时间操作的注册I/O和中断服务例程。开发人员将RTOS和执行程序设计进他们的项目,主要为了加强同时性和帮助多任务同步,但是避开其它任何有“妨碍“的构造。同样地,即使一个RTOS提供了I/O 调用形式方法,嵌入式程序员继续使用直接的I/O操作:
#define DATA_REGISTER 0xF00000F5
char getchar(void) {
return (*((char *) DATA_REGISTER)); /* read from port */
}
void putchar(char c) {
*((char *) DATA_REGISTER) = c; /* write to port */
}
多数受过训练的开发者常常将这样的直接I/O代码从硬件代码独立分离开。但是我还曾遇见大量的意大利面条式的I/O处理代码。
当普遍深入使用直接内存映射I/O的时候,对Linux开始接触的嵌入式开发人员总是面临将所有的这类代码移植到用户空间,将定义寄存器地址的#define 语句转换成mmap()调用. 这种处理方法对于一些种类的原型很好,但是不能支持中断处理,限制了实时响应,特别不安全,不适合作为商业发布。
RTOS 中断服务例程
在 Linux中, 中断服务专属于内核的范围。 在一个 RTOS中, 中断服务例程代码是自由形态而且与应用程序代码没有区别(不外乎返回序列)。很多 RTOS提供系统调用或者宏,来让代码自己检测它自己的切换点(比如 Wind River VxWorks的 intContext())。中断服务例程通常也使用标准的库函数,伴随着可重入性和可移植性问题。
大多数RTOS支持注册中断服务例程代码,中断仲裁句柄和中断服务例程调度。一些非常原始的嵌入式执行程序,仅仅支持在硬件矢量表里插入中断服务例程的开始地址。
即使你试图直接在用户程序空间执行读和写的操作,你不得不将你的Linux中断服务例程放入内核程序空间。
RTOS I/O 子系统
大多数RTOS会提供一个定制的标准C运行库 (比如 pSOS 的pREPC),或者可以从独立软件开发商的编译器中选择打补丁的C库 (libc) 同样可以得到glibc。这样,在最小化情况下,多数的 RTOS支持标准C类型I/O的一个子集(open/close/read/write/ioctl). 大多数情况下,这些调用和从他们衍生出来的调用可以转化为围绕基本I/O的非常薄的封装程序。有趣的是,因为大多数的?RTOS不支持文件系统,这些平台不提供针对flash和旋转媒质的抽象文件存储,常常使用完全不同的代码和/或者不同的应用程序接口(API) (比如 pSOS 的pHILE).
Wind River VxWorks 在这方面比其它多数RTOS平台做的较好些,提供功能丰富的I/O子集,主要克服了网络接口/多媒体接口里的集成和广泛化障碍。
延时处理
很多RTOS也支持一种叫”下半部 “("bottom half" )的机制,它针对可中断和/或者可抢占切换的I/O延时处理方法。其他RTOS没有这样的机制,但是替代地提供类似中断嵌套的机制来获得同样的效果。
典型RTOS应用I/O架构
下面描述一个典型的I/O配置(仅仅输入)和它向主要应用程序传递数据的路径处理过程依次如下:
* 一个硬件中断触发一个中断服务例程的执行.
* 中断服务例程做基本的处理和完成本地的输入操作,或者让RTOS调度延时的处理.在一些情况下,延时处理过程由在Linux里面被叫做用户进程来处理,在这里就是通常的RTOS任务.
* 无论在何时何地获得数据(中断服务例程或者延时切换),准备好的数据被放进队列(RTOS中断服务例程能够访问应用程序队列通过应用程序接口(API)和其它进程间通信 (?IPC),请看下面的API表).
* 一个或者多个应用任务然后从队列读消息,来取出数据.
在传统的RTOS和Linux之间的典型I/O的比较
输出常常由类似的机制来完成。替代使用write()或者相似的系统调用, 一个或者多个RTOS应用程序任务,将准备好的数据放进队列.队列中的数据由以下过程取出:一个I/O程序或者响应”准备好发送”中断的中断服务例程,一个系统时钟,或者其它阻塞在获取队列中的应用任务,然后直接执行I/O操作(可以是轮询,也可以是通过 DMA).
将RTOS I/O 映射进 Linux
上面描述的基于队列的生产/消费I/O模型,仅仅是很多种在传统设计中所采用的特别方法之一.让我们继续用这个直接的例子,来讨论几种在嵌入式Linux下的实现:
大规模移植到用户空间
对于勉强了解Linux设备驱动设计细节,或者非常匆忙的开发者,可能将大多数这样基于队列设计程序完整无缺地移植到用户空间。在这种驱动程序映射配置中,内存映射的物理I/O口通过函数mmap()提供的指针可以在用户空间操作。
#include
#define REG_SIZE 0x4 /* device register size */
#define REG_OFFSET 0xFA400000
/* physical address of device */
void *mem_ptr; /* de-reference for memory-mapped access */
int fd;
fd=open("/dev/mem",O_RDWR); /* open physical memory (must be root) */
mem_ptr = mmap((void *)0x0, REG_AREA_SIZE, PROT_READ+PROT_WRITE,
MAP_SHARED, fd, REG_OFFSET);
/* actual call to mmap() */
一个基于进程的用户线程进行与基于RTOS的中断服务例程或者延时任务一样的操作,然后使用SVR4进程间通信函数msgsnd()将消息放进队列,等待被另一个本地线程或者另一个进程利用函数msgrcv()来获取.
这种快速”脏的” 处理方法是好的原型,同时对于建立可发布型代码带来了巨大的挑战.首先重要的是需要在用户空间扫描中断.象DOS仿真(DOSEMU)项目提供基于信号的带SIG(Silly中断发生器)中断I/O,但是用户空间的中断处理过程非常慢(毫秒量级中断延时,所替代的基于内核的中断服务例程中断延时为数十微秒).更进一步讲,在用户空间的切换调度不能保证用户空间的I/O 线程100%的及时执行,即使采用可抢占Linux内核和实时调度策略.
为使用Linux驱动程序重新设计
写至少一个简单的Linux驱动程序在内核层次处理中断过程,更为可取.一个基本的字符驱动程序或者块驱动程序,能够在”上半部”直接扫描应用中断数据,或者延时到任务队列去后续处理, 任务队列是内核线程或者在2.6内核里为新的工作队列(下半部)机制.一个或者多个应用线程/进程能够打开设备然后执行同时的读操作,正像RTOS应用同时队列接受调用一样.要注意的是,这种方法将需要至少记录I/O线程使用设备读活动的开销,替代队列接受操作.
保留一个RTOS基于队列的I/O架构
为了减小移植到嵌入式Linux 后的影响,你也可以在原来的地方保留基于队列的方案,并且添加额外的线程或者守护进程,他们在新创建的设备上等待I/O操作.当数据准备好以后,这些线程或者守护进程被唤醒 ,按队列接受数据,由应用线程或者进程取尽使用.
移植方法
将RTOS移植到嵌入式Linux 与商业应用程序移植没有概念上的区别。当移植的基础性工作准备好了后(创建make/build脚本和工具,兼容性编译器,特定的包含文件等等),代码级移植将面临应用架构和应用程序(API)使用问题的挑战。
为了便于下面的讨论,我们假设“应用”部分(除针对I/O以外的所有代码)会被从基于RTOS系统中移植到单独的一个Linux进程;RTOS任务映射到Linux线程,而任务里的进程间通信(IPC)映射到Linux里相当的进程里和线程里。
映射RTOS任务到Linux基于进程的线程 Process-based Threads
移植的基本概念容易理解,同时问题出现在细节上。最显而易见的是正在应用中的RTOS应用程序接口,如何才能保留到Linux结构中继续使用。
整体分析―重构
假如你的项目没有很紧的时间要求,并且为了将来项目可反复使用建立可移植性代码,你会化时间分析当前RTOS应用程序结构,和怎样将它们映射到Linux结构中去。对于RTOS应用代码,你要考虑将RTOS任务一一映射到Linux基于进程的线程的生存能力,和是否将RTOS应用重新分配到多个Linux进程。依赖于选择,你会重新考虑使用中的RTOS进程间通信(IPC),采用合适的进程间或者进程内的通信机制。
在驱动程序层面,你肯定要把不规范的内嵌式RTOS代码转化为合适的驱动程序。如果你的旧应用程序已经很好被划分,或者使用RTOS I/O应用程序接口,或者分隔在不同的层面。你的工作将非常容易。如果特殊的I/O代码非常分散于整个你的旧应用程序,你会面临巨大无比的工作量了。
基于API的方法
对于急于摆脱旧RTOS的开发者,或者尝试将原型综合在一起的人,更可能试图将很多RTOS API映射或者转化到Linux相当的位置。 程序体一般的接口几乎是透明的(兼容的API,IPC,系统数据类型等)。其余部分可以通过用#define 重新定义和使用宏来解决。剩下的部分需要重新编码,作为完整的抽象层的一部分。
通过使用仿真库-很多商业嵌入式Linux都带有(比如我们公司的针对Wind River VxWorks 和 pSOS的仿真库),或者使用第三方公司提供的API映射包,比如MapuSoft,能够使你在移植基于API的程序时有良好的开端。
移植RTOS代码和API 到Linux的多叉方法
大多数项目采用混合的方法,映射所有兼容的或者容易转化的API,重新配置那些对运行速度有要求的部分,和“像鼹鼠那样打洞”重新编码剩余的部分直到编译通过和运行。
在内核和用户空间能够适用的API
对于急需重建和“更快又更脏”的API方法,你不得不重新分配RTOS应用程序和I/O代码以Linux内核和用户空的范例。
Linux分层次特权访问的结果是,通常仅仅内核代码(驱动程序)能够访问物理内存,其它的用户代码必须有根用户权限才能访问。
一般情况下,用户空间代码与Linux内核相隔离,只有当内核的出口信息出现在/proc/ksyms 时才能被它直接“看见”。更进一步讲,对于内核显示的系统调用不能够被直接调用,但是可以通过用户库调用。在Linux里面这种分隔是有意来加强稳定性和安全性。
相反的情况存在在写驱动程序的时候。静态链接的驱动程序专属于整个内核名字空间(不是出口),但是对于用户空间基于进程的符号和入口点完全不可见。并且,当你将驱动程序封装成可动态加载的模块时,你的程序在内核里面通过EXPORT_SYMBOL 宏使用显示的接口。
网络驱动程序移植
正如上面指出的,将字符和块驱动程序移植到Linux是直截了当的实践活动。移植网络驱动程序,要困难得多了
回想Linux伴随TCP/IP一起成长,而大多数的RTOS 到90年代晚期才将网络考虑进去。即使如此,旧的网络存储常常仅有基本的功能,比如只能处理单个的会话或者同时只能访问一个端口,或者只能支持单一网络媒质的物理接口。在某些情况下网络结构,在允许多重接口和物理类型连接后,才能实现(比如Wind River VxWorks MUX代码)
坏消息是,你不得不重新写大部分或者全部的现成网络接口。好消息是,对于Linux重新划分不是太困难,而且有大量的开源网络设备驱动例子程序可供选择。
你的移植任务会是,使用合适的封装格式和接口代码,组装下面图表中底部区域:
Linux 网络驱动程序框图
写网络驱动程序不是初学者的事,因为许多RTOS网络驱动程序实际上是从GPL的Linux接口程序演变而来,你也许通过代码本身发现程序易用性。更进一步讲,有大量的,并且一直在增长的系统集成和咨询社区,以合理的收费专注于帮助嵌入式开发者将他们的应用移植到Linux。
总结
这篇文章深入分析了,将整体软件从旧的RTOS转移到Linux过程中,嵌入式开发者面临的挑战和将得到的好处。寥寥2800字或者更多的内容对于深入研究许多驱动程序移植的细节来说太过于简单(总线接口的驱动程序API,地址转换等),但是丰富的现成的开源GPL驱动程序代码,可以作为你移植过程的文档和模板.本文作为导引肯定对你的团队在移植RTOS到Linux的时候有帮助,并为最佳适合于嵌入式Linux而重新规划代码有启发。