上个月调试一个机械臂控制节点。现象非常诡异本地测试 5ms 控制周期稳如泰山一上产线偶尔跳到 20ms、30ms极端情况直接急停我们做了所有标准实时优化PREEMPT_RT 内核线程 SCHED_FIFO 99mlockall 预分配内存CPU 绑定问题依然存在。最后用 perf 抓火焰图问题才彻底暴露控制回调根本不是被系统调度打断而是被 Executor 串行调度阻塞。一、问题的根源Executor 串行模型很多人写 ROS2 节点时会这样写auto node std::make_sharedrclcpp::Node(control_node); // 5ms 控制回调 auto control_timer node-create_wall_timer( std::chrono::milliseconds(5), []() { control_loop(); } ); // 50ms 感知回调 auto perception_timer node-create_wall_timer( std::chrono::milliseconds(50), []() { auto result heavy_computation(); // 耗时 50ms } ); rclcpp::spin(node);看起来完全没问题。但默认的rclcpp::spin(node)使用的是rclcpp::executors::SingleThreadedExecutor它的行为是所有回调在同一个线程串行执行先执行的回调必须执行完下一个才能执行这意味着感知回调执行 50ms控制定时器即便 5ms 到期也必须等待 50ms 回调结束控制周期瞬间变成 50ms这和线程优先级无关。二、为什么优先级 99也没用很多人说我已经把线程设成 SCHED_FIFO 99 了。问题在于优先级影响的是线程Executor 只有一个线程回调在该线程内部串行执行也就是说优先级无法改变 Executor 内部的回调顺序。在单线程 Executor 中spin() ├── perception_callback() └── control_callback()回调顺序由 Executor 决定而不是由 Linux 调度器决定。所以优先级再高也没用并不是因为实时优先级无效而是回调根本没有机会被调度。三、多线程 Executor 就安全吗很多人看到这里会说那我用 MultiThreadedExecutor 不就行了事情没那么简单。1. 默认回调组是互斥的所有回调默认属于MutuallyExclusive CallbackGroup即便使用rclcpp::executors::MultiThreadedExecutor executor(4);如果两个回调在同一个 callback group 里它们仍然不会并行执行。必须显式创建两个回调组auto control_group node-create_callback_group( rclcpp::CallbackGroupType::MutuallyExclusive ); auto perception_group node-create_callback_group( rclcpp::CallbackGroupType::MutuallyExclusive );否则多线程 executor 等于白用。2. 锁竞争带来的不可预测延迟假设你这样写std::mutex mutex; void control_callback() { std::lock_guardstd::mutex lock(mutex); control_logic(); } void perception_callback() { std::lock_guardstd::mutex lock(mutex); heavy_computation(); }即使回调在不同线程运行控制回调仍可能卡在 mutex 上。这种延迟不可预测与输入数据有关与线程切换有关工业现场我见过锁竞争导致控制回调延迟 200ms。多线程不是银弹。四、常见问题在产线中常见三类致命问题问题1控制回调被串行阻塞单线程 executor 下最典型。问题2多线程锁竞争延迟不可预测。问题3误以为 callback group 自动隔离默认不隔离。五、解决方案重点来了。工业实时控制不能依赖 Executor 调度。正确做法是控制循环必须独立于 Executor。方案一独立控制线程class ControlNode : public rclcpp::Node { std::thread control_thread_; std::atomicbool running_{true}; public: ControlNode() : Node(control_node) { control_thread_ std::thread([this]() { // 建议设置实时调度 绑核 const auto period std::chrono::milliseconds(5); auto next std::chrono::steady_clock::now(); while (running_ rclcpp::ok()) { control_loop(); next period; std::this_thread::sleep_until(next); } }); } ~ControlNode() { running_ false; control_thread_.join(); } };注意独立线程 ≠ 自动实时。必须配合SCHED_FIFOCPU 绑定mlockall关闭 CPU 频率调节限制中断干扰否则仍可能抖动。但至少你摆脱了 Executor 调度干扰。方案二进程级隔离更成熟的工业方案是控制节点独立进程感知节点独立进程不共享 executor不共享内存优势无锁竞争无回调互相阻塞CPU 绑核清晰进程级优先级隔离这是大型机器人系统常见架构。六、实时控制的三层防线工业系统真正稳定的实时模型是第一层操作系统PREEMPT_RTSCHED_FIFOCPU 绑定关闭 C-statemlockall第二层内存与数据结构预分配无锁结构环形缓冲区无动态分配第三层调度架构控制线程独立Executor 不参与控制周期回调组显式隔离进程级分离Executor 只是第三层的一部分。七、如何验证用 perfsudo perf record -F 99 -p $(pgrep control_node) -g -- sleep 30 sudo perf script | stackcollapse-perf.pl | flamegraph.pl flamegraph.svg检查控制循环是否被 executor 调度函数包裹是否频繁出现在 mutex 等待上是否出现长时间 sleep 延迟如果控制逻辑栈经常排在其他回调之后说明仍然被调度模型影响。八、结论Executor 不是实时调度器单线程 executor 串行执行所有回调多线程 executor 可能引入锁竞争callback group 默认不隔离控制循环不应依赖 executor timer实时控制必须线程或进程级独立如果控制周期依赖 Executor你的实时性就是偶然的。ROS2 非常强大但它的 Executor 设计目标是通用回调调度不是硬实时控制工业控制系统必须主动绕开这层调度。否则你以为自己在调 Linux 实时其实是在被 C 回调队列支配