Appearance
03 实时性与任务调度
位置:
08-ros / 03_高级话题 / 03_实时性与任务调度.md难度:⭐(高 —— 入门门槛高,面向进阶读者) 前置知识:Linux 线程/调度基础、C++ 内存管理、02_线程模型与Executor深度解析。 交叉引用:模块二 · 核心模块、04_组件化与进程内通信。
1. 概述
ROS 2 常被说成"real-time capable",但它不是实时操作系统。真实含义是:配合合适的 Linux 内核(PREEMPT_RT)、调度策略与编程纪律,ROS 2 可以做到有界的延迟(bounded latency)。本文讲清"实时"在 ROS 2 语境下的三层含义与落地手段:
- 定位:ROS 2 只提供"可被用于实时"的接口,不承诺实时。
- 手段:OS 层(PREEMPT_RT + SCHED_FIFO + 隔离核)+ 编程纪律(禁 malloc/例外/IO)+ 实时执行器。
- 度量:cyclictest / rdtsc 打点,以及 DDS 层对延迟的贡献分解。
2. 核心概念
2.1 "Real-time capable" 的精确含义
实时 = 延迟有上界(worst-case latency 可预测),不是"快"。ROS 2 的定位是:提供不破坏实时性的 API(预分配接口、无锁 waitset、可禁日志),但不保证默认配置下实时。
2.2 RT 线程纪律(核心)
RT 线程内禁止可能"无界阻塞或分配"的操作:
- 禁
malloc/new(堆分配可能触发缺页、锁竞争、系统调用)。 - 禁 C++ 异常抛出路径(展开栈可能分配)。
- 禁阻塞 IO /
printf/std::cout(stdout 可能阻塞、也可能走锁)。 - 禁无界循环、动态容器扩容。
- 用预分配环形缓冲传递数据,用优先级继承互斥锁(
PTHREAD_PRIO_INHERIT)避免优先级反转。
2.3 OS 层机制
- PREEMPT_RT:给 Linux 内核打实时补丁,把大部分不可抢占区间改成可抢占,降低调度延迟。
- SCHED_FIFO / SCHED_RR:实时调度策略,配合
chrt/sched_setscheduler提升线程优先级。 - cpu_isolate / isolcpus / cpuset:把某些 CPU 从通用调度中隔离出来,专供实时线程。
2.4 实时执行器
rclcpp 主 API 不承诺实时;社区提供两类替代:
rclcpp::experimental::executors::EventsExecutor:事件驱动(而非每轮扫描),面向实时。StaticSingleThreadedExecutor:实体集合编译期/启动时固定,省运行时分配。rclc(rclc_executor):面向微控制器的 C 执行器,明确以实时为目标。
说明:这些执行器是否满足你的 worst-case 要求,须用 cyclictest + 应用打点实测,不能只看文档声称。
3. 技术原理(架构 / 源码级)
3.1 为什么默认 executor 不实时
SingleThreadedExecutor 每轮循环涉及:
rcl_wait:底层 DDSWaitSet::wait()的唤醒延迟取决于 DDS 实现与系统调度延迟(RT 内核下可压到微秒级,普通内核下毫秒级抖动)。rcl_take:Fast DDStake_next_sample会反序列化到 ROS message,可能触发堆分配(取决于类型是否固定大小、是否有预分配 allocation)。- 回调内若用户代码
new/std::string/日志,直接破坏有界性。
EventsExecutor 的改进在于:用事件队列 + 预分配 Executable 对象,减少每轮动态行为。但它仍是 experimental 命名空间,API 可能变动。
3.2 数据流与内存分配分析(实时视角)
以"传感器回调 → 处理 → 发布"为例:
- 理想实时路径:数据进入预分配环形缓冲(无锁 SPSC)→ RT 线程唤醒(guard condition 触发,O(1)、不分配)→ 处理(纯栈 + 预分配对象)→ 发布。
- 破坏点定位:任何
std::vector::push_back(扩容)、std::string赋值、rclcpp::Clock::now()之外的日志宏、std::shared_ptr拷贝(原子自增)都可能引入分配或锁。 - 跨进程链路:序列化 1 次拷贝 + SHM 拷贝 + 反序列化 1 次拷贝(见 04)。RT 敏感时优先用 intra-process + 唯一所有权指针 跳过序列化。
3.3 OS 层延迟构成
| 环节 | 典型量级(待验证,随硬件/内核版本差异巨大) | 手段 |
|---|---|---|
| 调度延迟(普通内核) | 毫秒级抖动 | 换 PREEMPT_RT |
| 调度延迟(PREEMPT_RT) | 微秒~数十微秒 | SCHED_FIFO + 优先级设计 |
| 中断处理 | 微秒级 | 隔离核 + IRQ affinity |
| 缺页(首次访问内存) | 可能数百微秒~毫秒 | mlockall 锁内存 |
| DDS 序列化+传输 | 微秒~毫秒 | intra-process / SHM |
⚠️ 上表数字仅示意数量级,必须待验证(不同 CPU、内核、DDS 版本差异极大);切勿直接引用作为性能结论。
3.4 C++ 侧破坏 RT 的 API 与替代
| 破坏 RT | 替代 |
|---|---|
new / delete | 预分配对象池、placement new、环形缓冲 |
std::string 拼接/赋值 | 固定长度 char[N]、预分配 string + resize 不缩容 |
std::vector/std::map 动态增长 | 预分配 + reserve、固定数组 |
std::cout/printf | 无锁日志环、关日志(RCUTILS_LOG_* 关闭) |
普通 std::mutex | 优先级继承 mutex(PTHREAD_PRIO_INHERIT) |
| 异常抛出 | 编译 -fno-exceptions(部分 ROS 库不支持,需验证) |
std::shared_ptr 高频拷贝 | 栈对象 / unique_ptr / raw 引用 |
3.5 DDS 层延迟贡献分解
- 序列化:CDR 编码为固定 CPU 开销,随消息大小线性增长。
- 拷贝:RELIABLE QoS 会多一次历史缓存写入/读取。
- 发现/握手:首次通信有秒级发现延迟(不影响稳态,但影响首包)。
- 网络栈:UDP 下的 socket 缓冲、分片重组;SHM 可省网络栈(见 06 的共享内存对比)。
- 可靠性:
RELIABLE需 ACK/NACK,BEST_EFFORT延迟更低但丢包;实时控制常用BEST_EFFORT+ 高频率刷新。
4. 实践指南
4.1 内核与调度配置(bash)
bash
# 1) 确认是否 PREEMPT_RT
uname -r
grep PREEMPT_RT /boot/config-$(uname -r) # 期待 CONFIG_PREEMPT_RT=y
# 2) 隔离核(启动参数 isolcpus=2,3;运行时也可用 cpuset)
sudo sh -c 'echo 2-3 > /sys/fs/cgroup/cpuset/rt/cpuset.cpus'
# 3) 用实时优先级运行节点(SCHED_FIFO,优先级 80)
sudo chrt -f 80 ros2 run my_pkg my_node
# 4) 锁住内存,避免缺页(进程内 mlockall,或外部)
sudo setcap cap_ipc_lock+ep $(which ros2) # 授权 mlock4.2 进程内设置 SCHED_FIFO + mlockall(C++)
cpp
#include <pthread.h>
#include <sched.h>
#include <sys/mman.h>
void pin_and_prioritize(int cpu, int priority) {
cpu_set_t cpuset; CPU_ZERO(&cpuset); CPU_SET(cpu, &cpuset);
sched_setaffinity(0, sizeof(cpuset), &cpuset);
struct sched_param sp{};
sp.sched_priority = priority; // 1~99,越大越高
// SCHED_FIFO;带 PTHREAD_EXPLICIT_SCHED 时也需 pthread_setschedparam
sched_setscheduler(0, SCHED_FIFO, &sp);
mlockall(MCL_CURRENT | MCL_FUTURE); // 锁物理内存,防缺页
}4.3 预分配环形缓冲(无锁 SPSC 示意)
cpp
// 示例代码,需根据实际框架调整;固定大小 + 原子索引,无分配
template <typename T, size_t N>
struct SPSC {
std::array<T, N> buf_{};
std::atomic<size_t> head_{0}, tail_{0};
bool push(const T& v) {
size_t t = tail_.load(std::memory_order_relaxed);
size_t next = (t + 1) % N;
if (next == head_.load(std::memory_order_acquire)) return false; // 满
buf_[t] = v;
tail_.store(next, std::memory_order_release);
return true;
}
};4.4 cyclictest 测量调度延迟
bash
# 在隔离核上测调度延迟:-p 优先级 -a 亲和核 -m 锁内存 -l 循环 -i 间隔 -h 输出直方图
sudo cyclictest -p 90 -a 2 -m -l 1000000 -i 200 -h 500 -q > hist.txt
# 关注 Max Latency 与直方图尾部分布;目标:worst-case 在可接受上界内4.5 应用内 rdtsc 打点(测端到端延迟)
cpp
#include <x86intrin.h>
inline uint64_t rdtsc() { return __rdtsc(); }
uint64_t t0 = rdtsc();
do_work();
uint64_t dt = rdtsc() - t0; // 换算:dt / 主频(GHz) ≈ 纳秒5. 方案对比
| 方案 | 实时性 | 复杂度 | 适用 |
|---|---|---|---|
| 默认 SingleThreadedExecutor + 普通内核 | 无保证 | 低 | 非实时 |
| StaticSingleThreaded / EventsExecutor + PREEMPT_RT | 有界(需实测) | 中 | 实时节点 |
| rclc executor(C) | 面向微控制器 | 中 | 嵌入式 |
| 自定义无锁线程 + 环形缓冲(绕过 executor) | 可控最强 | 高 | 极致实时 |
绝对不适用场景:把同一个实时 executor 同时用于控制回路(10kHz、微秒级要求)和日志/调试/大文件 IO 回调——一次
printf或malloc就能把整个 executor 的 worst-case 拖到毫秒级,实时性名存实亡。实时路径必须物理隔离(独立线程/独立核/独立执行器)。
6. 工具链
cyclictest(rt-tests 包):测调度延迟金标准。文档:https://manpages.ubuntu.com/manpages/focal/en/man8/cyclictest.8.htmlchrt/taskset/numactl:调度与亲和配置。perf sched/ftrace/trace-cmd:观测调度与抢占事件。rdtsc/chrono::steady_clock打点:应用内延迟测量。stress-ng:制造负载,验证抗干扰。ros2 trace+ LTTng:端到端链路级延迟分析(Tracepoints)。
7. 参考资料
- 实时系统提案(design.ros2.org):https://design.ros2.org/articles/realtime_proposal.html
- cyclictest 手册页:https://manpages.ubuntu.com/manpages/focal/en/man8/cyclictest.8.html
- rt-tests 项目:https://git.kernel.org/pub/scm/utils/rt-tests/rt-tests.git(⚠️ 具体 commit 地址来源待验证,以 kernel.org 仓库为准)
- PREEMPT_RT 官方:https://wiki.linuxfoundation.org/realtime/(⚠️ 来源待验证,建议以 kernel.org / RT 邮件列表为准)
- 实验性 EventsExecutor:https://docs.ros.org/en/iron/p/rclcpp/generated/classrclcpp_1_1experimental_1_1executors_1_1EventsExecutor.html
- rclc executor:https://github.com/ros2/rclc
8. 学习路径
- 用
cyclictest分别在普通内核与 PREEMPT_RT 上测,直观感受调度延迟差异。 - 写一个
SCHED_FIFO + isolcpus + mlockall的 demo 节点,用chrt/htop验证亲和与优先级。 - 在回调里故意加
std::cout,用打点观察 worst-case 劣化,理解"纪律"的含义。 - 尝试
EventsExecutor/StaticSingleThreadedExecutor,对比默认执行器。 - 结合 04_组件化与进程内通信 用 intra-process 消除序列化延迟。
9. 核心面试三问
"ROS 2 是实时的"这句话对吗?"real-time capable" 到底承诺了什么? → 不对。ROS 2 只是"可被用于实时":提供预分配接口与无锁 waitset,但不保证默认配置实时。实时 = 延迟有上界,需 PREEMPT_RT + SCHED_FIFO + 编程纪律 + 实测才成立。
RT 线程里为什么禁止
malloc和printf?它们的"无界"体现在哪? →malloc可能触发缺页、堆锁竞争、甚至系统调用(brk/mmap),延迟无上界;printf/std::cout走 stdout 可能阻塞在 IO 或内部锁上。二者都让 worst-case 不可预测,故用预分配缓冲与无锁日志环替代。DDS 的
RELIABLE和BEST_EFFORT对端到端延迟的影响差异是什么? →RELIABLE有 ACK/NACK 与重传握手,首包/丢包重传会引入额外延迟与抖动;BEST_EFFORT无确认,延迟更低更稳定但会丢数据。实时控制常用BEST_EFFORT+ 高频刷新来掩盖丢包。