上一篇文章我们从整体架构出发,介绍了 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以及机器人实时数据链路,并继续分析这些通信机制和底层实时操作系统之间的关系。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐