将 RTOS 设备驱动程序移植到嵌入式 Linux
Linux 已经风靡嵌入式市场。据行业分析师称,三分之一到一半的新型嵌入式 32 位和 64 位设计采用了 Linux。嵌入式 Linux 已经主导了多个应用领域,包括 SOHO 网络和成像/多功能外围设备,并且目前在存储 (NAS/SAN)、数字家庭娱乐 (HDTV/PVR/DVR/STB) 和手持/无线设备(尤其是在数字移动电话中)领域取得了巨大进展。
新的嵌入式 Linux 应用程序并非像弥涅耳瓦一样从开发人员的头脑中涌现出来;大多数项目必须容纳数千甚至数百万行的遗留源代码。尽管数百个嵌入式项目已成功地将现有代码从诸如 Wind River 的 VxWorks 和 pSOS、VRTX、Nucleus 以及其他 RTOS 平台移植到 Linux,但这项工作仍然非常重要。
迄今为止,关于从遗留 RTOS 应用程序迁移到嵌入式 Linux 的大多数文献都集中在 RTOS API、任务和调度模型以及它们如何映射到 Linux 用户空间等效项上。在 I/O 密集的嵌入式编程领域,同样重要的是将 RTOS 应用程序硬件接口代码移植到更正式的 Linux 设备驱动程序模型。
本文调查了几种在遗留嵌入式应用程序中常见的内存映射 I/O 方法。这些方法包括从临时使用中断服务例程 (ISR) 和用户线程硬件访问,到某些 RTOS 库中提供的半正式驱动程序模型。本文还介绍了将 RTOS 代码转换为格式良好的 Linux 设备驱动程序的启发式方法和方法论。特别是,本文重点关注 RTOS 代码与 Linux 中的内存映射、移植基于队列的 I/O 方案以及为原生 Linux 驱动程序和守护程序重新定义 RTOS I/O。
最能描述基于 RTOS 的系统中的大多数 I/O 的词是“非正式”。大多数 RTOS 都是为较旧的无 MMU CPU 设计的,因此即使存在 MMU,它们也会忽略内存管理,并且不区分逻辑寻址和物理寻址。大多数 RTOS 也完全在特权状态(系统模式)下执行,表面上是为了提高性能。因此,所有 RTOS 应用程序和系统代码都可以访问整个机器地址空间、内存映射设备和 I/O 指令。实际上,即使存在这种区别,也很难区分 RTOS 应用程序代码和驱动程序代码。
这种非正式的架构导致了 I/O 的临时实现,并且在许多情况下,完全没有可识别的设备驱动程序模型。鉴于这种平等的非分区工作,回顾一些关键概念和实践在它们应用于基于 RTOS 的软件时的情况是有益的。
当商业 RTOS 产品在 20 世纪 80 年代中期上市时,大多数嵌入式软件都由带有轮询 I/O 和 ISR 的大型主循环组成,用于时间关键型操作。开发人员主要将 RTOS 和执行程序设计到他们的项目中,以增强并发性和帮助多任务同步,但他们避开了任何其他阻碍他们的结构。因此,即使 RTOS 提供了 I/O 形式,嵌入式程序员仍然继续以内联方式执行 I/O
#define DATA_REGISTER 0xF00000F5 char getchar(void) { return (*((char *) DATA_REGISTER)); } void putchar(char c) { *((char *) DATA_REGISTER) = c; }
更严谨的开发人员通常会将所有此类内联 I/O 代码与硬件无关的代码分离开来,但我遇到过很多 I/O 意大利面代码。当面对普遍存在的内联内存映射 I/O 使用时,刚接触 Linux 的嵌入式开发人员总是面临将所有此类代码按原样移植到用户空间的诱惑,即将#define寄存器地址转换为调用mmap()。这种方法对于某些类型的原型设计效果很好,但它不支持中断处理,实时响应能力有限,安全性不高,并且不适合商业部署。
在 Linux 中,中断服务完全是内核的领域。对于 RTOS,ISR 代码是自由形式的,并且通常与应用程序代码无法区分,除了返回序列之外。许多 RTOS 提供系统调用或宏,让代码检测其自身的上下文,例如 Wind River VxWorks intContext()。 ISR 使用标准库也很常见,并伴随着重入和可移植性挑战。
大多数 RTOS 支持 ISR 代码的注册,并处理中断仲裁和 ISR 分派。然而,一些原始的嵌入式执行程序仅支持将 ISR 起始地址直接插入到硬件向量表中。即使您尝试在用户空间中内联执行读取和写入操作,您也必须将您的 Linux ISR 放入内核空间。
大多数 RTOS 都附带定制的标准 C 运行时库,例如 pSOS 的 pREPC,以及来自编译器 ISV 的选择性修补的 C 库 (libc)。他们对 glibc 也这样做。因此,至少,大多数 RTOS 都支持标准 C 风格 I/O 的子集,包括系统调用 open、close、read、write 和 ioctl。在大多数情况下,这些调用及其派生调用解析为 I/O 原语周围的薄包装器。有趣的是,由于大多数 RTOS 不支持文件系统,因此那些为 Flash 或旋转介质提供文件抽象的平台通常使用完全不同的代码和/或不同的 API,例如 pSOS 的 pHILE。 Wind River VxWorks 在提供功能丰富的 I/O 子系统方面比大多数 RTOS 平台走得更远,主要是为了克服网络接口/媒体的集成和泛化障碍。
许多 RTOS 也支持下半部机制,即某种将 I/O 处理推迟到可中断和/或可抢占上下文的方法。其他 RTOS 不支持下半部机制,但可能会支持诸如中断嵌套之类的机制来实现类似的目的。
典型的 I/O 方案(仅输入)以及到主应用程序的数据传递路径如图 1 所示。处理过程如下
硬件中断触发 ISR 的执行。
ISR 执行基本处理,并在本地完成输入操作,或让 RTOS 调度延迟处理。在某些情况下,延迟处理由 Linux 称为用户线程的内容处理,这里是一个普通的 RTOS 任务。
无论数据最终在何时何地获取(ISR 或延迟上下文),就绪数据都会放入队列中。是的,RTOS ISR 可以访问应用程序队列 API 和其他 IPC——请参阅 API 表。
然后,一个或多个应用程序任务从队列中读取消息以使用已传递的数据。
输出通常通过类似的机制完成——一个或多个 RTOS 应用程序任务不是使用 write() 或类似的系统调用,而是将就绪数据放入队列中。然后,队列由 I/O 例程或 ISR 清空,该例程或 ISR 响应就绪发送中断、系统计时器或另一个等待队列内容的应用程序任务。然后,它直接执行 I/O,无论是轮询还是 DMA。
上面描述的基于队列的生产者/消费者 I/O 模型是遗留设计中采用的众多临时方法之一。让我们继续使用这个简单的示例来讨论嵌入式 Linux 下的几种可能的(重新)实现。
不愿学习 Linux 驱动程序设计细节或时间紧迫的开发人员可能会尝试将基于队列的设计的大部分完整地移植到用户空间范例。在这种驱动程序映射方案中,内存映射物理 I/O 通过 mmap() 提供的指针在用户上下文中发生
#include <sys/mman.h> #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 的 ISR 或延迟任务相同的处理。然后,它使用 SVR4 IPC msgsnd() 调用来为另一个本地线程或另一个进程排队消息,方法是调用 msgrcv()。
尽管这种快速而肮脏的方法适用于原型设计,但它为构建可部署的代码带来了重大挑战。最重要的是需要在用户空间中处理中断。诸如 DOSEMU 之类的项目提供基于信号的中断 I/O 和 SIG(愚蠢的中断生成器),但用户空间中断处理非常慢——毫秒级的延迟,而不是内核级 ISR 的数十微秒。此外,用户上下文调度,即使使用可抢占的 Linux 内核和实时策略,也无法保证 100% 及时执行用户空间 I/O 线程。
最好是咬紧牙关,至少编写一个简单的 Linux 驱动程序来处理内核级别的中断处理。基本的字符或块驱动程序可以直接在上半部处理应用程序中断数据,或者将处理推迟到 tasklet、内核线程或 2.6 内核中提供的新型工作队列下半部机制。一个或多个应用程序线程/进程可以打开设备,然后执行同步读取,就像 RTOS 应用程序执行同步队列接收调用一样。这种方法至少需要重新编码消费者线程 I/O 以使用设备读取而不是队列接收操作。
为了减少移植到嵌入式 Linux 的影响,您还可以保留基于队列的方案,并添加一个额外的线程或守护进程,该线程或守护进程等待新创建的设备上的 I/O。当数据准备就绪时,该线程/守护进程会唤醒并将接收到的数据排队,供消费应用程序线程或进程使用。
将 RTOS 代码移植到嵌入式 Linux 在概念上与企业应用程序迁移没有区别。在解决了移植的后勤问题(make/build 脚本和方法、编译器兼容性、包含文件的位置等等)之后,代码级移植挑战就变成了应用程序架构和 API 使用问题。
对于手头讨论的目的,让我们假设应用程序部分(除 I/O 特定代码之外的所有内容)从基于 RTOS 的系统迁移到单个 Linux 进程中。 RTOS 任务映射到 Linux 线程,任务间 IPC 映射到 Linux 进程间和线程间等效项。
尽管移植的基本形状很容易理解,但魔鬼在于细节。最突出的细节是正在使用的 RTOS API 以及如何使用 Linux 结构来适应它们。
如果您的项目时间不受限制,并且您的目标是为未来的项目迭代生成可移植的代码,那么您需要花一些时间分析当前 RTOS 应用程序的结构以及它如何/是否适合 Linux 范例。对于 RTOS 应用程序代码,您需要考虑将 RTOS 任务一对一映射到基于 Linux 进程的线程的可行性,以及是否将 RTOS 应用程序重新划分为多个 Linux 进程。根据该决定,您应该查看正在使用的 RTOS IPC,以确定适当的进程内与进程间范围。
在驱动程序级别,您肯定希望将任何非正式的内联 RTOS 代码转换为适当的驱动程序。如果您的遗留应用程序已经很好地分区,无论是使用 RTOS I/O API 还是至少隔离到不同的层,您的任务都会变得容易得多。如果临时 I/O 代码散布在您的整个遗留代码库中,那么您就需要努力工作了。
急于摆脱遗留 RTOS 的开发人员或试图将原型粘合在一起的开发人员更可能尝试就地将尽可能多的 RTOS API 映射或转换为 Linux 等效项。常见的实体,例如可比较的 API、IPC 和系统数据类型,几乎可以透明地移植。其他的可以通过 #define 重新定义和宏来解决。剩下的需要重新编码,理想情况下是作为抽象层的一部分。
您可以通过使用许多嵌入式 Linux 发行版(包括 MontaVista 用于 Wind River VxWorks 和 pSOS 的库)附带的仿真库,或通过使用 MapuSoft 等公司的第三方 API 映射包,在基于 API 的移植方面抢占先机。
大多数项目都采用混合方法,映射所有可比较或易于翻译的 API,在不会减慢速度的地方重新构建架构,并与剩余的代码玩打地鼠游戏,直到它可以构建和运行。
对于密集型重新架构和更快速更肮脏的 API 方法,您仍然必须(重新)划分您的 RTOS 应用程序和 I/O 代码以适应 Linux 内核和用户空间范例。表 1 说明了 Linux 在特权操作方面比传统 RTOS 严格得多,并有助于指导您完成(重新)分区过程。
表 1. Linux 和遗留 RTOS 中的特权操作
IPC | 同步 | 任务 | 命名空间 | |
---|---|---|---|---|
RTOS 应用程序 | 队列、信号、邮箱 非正式共享内存 | 信号量、互斥锁 | 完整的 RTOS 任务库 | 完整的应用程序、库和系统(链接时) |
RTOS 驱动程序 | 队列、信号、邮箱 非正式共享内存 | 信号量、互斥锁 | 完整的 RTOS 任务库 | 完整的应用程序、库和系统(链接时) |
Linux 应用程序 | 队列、信号、管道 进程内共享内存 共享系统内存 | 信号量、互斥锁 | 进程和线程 API | 本地进程、静态库和共享库 |
Linux 驱动程序(静态) | 共享系统内存 读/写进程内存 | 内核信号量 自旋锁 | 内核线程、Tasklet | 完整内核 |
Linux 模块(动态) | 共享系统内存 读/写进程内存 | 内核信号量 自旋锁 | 内核线程、Tasklet | 模块本地和导出的内核符号 |
表 1 中调用了两个重要的区别
RTOS 是平等的,允许应用程序和 I/O 代码触摸任何地址并执行几乎任何活动,而 Linux 则更具层次结构和限制性。
遗留 RTOS 代码可以查看系统中的每个符号或入口点,至少在链接时是这样,而 Linux 用户代码与内核代码及其随附的命名空间隔离并单独构建。
Linux 特权访问层次结构的结果通常是只有内核代码(驱动程序)实际访问物理内存。也这样做用户代码必须以 root 身份运行。
一般来说,用户空间代码与 Linux 内核隔离,并且只能看到 /proc/ksyms 中显式导出的符号。此外,对内核的可见系统调用不是直接调用的,而是通过调用用户库代码来调用的。这种隔离是故意的,旨在增强 Linux 的稳定性和安全性。
当您编写驱动程序时,情况恰恰相反。静态链接的驱动程序可以访问整个内核命名空间,不仅可以访问导出,而且对用户空间基于进程的符号和入口点具有零可见性。而且,当您将驱动程序代码封装在运行时可加载模块中时,您的程序只能利用内核中由 *EXPORT_SYMBOL* 宏显式导出的那些接口。
如上所示,将字符和块设备驱动程序移植到 Linux 是一项直接但耗时的活动。然而,移植网络驱动程序似乎更令人生畏。
请记住,虽然 Linux 是伴随着 TCP/IP 成长的,但大多数 RTOS 都在 20 世纪 90 年代后期才在其上嫁接了网络功能。因此,遗留网络通常只提供基本功能,例如只能处理单个端口上的单个会话或实例,或者只能支持单个网络介质上的物理接口。在某些情况下,网络架构是在事后进行泛化的,例如 Wind River VxWorks MUX 代码,以允许多个接口和物理连接类型。
坏消息是您可能必须重写大部分或全部现有网络接口。好消息是为 Linux 重新分区并不难,并且您有数十个开源网络设备驱动程序示例可供选择。
您的移植任务是用合适的包格式化和接口代码填充图 4 底部的区域。
编写网络驱动程序不适合初学者。然而,由于许多 RTOS 网络驱动程序实际上是从现有的 GPL Linux 接口派生而来的,因此您可能会发现代码本身有助于简化该过程。此外,还有一个庞大且仍在增长的集成商和顾问社区,他们专注于将帮助嵌入式开发人员将其应用程序迁移到 Linux 作为一项业务,费用合理。
在撰写本文时,作为战略营销总监和技术传播者,Bill 将其 17 年以上的行业经验专注于在当今动态的普及计算市场中推进 MontaVista 和嵌入式 Linux。他的背景包括丰富的嵌入式和实时经验,在操作系统、工具、软件许可和制造方面具有专业知识。