RTLinux 应用程序开发教程
RTLinux 是一种硬实时操作系统,与软实时系统或那些不提供任何调度保证的系统不同。RTLinux 是一个硬实时内核,它在没有实时需求时,将 Linux 或 BSD 作为空闲任务运行。
RTLinux 采用的双内核方法需要一种稍微不同的实时编程方法。实时代码被编写为由实时内核管理的内核模块。所有用户管理代码都作为由 Linux 管理的普通进程运行,并通过各种机制进行通信。这种代码分离将实时代码抽象成一个更简单的代码库,简化了实时代码和管理接口的开发。
一些实时操作系统方法试图强制内核和用户空间代码在实时和非实时调度约束下都能良好运行。相反,RTLinux 采用了正常的 UNIX 方法,即编写一个工具来做好一件事,并且做得好,而不是将所有东西都塞进一个万能的系统中。这产生了一个更简单的系统,并鼓励编写更简单的代码,同时提供了一个确定性的实时环境,该环境仅受驱动它的硬件的限制。
你们中那些以前使用过实时系统的人都知道,每个系统都有一个“特殊”的 API,“特殊”通常会被更生动的词汇所取代。然而,RTLinux 的 API 是基于 POSIX PSE 51 的,这是一个为嵌入式实时系统设计的标准。这种能力使开发人员能够在实时环境中使用标准的 pthread_* 调用。这意味着所有 POSIX 调用,例如 pthread_create(),以及所有的互斥锁调用、条件变量等,都可用于实时代码。对于引入不确定性且标准未涵盖的情况,RTLinux 提供了扩展,以使开发人员的生活更轻松。
从用户空间的角度来看,非实时代码与任何其他 Linux 进程完全相同。一旦实时组件被抽象出来,剩余的代码就可以像任何其他应用程序一样自由运行,而不用担心干扰实时操作。管理前端可以用 GTK+(或 Qt,当然)编写,与远程数据库通信,甚至直接在实时系统上托管整个 Oracle 实例。在 Linux 中所做的任何事情都不会影响实时代码的执行。虽然这不是编写草率的用户空间代码的许可证,但它确实消除了因某人错误处理 Java 垃圾回收器或在机器控制机械臂时通过 NFS 传输大文件而引起的任何并发症的担忧。
作为旁注,在实时内核中运行的代码也可以访问整个 Linux 内核的 API。然而,Linux 内核的设计并没有考虑到实时约束,并且许多调用并不总是安全的。可能会发生不确定的阻塞,具体取决于调用链。开发者需要决定哪些调用对于当前环境和问题是安全的,RTLinux 提供了某些常用函数的实时等效函数。由于大多数硬实时问题都涉及与硬件的直接交互,因此拥有内核 API 来处理诸如 PCI 设备初始化之类的任务可以避免许多麻烦。正如本文后面所演示的那样,在某些地方使用内核 API 是完全安全的,所以一切并非都不可用。
如果实时代码在其自身的实时内核中独立运行,而其余代码作为普通的 Linux 任务运行,那么如何管理实时代码呢?RTLinux 为这个问题提供了一些答案,并解决了不同情况下的各种需求。
首先,最常见的通信模型是实时 FIFO。任何使用过 Linux 下的普通 FIFO(使用 mkfifo 创建)的人都熟悉它的工作原理。一端的进程写入 FIFO,它表现为一个普通文件,而另一端的进程从中读取。使用 RTLinux,读取器可能是实时进程,而写入器是用户空间程序,通过 FIFO 向实时代码传递指令,反之亦然。在任何一种情况下,FIFO 设备都是普通的字符设备 (/dev/rtf*),两端都可以通过普通的 POSIX 调用(例如 open()、close()、read() 和 write())与设备交互。
这种方法适用于许多应用程序,因为用户空间代码只需处理一个普通文件即可进行通信。从实时的角度来看,将数据推送到 FIFO 的调用是非阻塞的,并且对执行几乎没有影响。但是,由于实时代码永远不能在等待用户空间处理数据时被阻塞,因此 FIFO 调用允许覆盖数据,而不是阻塞实时调用者。这意味着如果实时内核处于高负载状态,并且永远不调度 Linux 线程(以及您的用户空间代码),则 FIFO 可能会在用户空间读取之前填满。在这种情况下,FIFO 应该分配更多内存来缓冲,或者应该刷新 FIFO 以防止用户空间获取过时或损坏的数据。
另一种 IPC 机制是 mbuff 驱动程序。这是一个共享内存系统,允许内核和用户空间代码共享对同一内存区域的访问。对于某些应用程序,这是一种非常好的共享信息方式,尤其是在访问方法可能非顺序的情况下,就像实时 FIFO 一样。此外,与 FIFO 一样,实时代码在用户空间应用程序处理数据时不能被阻塞,因此两者之间没有同步方法。如果程序员需要这样做,则可以通过区域中的某些位标志来协调访问,但必须注意不要无意中阻塞实时代码。
第三个值得一提的系统是 RTLinux 的软中断系统。实时代码可以创建一个基于软件的中断请求(IRQ),其中在 Linux 内核下安装一个处理程序,该处理程序的运行方式就像它正在拦截真实的硬件 IRQ 一样。但在这种情况下,IRQ 实际上是由实时内核中的驱动程序生成的。这是一种非常有效的方法,可以安全地访问 Linux 内核中可能阻塞的调用。例如,RTLinux 调用 rtl_printf()(其行为类似于 Linux 的 printk())允许安全的实时 printk() 调用,方法是将数据推送到缓冲区并发出软件 IRQ 信号。安装在 Linux 下的处理程序拦截此信号,并将数据安全地移动到正常的内核环形缓冲区中,而不会潜在地引起阻塞。
说得够多了——让我们来看一个简单的例子,演示实时代码、线程和实时 FIFO 的用法。为了运行此示例,您需要运行 RTLinux,但在 RTLinux Open 下载版或 RTLinux Professional 发行版中详细说明了配置,此处不再赘述。您可以在 http://www.fsmlabs.com/products/download.htm 上在线找到这两个版本。
此时,我们假设一个运行 RTLinux 内核且已加载基本 RTLinux 模块(使用 insrtl 加载的模块)。我们不会带您完成通常的“hello world”程序,而是做一些略有不同的事情。在这里,用户空间进程将与实时代码通信,指导每个进程执行实时“hello world”中的不同步骤。
在我们深入代码之前,让我们简要讨论一下这个想法。与大多数 RTLinux 应用程序一样,它有两个组件:实时模块和用户空间管理部分。在本例中,实时代码由两个线程和一个实时 FIFO 处理程序组成。FIFO 处理程序监视 FIFO 以接收来自用户空间的命令。当控制处理程序接收到请求时,它通过另一个 FIFO 在内部将其传递给目标线程。两个目标线程生成“hello”和“world”作为周期性实时代码。它们从 FIFO 处理程序接收的控制命令指示每个线程交替地将“hello”和“world”写入另一个 FIFO。用户空间代码只需读取来自两个 hello world 线程的两个 FIFO,并定期向处理程序推送命令,指示它们切换角色。
首先,有一个简单的头文件,实时代码和用户空间代码共享。这定义了一些在 FIFO 中传递的值作为命令
#define NOOP 0 #define HELLO 1 #define WORLD 2 #define STOP 3 struct my_msg_struct { int command; int task; };
这四个状态指示实时线程的动作,并从用户空间通过控制 FIFO 发送到实时线程,封装在 my_msg_struct 结构中。task 字段指示命令应该发送到线程 1 还是线程 2。
首先,我们将回顾本练习中的实时组件。它是一个内核模块的事实不应该吓到您;对于任何以前做过 POSIX 代码的人来说,这应该看起来很简单
#include <rtl.h> #include <time.h> #include <unistd.h> #include <rtl_sched.h> #include <rtl_fifo.h> #include "control.h" RTLINUX_MODULE(thread_mod); pthread_t tasks[2]; void *thread_code(void *t); int my_handler(unsigned int fifo);
这就像您在任何其他应用程序中可能看到的头信息一样。有两个实时线程生成“hello”和“world”,因此我们将它们的线程 ID 存储在 pthread_t tasks[2] 数组中。这样我们就可以使用 ID 在清理期间取消线程。函数声明是我们的实时线程和控制 FIFO 处理程序的标准前向声明。
清单 1 显示了所有的初始化代码。正如您所看到的,这是在普通 Linux 内核模块中使用的方法。事实上,此时可以自由使用所有的 Linux 内核设施。由于实时内核不直接支持内存管理,因此任何预分配都应在此处完成。实时 FIFO 创建也需要在该函数中发生,因为在该函数完成后,您将以实时方式运行。
我们显式销毁现有 FIFO 的事实可能会让您感到惊讶,但总的来说这是一个好主意。这确保了此代码显式使用 FIFO,并且没有其他模块遗留的未关闭资源。如果两个模块意外地使用了同一个 FIFO,数据将会混合在一起并显得混乱。因此,我们销毁 FIFO,然后根据需要创建它们。此处选择的缓冲区大小是任意的,在实践中应根据您的需求进行调整。FIFO 本身各有特殊用途。第一个用于用户空间到实时通信。第二个和第三个用于控制线程将数据推送到实时线程,最后两个允许实时线程将数据推送回用户空间。
对于那些熟悉普通 POSIX 线程管理的人来说,接下来的几个步骤应该很简单。我们初始化一个线程属性以设置线程优先级,然后将其与 pthread_create() 调用一起传递,以指导线程创建。我们这样做两次,因为有两个实时线程:一个用于“hello”,一个用于“world”。
最后一行是一个 RTLinux 特有的调用;这为控制 FIFO 注册了一个函数处理程序。当数据被推送到控制 FIFO 时,将调用此函数来读取数据,解释数据并将其推送到正确的线程
void cleanup_module(void) { pthread_cancel (tasks[0]); pthread_join (tasks[0], NULL); pthread_cancel (tasks[1]); pthread_join (tasks[1], NULL); rtf_destroy(0); rtf_destroy(1); rtf_destroy(2); rtf_destroy(3); rtf_destroy(4); }
与 init_module() 类似,此调用在 Linux 内核的上下文中执行。我们在这里所要做的就是取消两个线程,加入它们,然后销毁我们正在使用的所有实时 FIFO。
清单 2 显示了完整的实时线程。我们从 init_module() 调用中传递了任务编号,因此我们将其从 void 转换为实际类型。此线程将使用的 FIFO 将比任务 ID 大 1,因为 FIFO 0 是控制 FIFO。为了安全起见,我们不希望线程在用户空间准备就绪之前填充 FIFO,因此我们将命令设置为 NOOP 以暂停代码。之后,我们使用 RTLinux 扩展进行调用,使线程以周期模式运行。这省去了我们在主循环期间尝试以更复杂的方式调度事情的工作。通过这一个调用,线程变为周期性的,每半秒(500,000,000 纳秒)被调度一次。在每次计划的唤醒时,代码尝试从控制 FIFO 读取。由于它以非阻塞模式打开,如果没有任何内容可读,它将立即返回,否则它将填充消息结构。根据结构中存在的值,它要么向 FIFO 写入消息,要么使用 POSIX I/O 调用进行清理并退出。
清单 3 是实时代码的最后一部分,是控制 FIFO 的处理程序。这本质上是一个回调,当 FIFO 上有待处理数据时执行。在本例中,执行很简单。它从控制 FIFO 读取消息,并根据结构中指定的任务,通过另一个 FIFO 将数据推送到正确的线程。用户空间应用程序很容易可以直接通过第二个 FIFO 写入线程,但采取此步骤是为了演示处理程序和实时组件之间的通信。请注意,我们在此处也使用了 POSIX 调用,在每次回调时打开和关闭处理程序和线程间 FIFO。虽然这增加了开销,可以通过一次打开 FIFO 并跟踪打开的文件描述符来轻松避免,但我们使用这种方法来演示 POSIX I/O 调用的简单性。
本节非常简单。用户空间代码由一个小的 C 应用程序组成,该应用程序与几个文件交互,执行读取和写入操作。对 FIFO 的这些读取和写入操作指示实时线程,控制哪个线程负责“hello”和“world”,最终触发关闭。
清单 4 显示了一个普通的用户空间应用程序。需要一些头文件,包括我们与实时代码的公共定义,以及其他普通的 I/O 头文件。我们声明了一些变量,然后打开实时 FIFO。它们被视为普通文件,因此只需对 /dev/rtf* 条目使用普通的 open() 调用即可。如果您将 FIFO 编号与实时代码进行比较,您将看到 /dev/rtf0 是控制 FIFO,而 3 和 4 是到实时线程的两个通道
msg.task = 0; msg.command = HELLO; write(ctl, &msg, sizeof(msg)); msg.task = 1; msg.command = WORLD; write(ctl, &msg, sizeof(msg)); hello_thread = 0;
如果您回顾实时代码,我们以暂停状态启动了实时线程,以便它们实际上不执行任何操作。我们需要做的第一件事是将命令写入控制 FIFO 以启动它们。为此,我们为线程 0 填写一个结构,命令值为 HELLO,以便告知它写入“hello”。然后对线程 1 执行相同的操作,告诉它打印“world”。为了将来参考,我们记住线程 0 负责“hello”。
清单 5 显示了主循环,没有任何真正的惊喜。由于我们正在监视两个文件,因此我们设置了一个普通的文件描述符集,供 select() 使用。实时代码每半秒运行一次;这里我们更频繁地采样,只是为了保持简单。在间隔更紧密(几十毫秒或更短)的真实环境中,数据可能会被时间戳或以不同的方式传输。
然而,在这里,轮询数据更简单。一旦我们读取数据,循环就会将字符串及其来源的 FIFO 转储到 stdout。为了演示交互式 FIFO 处理,代码每 20 次迭代与控制 FIFO 通信,告诉实时线程切换角色
msg.command = STOP; msg.task = 0; write(ctl, &msg, sizeof(msg)); msg.task = 1; write(ctl, &msg, sizeof(msg)); close(fd0); close(fd1); close(ctl); return 0; }
一旦主例程完成,还需要一个步骤来干净地关闭。实时线程需要关闭。如果我们未能做到这一点并直接退出,除了实时代码会持续覆盖 FIFO 缓冲区之外,不会发生任何不好的事情。相反,我们向控制 FIFO 发送一个停止命令,以便线程停止工作。正如我们将在演示中看到的那样,模块仍然会加载,但它将不再引起任何显着的资源利用率。
运行示例非常简单,并且与 RTLinux 附带的任何其他示例程序没有什么不同。如前所述,我们假设此时普通的 RTLinux 模块已加载到内核中。首先,您需要加载实时代码
insmod thread_mod.o
现在实时代码已启动并运行。在实时内核中,线程已经配置为周期性的并且正在执行,只是没有生成任何有用的东西。现在,启动用户空间应用程序
./thread_app FIFO 1: hello FIFO 2: world FIFO 1: hello ... FIFO 1: world FIFO 2: hello这将持续到用户空间代码完成从文件描述符集读取 1,000 次为止。请注意,随着代码指示控制线程反转角色,FIFO 输出将反转。这可能不会完全同步发生,因为如果实时内核没有时间,它没有义务处理用户空间代码。这意味着切换角色的代码可能只会通过第一个命令,但不会通过针对第二个线程的命令。这确实引入了潜在的复杂性,但为了实时操作而延迟用户代码很少是一件坏事,如果曾经是的话。
这是一个简单的 RTLinux 介绍,包括其概念、API 和一个简短的示例。一次性消化这么多内容可能有点多,但这应该只是一个起点。有关 RTLinux、RTLinuxPro 和其他 FSMLabs 产品的更多信息,请访问 http://www.fsmlabs.com/。

Matt Sherer 在 FSMLabs, Inc. 工作,该公司是 RTLinux 的开发者。他住在新墨西哥州的索科罗,在那里他试图在写作、编码和享受“迷人之地”之间保持平衡。可以通过 sherer@fsmlabs.com 联系 Matt。