ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

ros2_control为什么需要实时Linux?从read-update-write控制循环说起

ros2_control为什么需要实时Linux?从read-update-write控制循环说起 前面我们已经介绍了 ROS 2、DDS、Executor、CPU 核心隔离、内存管理、Zero-Copy以及 ros2_control 的基本架构。如果把这些内容全部串起来会发现一个非常有意思的现象ROS 2本身并不直接负责控制电机ros2_control负责把控制器与硬件连接起来而真正让这个控制循环“按时运行”的却是更底层的线程调度、CPU资源、内存管理、IRQ、驱动以及操作系统。所以当机器人从普通软件 Demo 进入真正的运动控制场景之后一个问题就变得非常关键为什么 ros2_control 往往需要一个具备实时能力的 Linux 运行环境很多人看到“实时 Linux”时第一反应是是不是CPU更快 是不是Linux换了一个更快的调度器 是不是给ROS 2线程设置一个高优先级就可以了实际上都没有这么简单。真正的实时性并不是让某一个程序“跑得更快”而是让一个具有明确时间约束的任务在系统受到其他任务、IRQ、内存、I/O和调度活动影响时仍然能够尽可能稳定地完成。对于 ros2_control 来说这个问题最终集中体现在一个非常简单的控制循环read ↓ update ↓ write ↓ 下一周期看起来只有三个步骤但如果控制频率达到 1 kHz那么意味着系统每秒需要完成 1000 次这样的循环。这时候任何一次异常调度、锁等待、内存操作、硬件通信延迟都可能成为控制周期中的变量。因此理解 ros2_control 为什么需要实时 Linux实际上就是理解一个 1 ms 的控制周期到底需要操作系统为它提供什么一、1 kHz控制循环到底意味着什么先从最简单的例子开始。假设一台机械臂的关节控制周期为1 kHz那么1秒 1000个周期 1个周期 1 ms也就是说控制系统每隔 1 ms 就需要完成一次读取状态 ↓ 计算控制量 ↓ 发送控制指令对应 ros2_controlread() ↓ update() ↓ write()假设一次理想控制周期read() 100 μs update() 200 μs write() 100 μs ------------------- 总计 400 μs从平均执行时间来看400 μs 1000 μs似乎还有 600 μs 的余量。但真正的实时系统不能只问平均执行时间是多少而必须继续问最坏情况下是多少例如正常周期 read 100 μs update 200 μs write 100 μs 总计 400 μs某一次系统受到干扰read 110 μs update 220 μs write 120 μs 调度延迟 300 μs 总计 750 μs仍然可能满足 1 ms。但是如果发生IRQ干扰 200 μs 锁等待 150 μs 调度延迟 300 μs 内存访问 100 μs整个周期就可能迅速逼近甚至超过 deadline。所以平均执行时间和最坏执行时间是两个完全不同的问题。对于普通应用平均性能通常非常重要。对于实时控制Worst-Case Latency Worst-Case Execution Time Jitter Deadline Miss更加关键。这也是实时 Linux 与普通 Linux 讨论方式上的根本区别。二、read → update → write的每一步都可能受到操作系统影响很多人理解 ros2_control 时会把它简单看成read() ↓ 控制算法 ↓ write()但真正运行时它其实是一个线程。例如Control Thread │ ↓ read() ↓ update() ↓ write()这个线程需要操作系统负责什么时候运行 运行多久 在哪个CPU运行 什么时候被抢占 是否会等待锁 是否受到IRQ影响 访问内存是否稳定因此控制循环是应用层定义的而控制循环能否稳定执行则受到操作系统调度和资源管理机制的影响。可以把它理解成应用层 ┌───────────────────┐ │ read/update/write │ └─────────┬─────────┘ ↓ 操作系统层 ┌───────────────────┐ │ Scheduler │ │ Thread │ │ Memory │ │ IRQ │ │ Synchronization │ └─────────┬─────────┘ ↓ 硬件层 ┌───────────────────┐ │ CPU │ │ Bus │ │ Driver │ │ Motor │ └───────────────────┘因此如果底层环境无法控制这些因素那么即使 ros2_control 本身写得非常规范整个控制周期仍然可能产生明显抖动。三、普通Linux为什么可能让控制线程“等一等”Linux 是一个非常强大的通用操作系统。它需要同时服务于ROS 2 浏览器 网络 日志 文件系统 AI 视觉 数据库 后台服务对于普通计算任务而言这种设计非常合理。但是对于一个 1 kHz 的实时控制线程来说系统真正关心的是当控制线程需要运行的时候它能不能及时获得 CPU假设当前 CPU 正在执行AI线程与此同时控制线程突然变成 Runnable。理想情况AI ↓ 控制线程立即获得CPU但真实系统还可能存在调度器决策 IRQ 内核活动 锁竞争 CPU迁移 Cache影响于是Runnable ↓ 等待 ↓ 真正开始执行这个时间差就是实时系统非常关注的Scheduling Latency。例如控制线程准备运行 t0 真正开始执行 t0 20 μs那么Scheduling Latency 20 μs如果偶尔变成t0 300 μs那么平均值可能仍然很好看但对于 1 ms 控制周期来说300 μs 已经占用了相当大的时间预算。这就是为什么实时系统并不是简单追求平均调度速度而是希望降低最坏情况下的调度延迟。四、SCHED_FIFO为什么经常出现在机器人实时控制中前面文章已经介绍过 Linux 的实时调度策略。这里把它放到 ros2_control 的实际场景中重新理解。例如Control Thread Priority 90同时Vision Thread Priority 50 Logging Thread Priority 20如果使用合适的实时调度策略例如SCHED_FIFO那么高优先级控制线程在满足调度条件时可以获得更强的调度优先级。逻辑上可以理解为Priority 90 控制 ↑ Priority 50 视觉 ↑ Priority 20 日志这样可以让关键控制任务优先于普通后台任务运行。但是这里一定要注意高优先级不等于完整实时性。因为控制线程仍然可能遇到IRQ 锁 内存 驱动 硬件通信 CPU资源竞争例如Control Thread Priority 90 ↓ 等待一个Mutex ↓ 低优先级线程持有Mutex这时候即使控制线程优先级是 90它也不能凭空获得被锁住的资源。这就是我们之前讨论过的优先级反转。因此实时调度和实时同步必须一起设计。五、为什么CPU核心隔离对ros2_control特别重要假设机器人计算平台有 8 个 CPU Core。系统同时运行Core 0~3 AI / Vision / SLAM Core 4~5 ROS 2普通任务 Core 6 Control Core 7 辅助实时任务那么可以进一步把Core 6作为主要控制核心。控制线程Control Thread ↓ CPU 6同时减少普通任务进入 CPU 6 的机会。这样做的目的不是让 CPU 6 “跑得更快”而是减少控制线程与非实时任务之间的竞争。这和 CPU Affinity 又有所不同。CPU Affinity线程允许在哪些CPU运行Core Isolation哪些普通任务尽量不要进入这个CPUIRQ Affinity硬件中断在哪个CPU处理三个概念必须区分。理想情况下CPU 6 ├── Control Thread ├── 相关实时任务 └── 必要的实时系统活动而CPU 0~5 ├── AI ├── Vision ├── SLAM ├── Planning ├── Network └── Logging这样就形成了相对清晰的计算域。对于机器人这种AI Vision ROS 2 Control同时运行的场景核心隔离尤其有意义。六、但CPU隔离以后为什么还会有实时问题这是理解实时 Linux 最容易出现误区的地方。很多人会认为控制线程 CPU独占就已经解决实时性。实际上远远不够。因为还有一个经常被忽略的角色IRQ。硬件设备产生中断之后Hardware ↓ IRQ ↓ CPU ↓ Interrupt Handler例如网卡 USB 存储 EtherCAT PCIe 传感器都可能产生中断。如果这些 IRQ 恰好进入控制 CPUCPU 6 Control Thread ↓ 正在执行 IRQ ↓ 插入执行那么控制线程仍然会被打断。因此真正的实时 CPU 隔离通常需要同时考虑CPU Affinity Core Isolation IRQ Affinity例如CPU 0~5 普通任务 大量IRQ CPU 6 实时控制 必要IRQ CPU 7 其他实时/系统任务这比简单地pthread_setaffinity_np()把线程绑定到 CPU 6 更完整。七、内存为什么又会成为新的问题假设我们已经完成SCHED_FIFO CPU 6隔离 IRQ合理配置控制周期还是可能出现抖动。为什么因为内存。例如控制线程执行std::vectordouble data; data.push_back(value);如果容量不足vector ↓ 重新申请内存 ↓ 复制数据 ↓ 释放旧内存那么一次控制周期的执行时间就可能发生变化。所以实时控制通常倾向于初始化阶段 ↓ 分配内存 ↓ 建立Buffer ↓ 建立Memory Pool ↓ 运行阶段 ↓ 尽量复用而不是每个控制周期 ↓ malloc ↓ 计算 ↓ free同样需要考虑Page Fault Cache Memory Bandwidth尤其是当 AI 和视觉任务同时运行时AI ↓ 大量内存访问 Vision ↓ 大量内存访问 Control ↓ 读取关键控制数据即使三个任务运行在不同 CPU 上底层内存系统仍然可能存在竞争。所以真正的实时资源隔离不是CPU隔离这么简单。而是CPU IRQ Memory Cache I/O共同考虑。八、硬件通信才是read/write真正的“最后一公里”如果只关注update()很容易忽略read() write()但对于机器人而言真正的数据最终都要到硬件。例如read() ↓ EtherCAT ↓ Servo Drive ↓ Encoder以及Controller ↓ write() ↓ EtherCAT ↓ Servo Drive ↓ Motor于是一个完整的控制周期实际上可能是┌────────────────────────────┐ │ 1 ms Control Loop │ │ │ │ read │ │ ↓ │ │ Hardware Communication │ │ ↓ │ │ Controller Update │ │ ↓ │ │ write │ │ ↓ │ │ Hardware Communication │ └────────────────────────────┘这意味着实时控制的延迟并不只发生在 ROS 2。它可能发生在ROS 2 ↓ Executor ↓ Scheduler ↓ Driver ↓ EtherCAT ↓ Motor任何一个环节产生额外等待都可能影响最终控制周期。例如控制周期 1 ms read 100 μs update 200 μs write 100 μs看起来很好。但是如果硬件通信偶尔read等待 300 μs那么整个周期预算就发生变化。因此工业机器人真正关注的是从读取状态到输出控制命令的整个闭环时间。而不是单独看某一个函数执行得多快。九、实时Linux真正解决的是什么现在可以重新回答实时 Linux 到底解决什么它并不是简单地让Linux跑得更快而是试图让系统中的关键实时任务获得更加可控的运行环境。对于 ros2_control可以从几个层面理解。第一层调度确定性控制线程需要及时获得CPU因此需要关注实时调度策略 线程优先级 调度延迟第二层CPU资源隔离避免AI Vision Logging Network频繁干扰Control因此需要CPU Affinity Core Isolation第三层IRQ管理避免大量非关键中断干扰实时 CPUIRQ Affinity Interrupt Management第四层内存确定性减少动态分配 Page Fault 不可预测内存访问并结合Memory Lock Preallocation Memory Pool第五层同步机制解决Mutex Priority Inversion Critical Section等问题。第六层硬件与驱动控制Driver DMA I/O Hardware Interrupt对实时控制链路的影响。所以一个完整的实时运行环境实际上是ros2_control │ ┌─────────┴─────────┐ ↓ ↓ Controller Hardware Interface │ │ └─────────┬─────────┘ ↓ RT Thread ↓ ┌───────────────────┐ │ Real-Time Linux │ │ │ │ Scheduler │ │ CPU Isolation │ │ IRQ Management │ │ Memory │ │ Synchronization │ └─────────┬─────────┘ ↓ Hardware这才是实时 Linux 在机器人控制中的真正意义。十、为什么“实时Linux ROS 2”不是简单叠加还有一个非常容易被误解的问题既然 ROS 2 是机器人软件框架实时 Linux 是实时操作系统那么是不是ROS 2 实时 Linux装在一起就自动获得实时控制能力当然不是。实时性最终是整个系统共同设计出来的。例如ROS 2如果 Executor 使用不合理回调阻塞 线程竞争仍然会产生问题。如果ros2_control控制器中频繁malloc new 日志输出 文件I/O仍然可能破坏实时性。如果CPU没有进行合理隔离AI Vision Control互相竞争仍然可能产生抖动。如果IRQ没有合理配置Network IRQ Storage IRQ USB IRQ仍然可能进入控制 CPU。所以真正的实时系统应该是一套完整方法任务分析 ↓ 周期设计 ↓ 线程设计 ↓ 调度策略 ↓ CPU隔离 ↓ IRQ隔离 ↓ 内存设计 ↓ 通信设计 ↓ 驱动设计 ↓ 实时OS ↓ 最终测量而不是简单安装一个软件包。十一、一个典型的ROS 2 ros2_control实时架构如果设计一套同时运行ROS 2 AI Vision SLAM Planning ros2_control的机器人系统可以考虑类似这样的逻辑架构┌──────────────────────────────────────────┐ │ Robot OS │ │ │ │ ┌──────────────────┐ │ │ │ Real-Time Zone │ │ │ │ │ │ │ │ ros2_control │ │ │ │ Controller │ │ │ │ Servo │ │ │ │ State Update │ │ │ └────────┬─────────┘ │ │ │ │ │ Dedicated CPU │ │ │ │ │ Real-Time Memory │ │ │ │ │ Controlled IRQ │ │ │ │ │ ↓ │ │ Hardware │ │ │ │ ┌───────────────────────────────────┐ │ │ │ Non-Real-Time Zone │ │ │ │ │ │ │ │ AI / Vision / SLAM / Planning │ │ │ │ Network / Logging / UI │ │ │ └───────────────────────────────────┘ │ └──────────────────────────────────────────┘这个架构的核心思想并不是所有 ROS 2 节点都必须实时。而是把真正需要严格时间约束的任务划入实时域。例如实时域 ├── Joint Control ├── Servo ├── Safety └── Hardware Update 非实时域 ├── Vision ├── AI ├── SLAM ├── Planning ├── UI └── Logging然后两者之间通过明确的通信边界进行数据交换。这实际上与前面讨论的CPU隔离 资源隔离 内存隔离 Zero-Copy全部联系起来了。十二、望获rtLinux在这套架构中的位置如果把整个机器人软件栈放在一起┌─────────────────────────────┐ │ Robot Application │ │ │ │ AI / Vision / Planning │ └──────────────┬──────────────┘ ↓ ┌─────────────────────────────┐ │ ROS 2 │ │ │ │ Node / DDS / Executor │ └──────────────┬──────────────┘ ↓ ┌─────────────────────────────┐ │ ros2_control │ │ │ │ Controller / Hardware │ └──────────────┬──────────────┘ ↓ ┌─────────────────────────────┐ │ Real-Time Layer │ │ │ │ Scheduler / Isolation │ │ Memory / IRQ / Resource │ └──────────────┬──────────────┘ ↓ ┌─────────────────────────────┐ │ Hardware │ └─────────────────────────────┘这里的实时操作系统并不是取代 ROS 2也不是取代 ros2_control。它解决的是另外一个层面的问题为上层实时任务提供更加确定的底层运行环境。对于需要高实时性、高可靠性以及更强资源隔离能力的机器人、工业控制和运动控制设备望获rtLinux可以作为这一层的一种实时 Linux 技术选择。尤其当系统开始从实验室Demo走向量产机器人 工业控制 高性能运动控制之后系统关注点也会从能不能跑变成最坏情况下能不能按时跑再进一步复杂任务同时运行时 关键控制任务能不能继续稳定按时运行这正是实时 Linux 所要解决的核心问题之一。十三、如何验证一个ros2_control系统到底“实时不实时”最后一定要回到测试。实时系统不能靠感觉判断。不能因为机器人动作很流畅就认为它具备实时性。也不能因为CPU利用率只有30%就认为没有问题。真正需要测试的是Control Cycle Time Scheduling Latency Worst-Case Latency Jitter Deadline Miss Page Fault IRQ Latency例如记录 100 万次控制周期周期 1000 999 1001 1000 998 1002 1000 ...然后得到平均周期1000.1 μs 最大周期1032 μs 最小周期991 μs进一步Jitter ≈ 最大偏差但如果出现最大周期1680 μs那么即使平均周期依然接近 1 ms也必须重点调查。进一步可以使用 Linux 的实时分析工具和 tracing 机制观察线程什么时候被唤醒 什么时候开始执行 被谁抢占 是否发生IRQ 是否等待锁 是否发生Page Fault最终形成Latency Histogram而不是只看一个平均数字。这也是实时系统开发与普通应用开发最大的区别之一实时性必须通过测量和压力测试证明而不是通过主观体验判断。十四、结语ros2_control真正考验的是整个系统现在再回头看read() ↓ update() ↓ write()它其实一点都不简单。因为每一次循环背后都可能涉及ROS 2 Executor ↓ Callback ↓ Thread ↓ Linux Scheduler ↓ CPU ↓ IRQ ↓ Memory ↓ Driver ↓ EtherCAT / CAN / PCIe ↓ Motor如果只是普通机器人应用偶尔慢一点可能并不会造成严重问题。但如果是1 kHz 运动控制那么一次偶发的调度延迟 一次锁等待 一次IRQ干扰 一次内存异常 一次驱动阻塞都可能改变控制周期。所以ros2_control并不是单独需要“更快的CPU”而是需要一个更加可控的运行环境。这也是为什么 ROS 2 实时控制最终会从Node一路深入Topic ↓ DDS ↓ Executor ↓ Thread ↓ Scheduler ↓ CPU ↓ IRQ ↓ Memory ↓ Driver ↓ Hardware而实时 Linux 的价值就是在这条链路的底层为关键任务提供更加面向确定性、实时调度和资源隔离的运行基础。对于望获rtLinux而言这也正是实时 Linux 技术可以与 ROS 2、ros2_control 结合的核心场景之一不是替代 ROS 2而是从操作系统层面支撑机器人实时控制任务。最终一个真正面向工业级机器人控制的系统可以概括成ROS 2 ↓ 机器人软件生态 ros2_control ↓ 标准化运动控制框架 实时Linux ↓ 确定性运行环境 实时硬件接口 ↓ 连接电机、传感器和执行器 Hardware ↓ 真正产生机器人运动从这个角度看ROS 2不是“实时系统”的终点而只是整个机器人实时计算栈中的一层。而下一步还可以继续追问一个更实际的问题如果 ros2_control 运行在 1 kHz控制线程到底应该如何设计优先级SCHED_FIFO、SCHED_RR 和 SCHED_DEADLINE 应该怎么与不同机器人控制任务结合下一篇可以直接进入“1 kHz机器人控制线程到底应该怎么调度”把SCHED_FIFO / SCHED_RR / SCHED_DEADLINE、控制周期、线程优先级、CPU核心隔离以及read-update-write放到同一个真实案例里分析。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进