ROS 2 Node节点详解:一个Node从启动到执行,到底发生了什么?
上一篇文章我们从整体架构出发,介绍了 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
↓
CPU
ROS 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以及机器人实时数据链路,并继续分析这些通信机制和底层实时操作系统之间的关系。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)