
在前面的文章中我们已经从多个角度分析了 ROS 2。第一篇我们从 Node、Topic、Service、Action、DDS 和 QoS 入手建立了 ROS 2 的整体认知。第二篇我们进一步分析了一个 ROS 2 Node 从启动到执行的过程理解了 Node、Process、Thread、Callback 和 Linux 调度器之间的关系。第三篇我们讨论了 Topic、Service、Action 三种通信机制。第四篇则深入分析了 ROS 2 QoS理解了 Reliability、Durability、History、Deadline 等机制。如果把前面的内容串起来会发现一个非常重要的问题消息已经通过 DDS 到达了 ROS 2 节点那么接下来是谁来执行这个消息对应的代码例如摄像头 │ ▼ Camera Node │ │ Image ▼ 视觉处理 Node │ ▼ Callback消息到了 Subscriber并不意味着处理函数就已经开始执行。中间还存在一个非常关键的角色Executor。Executor 可以理解成 ROS 2 中负责“寻找哪些回调可以执行并安排它们执行”的核心机制之一。它连接了ROS 2通信 ↓ Callback ↓ Executor ↓ 线程 ↓ 操作系统调度 ↓ CPU这也是为什么当机器人系统从普通功能开发进入实时控制之后Executor 会变得越来越重要。因为对于一个机器人来说真正影响控制性能的并不只是“消息有没有收到”还包括“消息收到之后Callback什么时候开始执行”“多个Callback同时就绪时谁先执行”“一个Callback执行期间另一个Callback能不能抢占”“多个线程之间会不会发生竞争”“Executor的执行机制会不会产生额外延迟”这些问题最终都会进入一个更底层的问题ROS 2中的任务到底是怎么被调度起来的一、Executor到底是什么为什么ROS 2需要它先从最简单的程序开始。假设我们有一个 ROS 2 NodeRobotController它订阅两个 Topic/imu /joint_state同时还有一个 Timercontrol_timer程序大致可以理解为RobotController Node │ ├── /imu Subscriber │ └── imu_callback() │ ├── /joint_state Subscriber │ └── joint_callback() │ └── Timer └── control_callback()当系统运行起来之后这三个 Callback 并不是自动同时执行的。例如某一时刻IMU数据到达 ↓ imu_callback 可执行 Joint State到达 ↓ joint_callback 可执行 Timer到期 ↓ control_callback 可执行此时可能有三个“工作”都处于 Ready 状态。那么问题来了CPU到底先执行哪个这就是 Executor 要处理的问题之一。可以简单把 Executor 理解成ROS 2中的回调执行管理者。它不断关注哪些Callback已经准备好了然后决定接下来执行哪个Callback从概念上可以画成ROS 2 Node │ ┌─────────────┼─────────────┐ │ │ │ ▼ ▼ ▼ Subscriber Subscriber Timer │ │ │ ▼ ▼ ▼ imu_callback joint_callback control_callback │ │ │ └─────────────┼─────────────┘ ▼ Executor │ ▼ Thread │ ▼ CPU所以Executor并不是一个简单的“while循环”。它实际上处于 ROS 2 应用层和底层操作系统调度之间。二、SingleThreadedExecutor所有Callback排队一个线程执行ROS 2 中最容易理解的 Executor 是SingleThreadedExecutor。顾名思义一个 Executor 使用一个线程来执行 Callback。假设我们有Callback A Callback B Callback C那么Executor │ ▼ 一个线程 │ ┌────────┼────────┐ ▼ ▼ ▼ Callback A B Callback C它不会让三个 Callback 同时运行。而是类似时间 → ────────────────────────────────── [A执行][B执行][C执行][A执行][B执行]假设A IMU处理 B 图像处理 C 控制算法那么在某一时刻A正在运行即使C已经Ready也不代表 C 可以立刻执行。它可能需要等待 A 执行完成。这就产生了一个非常重要的概念Executor层面的等待。例如T0 │ ├── IMU Callback Ready │ ├── Control Callback Ready │ └── Image Callback Ready如果 Executor 先执行Image Callback而图像处理用了20ms那么控制 Callback 即使已经 Ready也可能要继续等待。于是控制任务Ready │ │ 等待 ▼ 图像Callback执行 │ │ 20ms ▼ 控制Callback终于开始如果控制周期只有1ms那么这种等待就可能成为严重问题。这也说明Callback Ready并不等于Callback Running。三、MultiThreadedExecutor多个Callback可以并发执行为了解决单线程执行的限制ROS 2还提供MultiThreadedExecutor。它允许多个线程参与 Callback 执行。例如Executor │ ┌───────────┼───────────┐ ▼ ▼ ▼ Thread 1 Thread 2 Thread 3 │ │ │ ▼ ▼ ▼ Callback A Callback B Callback C这样时间 → ──────────────────────────── Thread 1: [A][A][A] Thread 2: [B][B] Thread 3: [C][C][C]多个 Callback 可以同时执行。例如Thread 1 └── Camera Callback Thread 2 └── LiDAR Callback Thread 3 └── Control Callback这样就可以降低某一个 Callback 长时间执行对其他 Callback 的影响。但是多线程并不意味着实时性问题自动解决。恰恰相反多线程会带来新的问题数据竞争锁竞争优先级反转CPU资源竞争Cache竞争Callback之间的同步问题不确定的执行顺序。例如Thread 1 │ ├── Callback A │ └── 修改共享变量 Thread 2 │ ├── Callback B │ └── 同时访问共享变量如果没有正确同步就可能出现A写数据 │ ├───────┐ │ │ ▼ ▼ B读取从而产生数据一致性问题。如果增加 MutexThread 1 │ ▼ Mutex Lock │ ▼ 共享资源 │ ▼ Mutex Unlock那么又可能出现Thread 1等待 Thread 2持有锁 Thread 3抢占最终演变成更加复杂的调度问题。所以MultiThreadedExecutor解决的是并发执行能力而不是直接提供确定性的实时调度。四、Callback Group为什么“多个线程”还不够理解 Executor还必须理解另一个概念Callback Group。因为在一个 Node 中并不是所有 Callback 都应该完全独立运行。例如Node │ ├── Callback A ├── Callback B ├── Callback C └── Callback D开发者可能希望A、B可以并发但是C、D不能同时执行这时候就需要 Callback Group 帮助定义这些 Callback 之间的执行关系。ROS 2中常见的 Callback Group 类型包括Mutually Exclusive ReentrantMutually Exclusive同一组Callback不能同时执行假设Group 1 ├── Callback A └── Callback B使用Mutually Exclusive那么A执行时 B不能同时执行即使系统使用的是MultiThreadedExecutor也不代表 A 和 B 可以无限制并行。可以理解为Thread 1 │ └── Callback A │ X │ Thread 2 └── Callback B等待这对于保护共享资源非常有意义。Reentrant允许并发执行如果 Callback Group 是Reentrant那么同一个 Group 内的 Callback 可以允许并发执行。例如Group ├── Callback A ├── Callback B └── Callback C可以出现Thread 1 → A Thread 2 → B Thread 3 → C但这里同样需要开发者保证代码本身具备线程安全性。所以Callback Group本质上是在告诉Executor这些Callback之间允许什么样的并发关系。五、Executor的真正问题Ready之后谁先执行现在把Node Subscriber Timer Callback Group Executor串起来。假设一个机器人控制 Node 同时存在IMU Callback Camera Callback Control Timer Diagnostics Callback某个时刻IMU → Ready Camera → Ready Control → ReadyExecutor面对的就是Ready Callbacks │ ┌───────┼────────┐ ▼ ▼ ▼ IMU Camera Control │ │ │ └───────┼────────┘ ▼ Executor │ ▼ 执行选择问题变成谁先运行这就是 ROS 2 实时系统里非常值得研究的问题。因为很多人理解 ROS 2 时会产生一种错觉消息来了 ↓ Callback执行实际上中间存在多个层次。更准确的链路应该是消息产生 ↓ DDS接收 ↓ ROS 2 Middleware ↓ Callback Ready ↓ Executor检测 ↓ Executor选择执行 ↓ 线程获得执行机会 ↓ Linux Scheduler调度 ↓ CPU开始执行 ↓ Callback真正运行任何一个环节出现延迟都可能影响最终响应时间。六、Executor和Linux调度器到底是什么关系这是理解 ROS 2 实时性的关键。很多文章把 Executor 直接称为“ROS 2 调度器”。这种说法容易造成误解。更准确地说Executor负责 ROS 2 Callback 的执行管理而真正决定线程何时获得 CPU 的是操作系统调度器。可以把两者理解成两个不同层次。ROS 2层 ────────────────────── Node Callback Callback Group Executor ────────────────────── 操作系统层 ────────────────────── Thread Scheduler Priority CPU Affinity IRQ CPU ──────────────────────Executor解决哪些 ROS 2 Callback 已经可以执行Linux Scheduler解决哪个线程现在可以获得 CPU举一个简单例子。假设Control Callback已经被 Executor 选中。但它所在的线程Control Thread并没有立刻运行。因为 CPU 此时可能正在执行高优先级线程或者IRQ处理或者其他系统任务于是就可能出现T0Control Callback Ready ↓ T1Executor选择 ↓ T2Control Thread Ready ↓ T3等待Linux Scheduler ↓ T4真正获得CPU ↓ T5Callback开始执行这里T0 → T1属于 ROS 2 Executor 层面的延迟。而T2 → T4更多涉及操作系统调度。这也是为什么仅仅修改 Executor并不能解决所有 ROS 2 实时性问题。七、为什么普通Linux上的ROS 2会出现“偶发卡顿”这时候我们可以解释一个机器人开发中非常常见的现象。系统平均运行完全正常控制周期 0.95ms 1.02ms 0.98ms 1.01ms但是偶尔突然出现5ms 10ms 20ms开发者可能会说“ROS 2偶尔卡一下。”但实际上“卡一下”并不一定是 ROS 2 本身的问题。整个执行链路都可能造成延迟。例如ROS 2 │ ▼ Executor │ ▼ Thread │ ┌─────┼─────┐ ▼ ▼ ▼ CPU IRQ Lock │ │ │ └─────┼─────┘ ▼ Scheduler │ ▼ CPU可能的原因包括1. Callback执行时间过长例如Camera Callback进行了大量图像处理5ms 10ms 20ms那么其他任务自然受到影响。2. 多线程之间发生锁竞争例如Control Thread │ ▼ Mutex ▲ │ Data Thread如果控制线程一直等待锁Control │ └── Waiting...即使它本身优先级很高也可能无法立即继续执行。3. CPU被其他任务占用例如CPU Core 3 │ ├── ROS 2 Control ├── Image Processing ├── Logging ├── Network ├── Kernel Task └── IRQ多个任务共享同一个 CPU 核。那么控制任务的执行时间就可能受到干扰。4. 中断干扰机器人设备往往存在大量硬件Ethernet USB Camera CAN PCIe Sensor Motor Controller这些设备都会产生中断。如果实时任务运行时受到大量 IRQ 干扰就可能出现控制任务 │ ▼ 运行 │ X │ IRQ │ ▼ 处理中断 │ ▼ 返回控制任务这就是为什么后续进行 ROS 2 实时优化时不能只研究 ROS 2。还需要研究Linux 调度器CPU AffinityIRQ AffinityCPU Core Isolation实时线程优先级锁内核抢占系统调用内存分配。八、从Executor进一步理解ROS 2实时控制现在假设我们有一个机器人控制周期1ms系统要求每1ms执行一次控制任务理想情况T0 T1 T2 T3 │ │ │ │ ▼ ▼ ▼ ▼ Control Control Control Control周期稳定1ms 1ms 1ms 1ms但是现实可能变成T0 T1 T2 T3 │ │ │ │ ▼ ▼ ▼ ▼ 0.9ms 1.1ms 3.2ms 0.8ms这就是Jitter周期抖动。平均值可能仍然很好Average ≈ 1ms但是Worst Case 3.2ms对于机器人控制来说后者往往更加值得关注。这时就需要进一步思考Executor ↓ Thread ↓ Scheduler ↓ CPU到底哪一层产生了抖动如果 Executor 本身造成了等待优化Executor执行策略如果线程调度存在较大延迟优化Linux实时调度如果 CPU 被大量任务干扰CPU Core Isolation CPU Affinity IRQ Affinity如果锁竞争严重重新设计Callback和共享资源因此ROS 2实时优化并不是一个单点问题。它更像是一条完整链路ROS 2 ↓ DDS ↓ QoS ↓ Executor ↓ Callback Group ↓ Thread ↓ Scheduler ↓ CPU ↓ Hardware真正的实时系统优化需要从整条链路进行分析。九、Executor设计与实时操作系统为什么底层能力最终会变得重要当机器人系统规模比较小时几个Node 几个Topic 普通Linux通常已经可以很好地完成开发和验证。但当系统进入更加复杂的实时场景几十个Node 大量Topic 多线程 多传感器 多轴控制 高速通信问题就会越来越复杂。例如一个人形机器人可能同时运行视觉 │ ├── Camera ├── Depth └── Object Detection 感知 │ ├── IMU ├── Force Sensor └── Joint State 决策 │ ├── Planner └── Behavior 控制 │ ├── Whole Body Control ├── Joint Control └── Motor Control 通信 │ ├── Ethernet ├── CAN └── DDS这些任务并不是同等重要。可以粗略理解成控制任务 ★★★★★ 状态估计 ★★★★ 规划 ★★★ 视觉 ★★ 日志 ★如果所有任务最终都在普通 Linux 环境下竞争 CPUControl Vision Planning Logging Network IRQ那么即使 ROS 2 本身没有明显问题也可能出现关键控制任务偶发延迟。这时就需要把关注点继续向下移动。从ROS 2 Executor深入Linux Thread再深入Linux Scheduler最终进入实时调度 CPU隔离 资源隔离 中断隔离 优先级管理这也是实时操作系统存在的价值之一。十、ROS 2 实时Linux不是替代ROS 2而是补足底层实时能力这里必须明确一个概念实时 Linux 和 ROS 2 并不是二选一。ROS 2解决的是机器人软件框架和模块通信问题。实时 Linux 解决的是底层任务执行和资源管理问题。二者处于不同层级。可以理解成┌──────────────────────────────┐ │ Robot Apps │ │ 感知 / 决策 / 控制 / 规划 │ ├──────────────────────────────┤ │ ROS 2 │ │ Node / Topic / DDS / QoS │ │ Executor / Callback Group │ ├──────────────────────────────┤ │ Real-Time Linux │ │ 调度 / 优先级 / 隔离 / IRQ │ ├──────────────────────────────┤ │ Hardware │ │ CPU / Memory / CAN / PCIe │ └──────────────────────────────┘在这种架构下ROS 2负责“机器人各个软件模块怎么协作。”实时 Linux负责“这些任务怎么更加确定地获得计算资源。”因此对于对实时性、确定性、资源隔离和可靠性要求更高的机器人系统望获rtLinux可以作为底层实时操作系统的一种技术选择与 ROS 2 上层软件架构结合。例如ROS 2 │ ┌───────────┼───────────┐ │ │ │ Perception Planning Control │ │ │ └───────────┼───────────┘ ▼ Executor │ ▼ Thread Model │ ▼ 望获rtLinux │ ┌──────────┼──────────┐ ▼ ▼ ▼ 调度 核心隔离 IRQ隔离 │ │ │ └──────────┼──────────┘ ▼ CPU这里最值得注意的不是简单地把ROS 2 望获rtLinux两个名字放在一起。真正有技术意义的是如何让 ROS 2 的任务执行模型与底层实时调度能力形成完整链路。例如ROS 2 Callback ↓ Executor ↓ Real-Time Thread ↓ CPU Core ↓ Real-Time Scheduling进一步还可以考虑控制任务 ↓ 专用CPU核心 ↓ 减少普通任务干扰 ↓ 降低调度抖动以及实时任务 ↓ 核心隔离 ↓ IRQ隔离 ↓ 资源隔离 ↓ 更确定的执行环境这类架构对于机器人控制系统的实时性设计具有重要意义。十一、理解Executor之后ROS 2的“实时性问题”就清晰了回顾整个 ROS 2 数据链路传感器 ↓ Publisher ↓ DDS ↓ QoS ↓ Subscriber ↓ Callback Ready ↓ Executor ↓ Callback ↓ Thread ↓ Linux Scheduler ↓ CPU现在我们就能准确回答QoS解决什么解决数据如何传输、保存以及在什么条件下认为通信异常。Executor解决什么解决哪些 Callback 可以执行以及如何组织 Callback 的执行。Linux Scheduler解决什么解决哪些线程获得 CPU以及线程之间如何进行调度。实时操作系统解决什么进一步提供面向确定性、低延迟和资源隔离的任务执行环境。因此ROS 2不是实时操作系统。Executor也不是Linux调度器。QoS更不等于实时性。这三个概念如果能够区分清楚基本就建立了理解 ROS 2 实时系统的核心框架。十二、从Executor出发下一步应该研究什么到这里ROS 2系列已经从Node一路走到了Topic ↓ DDS ↓ QoS ↓ Executor接下来就到了一个更加关键的问题如果 Executor 中存在多个 Callback而这些 Callback 最终又运行在线程上那么线程之间到底如何竞争例如高优先级控制线程 │ ▼ Mutex ▲ │ 低优先级数据线程如果低优先级线程拿着锁不释放而高优先级控制线程又必须等待这个锁就可能出现高优先级任务 ↓ 等待 ↓ 低优先级任务 ↓ 持锁这就是实时系统中非常经典的优先级反转Priority Inversion。而在 ROS 2 多线程系统中Callback Group、Executor、线程和共享资源又会让这种问题变得更加值得关注。所以接下来我们将继续往下深入《ROS 2多线程为什么会出现优先级反转从Callback Group到Linux锁机制》从一个最简单的 Mutex 开始继续分析Callback ↓ Callback Group ↓ Executor ↓ Thread ↓ Mutex ↓ Priority ↓ Priority Inversion也会进一步解释为什么一个看起来只是“锁一下数据”的操作最终可能影响整个机器人控制周期。