Skip to content

实时编程

实时计算是许多机器人系统的核心需求,尤其是安全关键和任务关键型应用,例如自动驾驶、航天器和工业制造。ROS 2 从设计之初就考虑了实时性能约束——这是 ROS 1 早期未涵盖的需求,而事后将 ROS 1 改造为实时友好的架构已经不现实。

这篇文档概述了实时计算的要求及软件工程师应遵循的最佳实践。核心要点如下:

构建实时系统,意味着实时循环必须在截止期限(deadline)内完成周期性更新,且只能容忍很小的误差(即允许的最大抖动)。为此,执行路径中必须避免一切非确定性操作,例如缺页中断、动态内存分配/释放,以及无限阻塞的同步原语。

倒立摆平衡是一个经典的控制问题,通常需要借助实时计算来解决。如果控制器意外阻塞过长时间,摆杆就会倒下或失稳。反之,只要控制器能以高于电机响应速度的频率稳定更新,就能及时处理传感器数据并保持平衡。

了解以上背景后,下面通过一个演示来体验实时编程。

该演示仅支持 Linux。ROS 社区中许多做实时计算的成员使用 Xenomai 或 RT_PREEMPT 作为实时方案,而演示中为优化性能所做的大量操作都依赖于 Linux 特有的机制。因此,macOS 和 Windows 用户请跳过这部分。

此外,本演示必须使用静态 DDS API 从源码构建。目前唯一支持的实现是 ConnextDDS。

首先,按照 从源码构建 ROS 2 的说明,以 Connext DDS 作为中间件进行编译。

运行前请确保系统至少有 8GB 可用内存。由于进程会锁定内存,swap 将不起作用。

source 你的 ROS 2 setup.bash:

Terminal window
source ./install/setup.bash

运行演示程序。如遇权限问题,可能需要加 sudo:

Terminal window
$ ros2 run pendulum_control pendulum_demo
Initial major pagefaults: 518
Initial minor pagefaults: 2139466
No results filename given, not writing results
rttest statistics:
- Minor pagefaults: 0
- Major pagefaults: 0
Latency (time after deadline was missed):
- Min: 1851 ns
- Max: 166796 ns
- Mean: 14229.182000 ns
- Standard deviation: 12288.040996

你可能会看到如下 stderr 输出:

Terminal window
mlockall failed: Cannot allocate memory
Couldn't lock all cached virtual memory.
Pagefaults from reading pages not yet mapped into RAM will be recorded.

演示程序在初始化完成后,会尝试通过 mlockall 将所有已缓存内存锁定在 RAM 中,并阻止后续的动态内存分配,以避免因大量新内存加载到 RAM 而触发缺页中断。(更多细节请参见实时设计文章。)

出现上述提示后,演示照常继续。你还可能看到类似如下的输出,反映执行期间发生的缺页中断数量:

rttest statistics:
- Minor pagefaults: 20
- Major pagefaults: 0

如果要消除这些缺页中断,需要做以下调整……

在 /etc/security/limits.conf 中添加(使用 sudo):

<your username> - memlock <limit in kB>

限制设为 -1 表示无限制。选择无限制时,可能还需要在编辑文件后配合执行 ulimit -l unlimited(以 root 身份)。

保存文件后,注销并重新登录,然后重新运行 pendulum_demo。

输出中将显示零缺页中断,或者一条 bad_alloc 异常的错误信息。如果是后者,说明可用内存不足以将进程分配的内存全部锁定到 RAM——只有增加物理内存才能实现零缺页中断。

要查看更多输出,需要先运行 pendulum_logger 节点。

在一个已 source install/setup.bash 的终端中,执行:

Terminal window
ros2 run pendulum_control pendulum_logger

你应该会看到输出消息:

Logger node initialized.

在另一个已 source 了 setup.bash 的终端中,再次运行 pendulum_demo。

该可执行文件启动后,前一个终端将不断打印输出:

Commanded motor angle: 1.570796
Actual motor angle: 1.570796
Mean latency: 210144.000000 ns
Min latency: 4805 ns
Max latency: 578137 ns
Minor pagefaults during execution: 0
Major pagefaults during execution: 0

该演示控制着一个简单的倒立摆仿真。倒立摆在独立线程中计算自身位置。一个 ROS 节点模拟摆的电机编码器并发布位置数据,另一个 ROS 节点充当简单的 PID 控制器,负责计算下一条控制指令。

logger 节点周期性地打印摆的状态以及演示在执行阶段的运行时性能统计。

pendulum_demo 运行结束后,按 Ctrl-C 退出 logger 节点。

pendulum_demo 执行期间,你将看到收集到的最终统计信息:

rttest statistics:
- Minor pagefaults: 0
- Major pagefaults: 0
Latency (time after deadline was missed):
- Min: 3354 ns
- Max: 2752187 ns
- Mean: 19871.8 ns
- Standard deviation: 1.35819e+08
PendulumMotor received 985 messages
PendulumController received 987 messages

延迟字段以纳秒为单位展示更新循环的最小、最大和平均延迟。这里的延迟是指更新预期发生时刻之后的滞后时间。

实时系统的要求因应用而异。假设本演示的更新循环频率为 1kHz(1 毫秒),我们的目标是最大延迟不超过更新周期的 5%。

本次运行的平均延迟很理想,但最大延迟不可接受——它实际上已经超过了更新周期。原因何在?

这很可能是非确定性调度器造成的。如果运行的是普通 Linux 内核且未安装 RT_PREEMPT 补丁,就很难达到预期的实时目标,因为 Linux 调度器不允许在用户态任意抢占线程。

更多信息请参见实时设计文章。

演示会尝试将调度器和线程优先级设置为适合实时性能的值。如果操作失败,将看到错误信息:「Couldn’t set scheduling priority and policy: Operation not permitted」。按下一节的说明调整权限后可以获得更好的性能:

在 /etc/security/limits.conf 中添加(使用 sudo):

<your username> - rtprio 98

rtprio(实时优先级)的取值范围是 0–99。但切勿将其设为 99,否则进程可能干扰以最高优先级运行的关键系统进程(例如看门狗)。本演示将尝试以优先级 98 运行控制循环。

演示运行结束后,可以绘制其中收集的延迟和缺页中断统计信息。

由于代码使用 rttest 进行插桩,因此支持以下命令行参数:

命令描述默认值
-i指定实时循环运行的迭代次数1000
-u指定更新周期,默认单位为微秒。使用后缀 “s” 表示秒,“ms” 表示毫秒,“us” 表示微秒,“ns” 表示纳秒1ms
-f指定写入收集数据的文件名

使用文件名再次运行演示以保存结果:

Terminal window
ros2 run pendulum_control pendulum_demo -f pendulum_demo_results

然后对结果文件运行 rttest_plot 脚本:

Terminal window
$ ros2 run rttest rttest_plot pendulum_demo_results
Writing results to file: pendulum_demo_results
...

该脚本将生成以下文件:

pendulum_demo_results_plot_latency.svg
pendulum_demo_results_plot_latency_hist.svg
pendulum_demo_results_plot_majflts.svg
pendulum_demo_results_plot_minflts.svg

可以用任意图片查看器打开这些图表。