引言
在过去几年中,Linux成功地取代了一些最主要的传统RTOS(实时操作系统)平台,成为了各种各样的嵌入式设备和应用中首选的嵌入式操作系统。尽管一度曾被认为是不重要的平台,但今天嵌入式Linux已经成为主流,广泛应用于消费电子、手持和无线设备、数据联网以及电信设备等领域。Google公司在2007年11月发布的Android手机操作系统正是基于Linux内核的操作系统,使得Linux在数字移动电话业取得跨越式发展。
笔者在从台式频谱仪到手持式频谱仪的项目研发中实现了RTOS到Linux的应用移植。本文介绍了整体的设计思路和一些关键问题的实现细节。
1RTOS到Linux的移植分析
几乎所有的RTOS都有一个简单的编程模型,它由多线程的执行(通常称为任务)构成,包含在单一的地址空间中。在RTOS中,单一主程序下多任务同时运行,具有很高的实时响应能力。
过去大多数嵌入式处理器没有内存管理单元,因此RTOS是单地址空间模式,即它们的物理地址和逻辑地址都是一样的。然而目前大多数的中高端处理器配备了MMU(内存管理单元)。在MMU的支持下,Linux采用虚拟内存管理,将地址空间分为物理地址和虚拟地址,因此系统操作硬件时要进行地址映射。
根据两类系统的体系结构,RTOS移植到Linux的基本框架如图1所示。
图1RTOS移植到Linux的基本框架
由图1可看出,移植的基本步骤为:
① RTOS的全部应用代码移植到一个Linux单进程;
② RTOS的任务转换成Linux线程;
③ RTOS的物理地址空间映射到Linux的虚拟地址空间。
在具体的应用移植过程中,还应考虑在Linux系统下解决上层应用实时响应底层硬件中断,应用层与内核层的异步通信、数据交换,以及多进程、多线程的设计等问题。
2RTOS到Linux的移植实现
2.1地址映射
多数RTOS是针对较早的无MMU的CPU而设计,所以忽略了内存管理部分,即使当MMU问世后也是这样——不区分物理地址和虚拟地址。大多数 RTOS还全部运行在特权模式,虽然表面上看来是增强了性能,但全部的RTOS应用和系统代码都能够访问整个地址空间、内存映射过的设备以及其他I/O操作。这样,即使存在差别,也很难把RTOS应用程序代码同驱动程序代码区分开来。
对于当前包含MMU的处理器而言,Linux系统提供了复杂的存储管理系统,使得进程所能访问的虚拟内存达到4 GB。
在Linux系统中,进程的4 GB虚拟内存空间[1]被分为两个部分——用户空间与内核空间。用户地址空间一般分布为0~3 GB,剩下的3~4 GB为内核空间。上层应用程序通常情况下只能访问用户空间的虚拟地址,不能访问内核空间的虚拟地址。应用程序只有通过系统调用(代表应用程序进程在内核态执行)等方式才可以访问到内核空间。
而外设I/O资源是不在Linux内核虚拟地址空间中的(如SRAM或硬件接口寄存器等),若需要访问某外设I/O资源,必须先将其物理地址映射到内核虚拟地址空间中,然后才能在内核空间中访问它。
Linux内核访问外设I/O资源的方式有两种:静态映射(map_desc)和动态映射(ioremap)。对于静态映射,内核在系统启动时通过map_desc结构体静态创建I/O资源到内核地址空间的线性映射表(即page table),这种映射表是一一映射的关系。开发人员可以自定义该I/O内存资源映射后的虚拟地址。创建好了静态映射表,在内核或驱动中访问该I/O资源时则无需再进行ioremap映射,可以直接通过映射后的I/O虚拟地址去访问它。
这里主要讨论更常用的动态映射方式。动态映射方式是直接通过内核提供的ioremap函数动态创建一段外设I/O内存资源到内核虚拟地址的映射表,从而可以在内核空间中访问这段I/O资源。代码如下:
#define bcon*(volatile unsigned long*)ioremap(0x56000010,4)//动态映射
上述代码的含义是将0x56000010开始的4字节的物理地址映射到内核的虚拟地址中,返回的起始虚拟地址值赋给bcon宏定义。对宏定义的操作即对物理地址的操作。
ioremap宏定义在asm/io.h内:
#define ioremap(addr, size)__ioremap(addr, size, 0)
__ioremap函数原型为(arm/mm/ioremap.c):
void __iomem * __ioremap(unsigned long phys_addr, size_t size, unsigned long flags);
其中,phys_addr为要映射的起始的I/O地址;size为要映射的空间的大小;flags为要映射的I/O空间和权限有关的标志。
该函数返回映射后的内核虚拟地址(3G~4G),接着便可以通过读写该返回的内核虚拟地址去访问这段I/O内存资源。所以,在移植的开始就应该在头文件中完成设备物理地址的映射,方便后续的开发。
2.2多进程多线程设计
大多数的RTOS内核都提供多任务的管理机制。任务是一个具有独立功能的无限循环的程序段的一次运行活动,是实时内核调度的单位。多任务在内核的管理、调度下并行执行,而且任务都是无限循环的,持续实现其功能。多任务实时操作系统示意图如图2所示。
图2多任务实时操作系统示意图
在比较两类嵌入式系统的架构之后,移植的过程中很自然地将RTOS的多任务转换成Linux的多进程、多线程。
进程是Linux系统资源管理的最小单位,是程序的一次执行过程,是Linux资源分配的基本单位。线程是在进程内部,它是比进程更小的能独立运行的基本单位,是Linux系统分配CPU时间的基本单位。线程比进程更节约资源,节约时间。在具体的移植过程中,采用主进程等待上层连接,主进程下多线程并行执行。同时采用互斥信号量解决线程访问资源的同步问题。
Linux主进程程序流程如图3所示。
图3Linux主进程程序流程
2.3应用层与内核层通信
由于RTOS的单地址空间模式使得其内核层与应用层没有区别,所以在数据交换、实时响应等方面有一定的优势。而Linux系统提供了严格的内存管理机制,能保证系统更加稳定地运行。但同时增加了应用层与内核层,以及应用层与底层硬件通信的难度。本节内容主要解决应用层与内核层的信号通知、数据交换这两个关键问题。
2.3.1异步信号通知机制
RTOS是对外来事件在限定时间内能作出反应的系统。在RTOS中,时间是一种重要的系统资源,对外部事件的响应和任务的执行都必须在限定的时间内完成。在多机系统中,还必须在限定的时间内完成消息的发送和接收。在RTOS中,输出结果的正确性不仅取决于计算所形成的逻辑结束,还要取决于结果产生的时间。
Linux在发行最初并未定义为一款实时操作系统。随着Linux内核的不断发展,如今稳定的Linux2.6内核已经具备了很好的实时响应能力。本文的研究项目中,需要上层应用对底层硬件进行实时响应。RTOS并没有严格区分上层应用和内核,其多任务并行执行,能很好达地到实时响应的目的。而移植到Linux系统中,上层应用和底层硬件并不能直接通信,要经过内核驱动层。虽然可以采用查询方式实现,但是实时性不高,同时浪费CPU资源。本文采用异步信号通知机制,实现了上层应用对底层硬件的实时响应。
异步通知[2]的意思是:一旦设备就绪,则主动通知应用程序,这样应用程序根本不需要查询设备状态,这一点非常类似于硬件上“中断”的概念,比较准确的称谓是“信号驱动的异步I/O”。信号是在软件层次上对中断机制的一种模拟,在原理上进程收到信号与处理器收到中断请求是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达,原理如图4所示。
图4异步信号通知示意图
在具体的程序设计过程中,上层应用为了能处理一个设备释放的信号,要完成3项工作:
① 通过F_SETOWN控制命令设置设备文件的拥有者为本进程,这样从设备驱动发送的信号才能被本进程接收到。
② 通过F_SETFL控制命令设置设备文件支持FASYNC,即异步通知模式。
③ 通过signal()函数连接信号和信号处理函数。
在上层应用设置捕获信号后,还应在设备驱动端设置信号源,在合适的时机让设备驱动释放信号,其相关代码也包括3部分:
① 支持F_SETOWN命令,能在这个控制命令处理中设置filp﹥f_owner为对应进程ID。
② 支持F_SETFL命令的处理,每当FASYNC标志改变时,驱动程序中的fasync()函数将得以执行。
③ 在设备资源可获得时,调用kill_fasync()函数释放相应的信号给上层应用。
上述3项工作和上层应用的3项工作是一一对应的。按其步骤设计程序,即可实现上层应用通过内核层对底层硬件的及时响应。
2.3.2proc方式数据共享
除了前面提到的信号、套接字、信号量外,Linux还有管道、报文队列、共享内存等进程间通信机制。在移植过程中,由于Linux系统分为应用层和内核层,所以不仅要进行进程间的通信,还要实现应用层与内核层的数据交换。以上的机制多是基于进程间通信,并不能很好地满足要求。在这里采用proc文件系统的方法在Linux内核层和应用层之间进行数据交换。
在Linux系统中,proc文件系统是一个虚拟文件系统,用于内核向用户导出信息。利用proc文件系统通信是比较方便的一种应用层与内核层的数据交换方式,可以将对虚拟文件的读写作为与内核中实体进行通信的一种手段。内核的很多数据都是通过这种方式出口给上层应用的,内核的很多参数也是通过这种方式来让上层方便设置的。实际上,很多应用严重地依赖于proc文件系统,因此它几乎是必不可少的组件。
对于proc文件系统的使用,有如下的接口函数:
struct proc_dir_entry *create_proc_entry(const char *name,mode_t mode,struct proc_dir_entry *parent);
typedef int (read_proc_t) (char *page, char **start, off_t off, int count, int *eof, void *data);
typedef int (write_proc_t) (struct file *file, const char __user *buffer,unsigned long count, void *data);
void remove_proc_entry(const char *name, struct proc_dir_entry *parent);
以上函数作用分别是创建proc文件系统节点、读写proc节点,以及删除proc节点。具体移植的proc程序流程如图5所示。
图5proc程序流程
2.4调试运行
根据移植的基本框架,在解决了以上几个关键问题后,基本完成了整个移植的过程。最后要做的就是程序的调试。对于程序语法的调试,在编译的过程中解决。根据Linux平台下的编译器gcc的提示信息,修改出现的语法类错误。在保证了应用程序文件的成功编译后,采用gdb调试软件进行功能的调试,同时结合打印函数printf跟踪调试。在程序适当的位置加入printf打印信息,例如根据创建proc节点的返回值来打印成功或者失败的信息,可以很直观地了解程序的运行情况,是很有效的调试方法。通过两种手段的结合,最后完成应用程序的调试。结果表明,能够在Linux系统下正常运行。
结语
现在越来越多的开发团队正在放弃第一代实时操作系统,选择更稳定的开放式的嵌入式Linux平台。参考本文概括的应用程序的移植步骤以及相关的关键技术,开发人员可以通过更少的时间,将以前的RTOS的代码成功地移植到一个现代化的Linux平台上来。