上一篇文章我们从整体架构出发,介绍了 ROS 2 为什么会成为机器人软件开发的重要基础框架,并沿着Node、Topic、Service、Action、DDS、QoS、Executor这条技术链路,对 ROS 2 的基本组成进行了梳理。
如果把 ROS 2 看成一座城市,那么 Node 就像城市中的一个个功能单元。
摄像头需要一个 Node,激光雷达需要一个 Node,定位需要一个 Node,路径规划需要一个 Node,运动控制也需要一个 Node。
但真正开始开发 ROS 2 之后,一个看似简单的问题很快就会出现:
Node 到底是什么?
很多 ROS 2 入门教程会告诉我们:
Node 是 ROS 2 中最基本的执行单元。
这句话没有错,但如果只停留在这个层面,其实很难真正理解 ROS 2。
因为一个 Node 并不是凭空运行的。
一个 Node 启动以后,最终还是要变成操作系统中的进程、线程和任务;Node中的Subscription、Timer、Service等事件,又会转化成Callback;Callback最终交给Executor组织执行;Executor背后的线程最终还是需要从操作系统获得CPU时间。
也就是说,一个看起来简单的:
ROS 2 Node背后实际上连接着:
Node ↓ Process ↓ Thread ↓ Callback ↓ Executor ↓ Linux Scheduler ↓ CPU如果机器人只是做一个简单的Demo,这些底层细节可能暂时不重要。
但当机器人进入高速运动控制、工业机器人、机械臂、人形机器人、移动机器人等复杂场景以后,这条链路就会越来越重要。
因为机器人真正需要关注的,不仅是:
“这个Node能不能运行?”
还包括:
“这个Node什么时候运行?”
“它能不能按预期频率运行?”
“当多个Node同时工作时,谁优先执行?”
“一个Callback执行太久,会不会影响另一个关键Callback?”
“CPU被其他任务占用时,实时控制任务怎么办?”
而这些问题,已经从 ROS 2 应用层逐渐进入操作系统层。
所以,这一篇我们就从一个最基础的 ROS 2 Node 出发,一层层把它拆开,看看一个 Node 从启动到真正执行,中间到底发生了什么,以及为什么机器人实时性最终无法绕开操作系统。
一、ROS 2 Node到底是什么?为什么机器人软件需要Node?
在 ROS 2 中,Node 是非常核心的概念。
一个 Node 通常承担一个相对明确的功能。
例如:
camera_node lidar_node imu_node localization_node planner_node controller_node可以把它理解成:
机器人系统 │ ├── 摄像头 Node ├── 激光雷达 Node ├── IMU Node ├── 定位 Node ├── 规划 Node └── 控制 Node这样设计最大的意义,是把复杂机器人系统拆成多个相对独立的功能模块。
例如一个移动机器人:
摄像头 ↓ camera_node ↓ 视觉识别 ↓ perception_node ↓ localization_node ↓ planner_node ↓ controller_node ↓ 电机每个 Node 都可以专注自己的事情。
摄像头 Node 不需要知道路径规划算法到底怎么实现。
路径规划 Node 也不需要知道摄像头底层驱动是怎么工作的。
只要双方遵守约定的数据接口,就可以通过 ROS 2 的通信机制进行协作。
这种设计对于机器人软件开发非常重要。
因为机器人软件通常具有非常明显的模块化特点。
例如:
感知模块
负责:
摄像头
激光雷达
深度相机
IMU
GPS
定位模块
负责:
位姿估计
里程计
SLAM
多传感器融合
规划模块
负责:
全局路径规划
局部路径规划
轨迹规划
避障
控制模块
负责:
速度控制
位置控制
关节控制
电机控制
如果这些东西全部写在一个程序中,系统会越来越难维护。
因此 Node 带来的第一层价值就是:
把机器人复杂的软件系统拆成可以独立开发和协作的功能模块。
但这里需要特别注意:
Node是逻辑层面的模块,并不等于一个独立的CPU。
更不意味着:
一个Node就一定对应一个线程。
也不意味着:
一个Node就一定对应一个进程。
这正是很多初学者容易混淆的地方。
Node是 ROS 2 对机器人软件功能组织的一种抽象。
真正运行它的时候,还需要落到底层的进程和线程。
Node和Process到底是什么关系?
例如我们写一个简单的 ROS 2 程序:
int main(int argc, char * argv[]) { rclcpp::init(argc, argv); auto node = std::make_shared<MyNode>(); rclcpp::spin(node); rclcpp::shutdown(); return 0; }当这个程序启动以后,操作系统首先看到的并不是:
“这里来了一个ROS 2 Node。”
操作系统看到的是一个进程。
例如Linux中可以看到:
ps -ef或者:
top看到对应的进程。
也就是说,可以粗略理解成:
ROS 2层 ↓ Node Linux层 ↓ Process但是一个进程里面又可以包含多个线程。
所以更完整的关系是:
ROS 2 Node ↓ Process ↓ Thread ↓ Callback当然,真实系统会比这个模型复杂,因为一个进程里可以运行多个Node,一个Node也可能涉及多个执行线程。
但这个层次关系对于理解ROS 2实时性非常重要。
二、一个Node启动以后发生了什么?从main函数一直走到Callback
理解Node最好的方式,不是死记定义,而是跟踪它的一次完整运行过程。
假设我们有一个非常简单的机器人控制Node:
controller_node它负责每隔1ms读取机器人状态,然后计算控制指令。
代码可能类似:
auto node = std::make_shared<ControllerNode>(); rclcpp::spin(node);当程序运行起来之后,大致会经历下面几个层次。
第一步:操作系统启动进程
Linux首先创建一个进程。
例如:
controller_node ↓ Linux Process此时操作系统负责:
分配虚拟地址空间
建立页表
创建进程控制结构
加载程序代码
建立运行环境
对于普通应用程序来说,这些东西可能完全透明。
但是实时机器人系统必须开始关注一个问题:
进程什么时候真正获得CPU?
因为:
创建成功 ≠ 立即执行。
进程被创建出来以后,还需要由操作系统调度器决定:
什么时候让它运行。
这就进入了线程调度。
第二步:创建线程
一个进程至少需要一个执行线程。
例如:
controller_node │ └── main thread线程才是真正被操作系统调度的对象。
也就是说:
Process ↓ Thread ↓ CPU如果CPU当前正在运行其他线程,那么你的控制线程就需要等待调度。
假设机器人控制周期要求:
1ms那么真正的问题就来了:
如果控制线程本应该在:
10.000ms执行。
但由于CPU正在处理其他任务,直到:
10.400ms才真正开始运行。
那么这里就已经出现了:
400μs的调度延迟。
如果偶尔出现一次,系统可能仍然正常。
但如果延迟不断变化:
100μs 300μs 80μs 700μs 150μs 2ms那么系统就产生了明显的:
Jitter,也就是执行抖动。
这对于机器人控制非常重要。
因为机器人不是只要求:
“平均每1ms执行一次。”
而是往往更加关注:
“最坏情况下到底会延迟多少?”
第三步:ROS 2创建Node
线程启动以后,程序开始初始化ROS 2。
例如:
auto node = std::make_shared<ControllerNode>();这一步实际上完成了很多事情。
Node需要建立自己的:
Node name
Namespace
Publisher
Subscription
Timer
Service
Parameter
Callback
例如:
controller_node │ ├── /joint_states Subscription ├── /cmd_vel Publisher ├── control_timer └── parameters此时Node在ROS 2系统中已经“注册”了自己的身份和通信接口。
如果它订阅:
/joint_states那么当新的关节状态数据到达时,就会产生相应的处理事件。
例如:
/joint_states ↓ Subscription ↓ Callback问题再次出现:
Callback什么时候执行?
这时候就必须理解Executor。
三、Callback和Executor:Node真正开始“动起来”的地方
很多ROS 2初学者容易认为:
Topic收到消息 → Callback自动执行。
从使用角度看,这样理解并没有问题。
但如果要深入理解ROS 2,就必须进一步问:
到底是谁执行这个Callback?
答案就是:
Executor。
Executor是ROS 2执行机制中的重要组成部分,它负责组织和执行Node中的各种回调。
例如一个Node可能同时存在:
Timer Callback Subscription Callback Service Callback Action Callback可以想象一个机器人控制Node:
Controller Node │ ├── Timer Callback │ └── 每1ms执行 │ ├── Joint State Callback │ └── 接收关节状态 │ ├── Parameter Callback │ └── 参数变化 │ └── Diagnostics Callback └── 状态监控这些Callback并不是全部同时执行。
最终仍然需要执行线程。
因此可以进一步理解:
ROS 2 Callback ↓ Executor ↓ Executor线程 ↓ Linux Scheduler ↓ CPU这条链路非常重要。
因为很多机器人实时问题,并不是发生在:
“算法写错了。”
而是发生在:
“算法虽然正确,但是没有在预期时间执行。”
SingleThreadedExecutor:所有Callback排队执行
最简单的一种方式就是SingleThreadedExecutor。
可以简单理解为:
一个Executor ↓ 一个执行线程 ↓ 多个Callback例如:
Callback A Callback B Callback C Callback D可能变成:
A → B → C → D假设:
A = 激光雷达数据处理 B = 摄像头处理 C = 控制算法 D = 状态监测如果A执行了很长时间,那么C就需要等待。
例如:
A ████████████████ 5ms B ████ 1ms C ██ 0.5ms如果C原本要求:
1ms周期那么A就可能对C造成影响。
这就是单线程执行模型下非常典型的问题:
一个Callback的执行时间可能影响其他Callback的响应时间。
对于普通机器人任务,这种影响可能并不严重。
但对于实时控制任务来说,就需要更加谨慎。
MultiThreadedExecutor:多个线程并行执行
因此ROS 2还提供了MultiThreadedExecutor。
可以简单理解为:
Executor │ ├── Thread 1 → Callback A ├── Thread 2 → Callback B ├── Thread 3 → Callback C └── Thread 4 → Callback D这样多个Callback就有机会并行运行。
听起来问题解决了。
但实际上,新的问题又出现了:
多个线程并行以后,资源竞争怎么办?
例如:
Thread 1 ↓ 读取机器人状态 Thread 2 ↓ 修改机器人状态两个线程如果同时访问共享数据,就可能需要:
Mutex Lock Synchronization于是:
Thread A ↓ 获取Lock ↓ 访问资源 Thread B ↓ 等待Lock这时候,线程之间又产生了等待。
对于普通应用来说,这属于常规并发问题。
但对于实时机器人来说,情况更加复杂。
因为等待时间本身可能是不确定的。
Callback Group:ROS 2如何管理Callback之间的并发?
ROS 2还提供了Callback Group机制。
可以简单理解为:
开发者可以告诉ROS 2,哪些Callback之间允许并行,哪些Callback之间需要互斥。
例如:
Callback Group A │ ├── Sensor Callback └── State Callback Callback Group B │ ├── Control Callback └── Timer Callback不同Callback Group可以采用不同的并发关系。
其中常见的概念包括:
Mutually Exclusive
Reentrant
Mutually Exclusive可以理解为:
同一组中的相关Callback不同时执行。
Reentrant则允许更加灵活的并发执行。
这对于构建复杂机器人软件非常重要。
因为随着Node数量增加,Callback数量也会快速增加。
最终系统可能变成:
几十个Node ↓ 上百个Callback ↓ 多个Executor ↓ 多个线程 ↓ 多个CPU核心到了这个阶段,机器人系统实际上已经不再是一个简单的“ROS程序”。
它已经成为一个真正的:
多任务并发系统。
而多任务系统最终绕不开一个问题:
CPU到底应该把时间给谁?
四、当ROS 2进入机器人实时控制,真正的问题开始出现在Linux内核
现在把前面的链路重新画一遍:
机器人控制算法 ↓ ROS 2 Node ↓ Callback ↓ Executor ↓ Thread ↓ Linux Scheduler ↓ CPU到了这里,ROS 2和操作系统之间的边界就非常清楚了。
ROS 2可以决定:
“这个Callback需要执行。”
但是它并不能独立决定:
“这个Callback必须在CPU上立刻执行。”
真正决定线程什么时候运行的,是操作系统调度器。
这就是为什么:
ROS 2实时性 ≠ 仅仅优化ROS 2代码。
假设:
控制线程 优先级高同时:
视觉线程 普通优先级以及:
日志线程 普通优先级如果底层操作系统没有针对实时任务进行合理配置,那么高优先级任务仍然可能受到:
中断
内核线程
CPU迁移
调度延迟
锁竞争
内存访问
系统服务
等因素影响。
因此机器人实时系统通常会进一步关注:
1. 实时调度策略
例如:
SCHED_FIFO SCHED_RR SCHED_DEADLINE不同调度策略适用于不同的实时任务模型。
例如一个高优先级机器人控制线程,可以采用实时调度策略运行。
2. CPU Affinity
如果一个控制线程可以运行在:
CPU 3那么可以减少它在多个CPU之间迁移的可能。
例如:
CPU 0 → 普通任务 CPU 1 → 感知 CPU 2 → 规划 CPU 3 → 控制这就是CPU亲和性。
但这里必须注意一个很重要的概念:
CPU绑定并不等于CPU核心隔离。
如果只是把控制线程绑定到CPU 3:
Control Thread ↓ CPU 3并不意味着CPU 3上没有其他工作。
CPU 3仍然可能受到:
IRQ Kernel Thread Timer RCU System Task等因素影响。
所以更加严格的实时系统还需要进一步研究:
CPU核心隔离。
3. IRQ Affinity
机器人系统中大量硬件设备会产生中断。
例如:
Ethernet CAN PCIe USB GPIO Sensor Motor Controller设备产生中断以后,CPU需要响应。
如果这些中断全部集中在机器人控制核心上,那么就可能造成:
实时控制任务 ↓ 正在运行 硬件中断 ↓ 突然到来 CPU ↓ 处理中断 控制任务 ↓ 延迟因此实时系统通常需要进一步考虑:
中断应该由哪个CPU核心处理?
这就是IRQ affinity。
于是我们可以看到:
ROS 2应用层的一个简单控制Callback,往下其实连接了一整套复杂的操作系统机制。
五、为什么机器人最终需要关注实时操作系统?ROS 2与望获OS如何连接起来?
到了这里,我们可以回答文章开头的问题:
一个ROS 2 Node从启动到真正执行,到底发生了什么?
可以把整个过程简化成:
① Node启动 ↓ ② Linux创建进程 ↓ ③ 创建线程 ↓ ④ ROS 2初始化Node ↓ ⑤ 创建Publisher / Subscription / Timer ↓ ⑥ DDS接收或发送数据 ↓ ⑦ 产生Callback ↓ ⑧ Executor管理Callback ↓ ⑨ Executor线程等待CPU ↓ ⑩ Linux Scheduler进行调度 ↓ ⑪ CPU执行Callback ↓ ⑫ 控制算法产生结果 ↓ ⑬ 数据发送给执行器真正值得注意的是:
第⑧步以后,ROS 2应用层与操作系统底层开始高度耦合。
如果只是普通机器人Demo,系统可能完全没有必要追求极端的实时性。
但是对于以下场景:
工业机器人
机械臂
运动控制
高速移动机器人
人形机器人
无人系统
智能制造设备
高精度执行机构
情况会发生变化。
因为这些系统往往同时存在:
AI计算 视觉处理 传感器采集 路径规划 运动控制 网络通信 状态监测 日志系统这些任务全部运行在同一个计算平台上。
于是:
CPU资源 ↓ 多个任务竞争而机器人控制任务又有明确的时间约束。
因此真正的问题逐渐变成:
如何让关键控制任务获得更加确定的执行环境?
这也是实时Linux需要解决的问题。
而对于望获OS而言,这里就是望获rtLinux可以切入的技术层。
需要强调的是:
望获rtLinux不是ROS 2的替代品。
两者属于不同的软件层。
可以简单理解成:
┌─────────────────────────────┐ │ AI / Robot Application │ ├─────────────────────────────┤ │ ROS 2 │ │ Node / Topic / Action │ │ Executor / QoS │ ├─────────────────────────────┤ │ DDS / Middleware │ ├─────────────────────────────┤ │ 望获rtLinux │ │ 实时调度 / 核心隔离 / 资源管理 │ ├─────────────────────────────┤ │ 国产CPU / SoC │ └─────────────────────────────┘ROS 2负责解决机器人软件开发和模块协作问题。
望获rtLinux则处于更加底层的操作系统层,关注实时任务运行所需要的操作系统基础能力。
尤其值得关注的是:
核心隔离。
对于机器人系统来说,如果所有任务都混在同一组CPU资源上:
视觉 AI 日志 网络 控制 系统服务那么一个任务的负载变化,就可能影响另一个任务。
而核心隔离的思路,就是尽可能将不同类型的任务进行资源边界划分。
例如:
CPU 0 系统任务 网络 后台服务 CPU 1 AI / 感知 CPU 2 ROS 2规划 CPU 3 实时控制这里的意义并不是简单地“把程序绑到某个CPU”。
真正的核心隔离需要综合考虑:
CPU Affinity IRQ Affinity Kernel Thread RCU Timer Scheduler Housekeeping等多个层面的资源和任务。
因此,从ROS 2 Node继续向下研究,会发现一个非常有意思的技术关系:
Node ↓ Callback ↓ Executor ↓ Thread ↓ Scheduler ↓ CPUROS 2解决的是上层机器人软件如何组织和执行。
操作系统解决的是底层线程如何获得资源、如何调度以及如何减少不同任务之间的干扰。
这两层共同决定了一个机器人软件系统最终的运行特性。
对于机器人企业而言,这意味着未来的软件架构可能不再只是:
“ROS 2能不能运行起来?”
而会进一步变成:
“ROS 2能不能在一个具有确定性资源管理能力的操作系统上稳定运行?”
尤其是当机器人从实验室Demo进入真实工业环境之后,这个问题会更加明显。
因为实验室环境通常允许开发者使用:
普通PC 普通Linux 普通网络 普通调度只要功能能够运行即可。
但是进入产品阶段以后,系统需要进一步考虑:
稳定性 实时性 可靠性 资源隔离 故障恢复 长期运行 国产芯片适配于是,ROS 2与实时操作系统之间就建立起了一个更加明确的关系:
ROS 2 负责机器人软件生态 + 望获rtLinux 负责底层实时运行环境这并不是说所有ROS 2机器人都必须使用望获rtLinux。
不同机器人、不同控制周期、不同硬件平台和不同安全要求,会对应不同的系统架构。
但对于需要更加严格实时性、确定性和资源隔离能力的机器人系统而言,从ROS 2向实时操作系统进一步延伸,是一个非常值得研究的技术方向。
写在最后:一个Node背后,其实是一整套操作系统机制
很多时候,我们看到ROS 2程序可能只有几行:
auto node = std::make_shared<MyNode>(); rclcpp::spin(node);看起来非常简单。
但真正运行的时候,背后已经经历了:
Node ↓ Process ↓ Thread ↓ Callback ↓ Executor ↓ Scheduler ↓ CPU这也是为什么学习ROS 2不能只停留在API层面。
如果只知道:
“怎么创建Node。”
只能算是入门。
如果进一步理解:
“Node中的Callback是怎么执行的?”
就开始进入ROS 2运行机制。
如果继续理解:
“Executor如何管理Callback?”
就开始进入ROS 2执行模型。
如果再继续研究:
“Executor线程什么时候获得CPU?”
就已经进入Linux调度。
而如果继续追问:
“如何让机器人控制任务获得更加确定的CPU执行环境?”
那么就自然进入:
实时Linux、CPU核心隔离、中断隔离、实时调度以及实时操作系统。
这也是整个“ROS 2 × 实时操作系统”系列接下来要继续研究的核心。
下一篇,我们将继续解决一个非常实际的问题:
ROS 2中的Topic、Service和Action到底有什么区别?为什么机器人中的激光雷达、控制指令和导航任务不能使用同一种通信方式?
从三种通信模型出发,我们会进一步进入DDS、QoS、Reliable、Best Effort、Deadline以及机器人实时数据链路,并继续分析这些通信机制和底层实时操作系统之间的关系。