RTAI 环境:实时任务通信与性能优化
1. RTAI 通信机制概述
RTAI 提供了多种通信和同步机制,主要用于内核空间实时任务之间的交互,同时也支持实时任务与用户空间 Linux 进程的通信。
1.1 消息传递(Messaging)
消息传递是一种直接的任务到任务的通信机制。一个任务可以直接向另一个任务发送单个整数。接收任务可以等待来自特定发送者或任何任务的消息(整数)。此外,还有一种全双工版本的消息传递,称为“远程过程调用(Remote Procedure Calls)”。这些机制都在
rtai_sched
模块中实现。
1.2 与 Linux 进程通信
RTAI 支持两种机制来实现实时任务与用户空间 Linux 进程之间的通信:RT FIFOs 和共享内存。
- RT FIFOs
- RT FIFO 是连接一个实时任务和一个 Linux 进程的点对点链接,类似于 Unix 管道。虽然实现上允许 FIFO 是双向的,但在实际应用中,单向 FIFO 更为常见。例如,如果 FIFO 的一端写入命令后立即尝试读取该命令的结果,很可能会读回刚刚写入的命令。因此,通常由程序员确定 FIFO 的方向,必要时可以创建两个 FIFO,一个用于发送命令,另一个用于读取响应。
-
用户空间进程将 RT FIFOs 视为字符设备,路径为
/dev/rtf0到/dev/rtf63。进程通过open()函数打开 FIFO 进行读写操作,然后使用read()或write()函数在文件描述符上传输数据。如果在 RTAI 构建过程中选择了 FIFO 支持,rtf节点会自动添加到文件系统中。实时任务则通过 RTAI 特定的 API 访问 FIFO。 - 共享内存
- 当需要在一个实时任务和一个用户空间进程之间大致同步地传输相对少量数据时,FIFO 模型很有用。但在某些情况下,多个进程可能需要访问单个实时任务生成的数据,或者需要在核空间的实时任务和用户空间的一个或多个进程之间快速移动大量数据(如视频帧缓冲区),此时共享内存模型更为合适。因为使用共享内存时,数据无需在不同域之间复制,实时任务写入共享内存区域,进程可以随时读取。
2. 用户空间实时任务 - LXRT
RTAI 及其管理的实时任务在内核空间特权级别 0 运行,这在开发过程中会带来一些问题,因为很难使用像 GDB 这样的源代码级调试器调试内核代码。幸运的是,RTAI 提供了 LXRT 机制,允许使用与内核空间 RTAI 相同的 API 在用户空间运行实时任务。
2.1 LXRT 工作原理
创建 LXRT 任务时,将其作为一个普通的 Linux 用户空间进程,包含
main()
函数。在初始化过程中,会创建一个在内核空间运行的“伙伴”任务,代表用户空间任务执行操作。例如,当 LXRT 任务调用
rt_task_wait_period()
时,LXRT 会让在内核空间运行的伙伴任务执行实际函数,只有当伙伴任务唤醒后,控制权才会返回给 LXRT 任务。同样,可以创建 RTAI 通信和同步对象,如信号量和邮箱,每个对象通过名称进行标识,将对象名称传递给相应的初始化或创建函数,该函数会返回一个指向数据结构的指针,用于访问该对象。
2.2 使用 LXRT 测量延迟
为了介绍 RTAI,我们进行一个延迟实验的变体,该实验由一对 LXRT 任务组成。
-
rt_process任务 :周期性唤醒,读取当前时间,计算与理想周期的偏差,并检查最小和最大值。经过一定次数的循环后,通过 RTAI 邮箱将这些信息发送给check任务。 -
操作步骤:
-
进入
Rtdemos/TaskJitter目录,打开rt_process.c文件。 -
在
main()函数中,大约第 69 行,为了使 LXRT 正常工作,需要使用SCHED_FIFO调度策略并将进程锁定在内存中(无分页)。调用rt_task_init()函数创建名为“LATCAL”的 LXRT 任务,并返回一个指向RT_TASK结构的指针。 -
调用
rt_mbx_init()函数创建名为“LATMBX”的邮箱,并返回一个指向邮箱结构的指针。 -
在
oneshot相关的if语句中,调用start_rt_timer()函数以指定的周期(如 100,000 纳秒,即 100 微秒)启动系统定时器。 -
调用
rt_task_make_periodic()函数启动“LATCAL”任务,使其与定时器具有相同的周期,即每隔一个定时器滴答唤醒一次。 -
在任务的
while()循环中,调用rt_task_wait_period()函数阻塞,直到下一个调度执行时间。每次任务唤醒时,读取当前时间并计算与预期唤醒时间的偏差。经过几千次循环后,使用rt_mbx_send_if()函数将数据结构发送到邮箱。 -
使用
rt_receive_if()函数检查伙伴任务“LATCHK”是否发送了消息。如果返回非零值,则表示收到消息,此时“LATCAL”任务应退出。 -
使用
rt_get_adr()函数在 LXRT 命名空间中查找名称,并返回相应的句柄。
-
进入
-
check任务 :创建自己的伙伴任务,获取“LATMBX”邮箱的地址(句柄),使用rt_mbx_receive()函数阻塞等待邮箱中的消息,收到消息后打印各种偏差参数,最后检查信号处理程序设置的标志,决定是否退出。
实验操作步骤:
1. 编译项目,除了
rt_process
和
check
外,还会生成
jittermod.o
目标文件。
2. 由于只有超级用户进程才能设置调度器和锁定内存,因此需要以超级用户身份运行程序。
3. 在运行任务之前,需要加载 LXRT 模块。可以使用
modprobe rtai_lxrt
命令,该命令会检查
rtai_lxrt
的依赖关系,并在必要时加载所需的模块。
4. 有两种方式运行这两个任务:
- 打开两个 shell 窗口,分别运行
rt_process
和
check
。
- 在同一个窗口中,将
rt_process
作为后台任务运行,将
check
作为前台任务运行。命令如下:
./rt_process &
./check
实验结果以纳秒为单位显示。启动 Netscape 浏览器后,可以观察到最大偏差值变得很糟糕,这表明传统的 LXRT 实际上只是“软”实时,可作为将任务迁移到内核空间之前的调试工具,但在确定性性能方面没有优势。
2.3 LXRT 中的硬实时性能
LXRT 的后期版本提供了一种功能,允许在用户空间实现硬实时性能。通过调用
rt_make_hard_real_time()
函数,可以将用户空间进程转换为硬实时进程(更准确地说是更接近硬实时)。该函数通过操作调度器并在硬实时进程执行时阻塞硬件中断来实现。可以通过调用
rt_make_soft_real_time()
函数将进程恢复为正常的软实时。
rt_process
程序提供了两个运行时参数,分别以“h”和“o”开头,用于改变其行为。“h”表示硬实时,如果执行
./rt_process h
,程序将调用
rt_make_hard_real_time()
函数。运行该程序后,即使启动 Netscape 浏览器,也能看到明显更好的结果,延迟可降低到几十微秒甚至更低。
3. 定时器模式 - 周期性与一次性
RTAI 支持两种定时器滴答中断模式:周期性和一次性。
3.1 周期性模式
在周期性模式下,将定时器设置为以指定的周期中断。当定时器计数器溢出并产生中断时,会自动重新加载正确的起始值并重新开始计数,因此在服务定时器芯片本身时没有额外开销。大多数操作系统都采用这种方式工作。但周期性任务只能以定时器滴答间隔的整数倍进行调度。例如,如果滴答间隔为 1 毫秒,那么最短的任务周期就是 1 毫秒。如果需要更精细的粒度,可以减小滴答间隔,但这会导致定时器滴答服务的固定开销增加,滴答间隔越短,处理器用于处理滴答中断的时间比例就越高。当滴答间隔为 10 微秒时,系统可能几乎完全用于处理滴答中断,而没有时间执行实际工作。
3.2 一次性模式
一次性模式是解决上述问题的一种方案。与周期性模式不同,一次性模式在每次定时器中断时重新编程定时器。即在每次滴答中断时,计算到下一个“事件”的时间,并为该间隔编程定时器。此时,定时间隔的分辨率由驱动定时器的时钟决定,而不是定时器中断的周期性间隔。
例如,假设有三个周期性任务:
| 任务 | 间隔 |
| ---- | ---- |
| Task1 | 1.3 毫秒 |
| Task2 | 600 微秒 |
| Task3 | 2.1 毫秒 |
假设这三个任务同时启动,它们将按间隔从小到大的顺序排列在等待列表中,其他任务的间隔表示为第一个间隔到期后的剩余时间:
| 任务 | 剩余间隔(微秒) |
| ---- | ---- |
| Task2 | 600 |
| Task1 | 700 |
| Task3 | 1500 |
定时器设置为 600 微秒,当定时器中断发生时,Task2 准备执行,列表更新以反映下一个间隔。由于 Task2 的 600 微秒周期仍然小于其他任务的剩余间隔,因此定时器再次设置为 600 微秒,列表更新如下:
| 任务 | 剩余间隔(微秒) |
| ---- | ---- |
| Task2 | 600 |
| Task1 | 100 |
| Task3 | 900 |
在下一次定时器中断时,Task2 再次准备执行,列表更新,定时器设置为 100 微秒:
| 任务 | 剩余间隔(微秒) |
| ---- | ---- |
| Task1 | 100 |
| Task2 | 500 |
| Task3 | 800 |
这种灵活性的代价是需要在每次滴答中断时重新计算剩余间隔并重新编程定时器,从而增加了定时器开销。据 RTAI 团队称,在 233-MHz 的奔腾 III 处理器上,周期性模式支持高达 90 kHz 的滴答率,而一次性模式支持高达约 30 kHz 的滴答率。可以通过调用
rt_set_one_shot_mode()
函数启用一次性模式,
rt_set_periodic_mode()
函数用于设置周期性模式,该模式为默认模式。
rt_process
程序的“o”运行时参数用于设置一次性定时模式。
4. 迁移到内核空间
LXRT 是开始使用 RTAI 的好方法,因为可以使用 DDD 调试器观察程序运行情况。现在,我们来看看在内核空间的情况。
4.1 内核空间任务实现
查看
TaskJitter/
目录下的
rt_module.c
文件,在
init_module()
函数(大约第 83 行)中,
rt_task_init()
函数比 LXRT 形式需要更多参数,需要指定实现任务的函数(如
latency()
)以及任务栈的大小,但不需要为任务命名。
oneshot
现在是一个模块参数。
init_module()
函数的初始化操作与
rt_process.c
基本相同,
cleanup_module()
函数的后处理操作也与
rt_process.c
类似。
latency()
函数从第 33 行开始,首先注册邮箱名称,以便在用户空间运行的
check
任务能够找到它,然后进入与
rt_process
几乎相同的无限循环,唯一的区别是它不检查来自
check
任务的消息。停止内核空间任务的唯一合理方法是移除模块。
在
latency()
函数中使用
rt_printk()
函数,因为
printk()
函数在 RTAI 任务中使用不安全。
printk()
函数认为它已经禁用了中断,但实际上只禁用了 Linux 中断,而“真正”的中断由 RTAI 控制。
rt_printk()
函数在 RTAI 级别管理中断,因此可以在 RT 任务中安全使用。
运行该程序后,即使在加载 Netscape 浏览器时,延迟值也都为零。检查代码可以发现,
latency()
函数中的算法与
rt_process.c
中的循环算法相同,这表明我们在内核空间实现了真正的硬实时性能。
5. RTAI /proc 文件
安装
rtai
模块后,会在
/proc
目录下发现一个新的子目录
/proc/rtai
。每个 RTAI 模块都会在
/proc/rtai
中创建自己的文件,用于传达其状态信息。例如,在安装
rtai_lxrt
前后查看
/proc/rtai/scheduler
文件,该文件列出了实时任务及其相关的有用信息。安装
rtai_lxrt
后,列表中会显示 16 个任务。查看
/proc/rtai/lxrt
文件也会发现它创建了 16 个任务。启动
rt_process
后再次查看
/proc/rtai/lxrt
文件,会发现列表中增加了两个对象:“LATCAL”任务和“LATMBX”邮箱。
6. 实时 FIFOs 和共享内存示例
进入
$(HOME/)Rtdemos/FIFO
目录,打开
data_acq.c
文件,这是一个伪数据采集应用程序,使用 RTAI FIFO 和共享内存区域与日志记录进程进行通信。
6.1 数据采集任务
data_acq
程序支持多个通道,可以通过
data_acq.h
中定义的
channel_t
数据结构控制采集过程,例如可以独立设置每个通道的“采样率”和“增益”。在这个伪版本中,
data_acq
会在每个通道上以指定的速率和范围生成锯齿波形。
在
init_module()
函数(第 89 行)中,首先创建一个 FIFO 用于将数据从 RT 任务传输到日志记录器,FIFO 由一个整数标识,大小任意设置为 1024 字节。然后创建一个 FIFO 信号处理函数,该函数是一个回调函数,每当 FIFO 的用户空间端被访问时都会被调用。
由于数据 FIFO 可能会填满,特别是在
data_acq
任务在日志记录器之前启动的情况下,
data_acq
任务可以在信号量上挂起,直到日志记录器从 FIFO 中读取数据,从而腾出空间。FIFO 信号处理函数在数据 FIFO 被读取且
data_acq
任务挂起时会释放信号量。
init_module()
函数还会创建该信号量。
接下来,为
channel_t
控制结构分配共享内存区域,使用
rtai_kmalloc()
函数,这是 RTAI 安全版本的
kmalloc()
函数。RTAI 共享内存使用与 LXRT 相同的对象命名空间,以便用户空间进程可以访问共享内存。最后,初始化并启动
data_acq
任务。
data_acq
任务从第 45 行开始,按照约定,如果通道的
sample_period
字段为 0,则该通道未启用采集功能,所有通道初始化为禁用状态,日志记录器会为每个通道设置适当的操作值。在主循环中,每次任务唤醒时,会遍历
channel[]
数组中的所有通道,查找需要生成数据样本的通道。对于每个采样周期已到期的通道,会填充一个
data_point_t
结构(包含通道号、数据值和时间戳),并将其发送到数据 FIFO。
rtf_put()
函数返回写入的字节数,如果 FIFO 填满,写入的字节数可能小于请求的字节数,此时
data_acq
任务会在信号量上挂起。
由于
data_point_t
记录为 16 字节,是 FIFO 大小的整数子倍数,因此要么写入完整的记录,要么不写入任何内容。同样,在日志记录器端,要么读取完整的记录,要么 FIFO 为空。当 FIFO 填满时,当前数据点会被丢弃。
6.2 日志记录进程
查看
logger.c
文件,它以一个
channel_t
结构数组初始化四个通道,使用信号处理函数拦截
Control-C
信号,以便优雅地终止程序。日志记录器将从
data_acq
接收的数据写入磁盘文件,文件名作为运行时参数传递。注意,FIFO 作为普通文件以只读方式打开。
综上所述,RTAI 提供了丰富的机制来实现实时任务的通信和同步,通过 LXRT 可以在用户空间进行调试,最终迁移到内核空间实现硬实时性能。同时,RT FIFOs 和共享内存为实时任务与用户空间进程之间的通信提供了有效的解决方案。不同的定时器模式可以根据具体需求选择,以平衡定时器开销和任务调度的灵活性。
RTAI 环境:实时任务通信与性能优化
7. RTAI 通信机制总结与对比
为了更清晰地理解 RTAI 提供的各种通信机制,我们对它们进行总结和对比。
| 通信机制 | 适用场景 | 特点 | 操作方式 |
|---|---|---|---|
| 消息传递(Messaging) | 内核空间实时任务之间 |
直接任务到任务通信,可发送单个整数,有全双工版本“远程过程调用”,在
rtai_sched
模块实现
| 发送任务使用相应函数发送整数,接收任务等待消息 |
| RT FIFOs | 实时任务与用户空间 Linux 进程通信,数据量相对较小且需同步传输 | 类似 Unix 管道,通常为单向,用户空间进程视为字符设备,实时任务通过特定 API 访问 |
用户空间用
open
、
read
、
write
操作,实时任务用 RTAI 特定 API
|
| 共享内存 | 多个进程访问单个实时任务生成的数据,或大量数据快速移动 | 数据无需复制,实时任务写入,进程读取 |
使用
rtai_kmalloc
分配,通过命名空间访问
|
从这个表格中可以看出,不同的通信机制适用于不同的场景。消息传递主要用于内核空间任务间的简单通信;RT FIFOs 在实时任务和用户空间进程间小数据同步传输中表现出色;共享内存则在处理大数据量或多进程访问时更具优势。
8. LXRT 性能分析与应用建议
8.1 性能分析
通过前面的实验我们发现,传统的 LXRT 属于“软”实时,在启动如 Netscape 这样的程序时,最大偏差值会变得很糟糕,说明其在确定性性能方面存在不足。但 LXRT 提供了调试的便利,因为可以使用 DDD 等调试工具观察程序运行情况,这对于开发初期定位问题非常有帮助。
而后期版本的 LXRT 通过
rt_make_hard_real_time()
函数可以实现接近硬实时的性能,在执行
./rt_process h
后,即使启动 Netscape 浏览器,延迟也能降低到几十微秒甚至更低,大大提高了实时性。
8.2 应用建议
- 在开发初期,建议使用 LXRT 进行调试。利用其可以在用户空间运行实时任务的特性,方便使用调试工具,快速定位和解决代码中的问题。
- 当需要更高的实时性能时,可以考虑将任务迁移到内核空间。但在迁移之前,可以先使用 LXRT 的硬实时功能进行测试,评估是否能满足实际需求。如果 LXRT 的硬实时性能能够满足要求,且开发和维护成本较低,那么可以继续使用 LXRT;如果仍然无法满足需求,则需要将任务迁移到内核空间。
9. 定时器模式的选择流程
对于 RTAI 的周期性和一次性定时器模式,我们可以通过一个流程图来帮助选择合适的模式。
graph TD
A[开始] --> B{是否需要精细的任务调度粒度?}
B -- 是 --> C{能否接受较高的定时器开销?}
B -- 否 --> D[选择周期性模式]
C -- 是 --> E[选择一次性模式]
C -- 否 --> D
D --> F[结束]
E --> F
这个流程图展示了选择定时器模式的基本思路。首先判断是否需要精细的任务调度粒度,如果不需要,那么周期性模式就可以满足需求;如果需要,再考虑是否能够接受较高的定时器开销。如果可以接受,那么选择一次性模式;如果不能接受,还是选择周期性模式。
10. 内核空间与用户空间任务的切换考量
在使用 RTAI 时,可能需要在用户空间(通过 LXRT)和内核空间之间切换任务。以下是一些需要考量的因素:
10.1 开发难度
- 用户空间(LXRT) :开发难度相对较低,因为可以使用常见的调试工具,如 DDD、GDB 等,方便进行代码调试和错误排查。同时,用户空间的代码与普通的 Linux 进程代码类似,开发人员更容易上手。
- 内核空间 :开发难度较高,因为内核代码的调试比较困难,不能直接使用普通的调试工具。而且内核空间的代码对系统的稳定性和安全性影响较大,一个小的错误可能会导致系统崩溃。
10.2 实时性能
- 用户空间(LXRT) :传统的 LXRT 是“软”实时,实时性能有限。虽然后期版本提供了硬实时功能,但仍然可能受到系统其他进程的影响。
- 内核空间 :可以实现真正的硬实时性能,因为内核空间的任务具有更高的优先级,能够更及时地响应事件,减少延迟。
10.3 资源管理
- 用户空间(LXRT) :用户空间进程的资源管理相对简单,由操作系统负责内存分配和回收,进程之间的资源隔离较好。
- 内核空间 :内核空间需要手动管理资源,如内存分配和释放,需要更加谨慎,否则可能会导致内存泄漏等问题。
综合考虑以上因素,如果对实时性能要求不高,且注重开发的便利性和调试的可行性,那么可以选择在用户空间使用 LXRT 进行开发;如果对实时性能有严格的要求,且开发人员具备足够的内核开发经验,那么将任务迁移到内核空间是更好的选择。
11. 总结与展望
RTAI 为我们提供了一套强大的实时任务通信和同步机制,涵盖了内核空间和用户空间的多种应用场景。通过消息传递、RT FIFOs 和共享内存等机制,实现了实时任务之间以及实时任务与用户空间进程之间的高效通信。
LXRT 作为一种过渡方案,在开发初期提供了调试的便利,同时后期版本的硬实时功能也为用户空间的实时应用提供了更多的可能性。而定时器模式的选择则可以根据具体需求平衡定时器开销和任务调度的灵活性。
在未来的实时系统开发中,我们可以进一步探索 RTAI 的更多功能,结合不同的通信机制和定时器模式,开发出更加高效、稳定的实时应用。同时,随着硬件技术的不断发展,我们也可以期待 RTAI 在性能和功能上的进一步提升,为实时系统的发展提供更强大的支持。
总之,RTAI 为实时系统开发提供了丰富的工具和方法,我们需要根据具体的应用场景和需求,合理选择和使用这些工具,以实现最佳的实时性能和开发效率。
超级会员免费看
1094

被折叠的 条评论
为什么被折叠?



