面试的时候被问:"ROS2里的Node是什么?它和进程的关系是什么?"

我说"Node就是一个进程"。面试官说:"那一个进程里能不能跑多个Node?"我愣住了。

这是ROS2的基础概念,但很多人用了好久都没搞清楚。今天把Node这个东西从头到尾讲明白。

Node是什么

在ROS2中,Node(节点)是系统中最小的执行单元。每个Node负责一个具体的功能:读取传感器数据、发布控制指令、处理图像、执行路径规划等等。

一个机器人系统通常由多个Node组成。比如一台移动机器人可能有这些Node:激光雷达驱动Node(读取雷达数据并发布点云)、导航Node(接收点云和地图,计算路径)、控制Node(接收路径,输出电机指令)、状态监控Node(监控电池电量和各模块状态)。

每个Node都是独立的,通过topic、service、action这些通信机制互相交互。这就是ROS2的"模块化"设计思想——把复杂系统拆分成多个小模块,每个模块独立开发、独立测试、独立部署。

Node和进程的关系

这是面试常考的问题。

在ROS2中,一个Node通常运行在一个独立的进程里。你用ros2 run启动一个节点,就是一个进程。但一个进程里也可以跑多个Node,这叫"组件化"(Component),后面会专门讲。

反过来,一个Node也只能属于一个进程。你不能让一个Node跨多个进程运行。

用ros2 node list可以查看当前运行的所有Node:

$ ros2 node list
/lidar_driver
/navigator
/controller
/monitor

用ros2 node info /lidar_driver可以看到这个Node的详细信息:它发布了哪些topic、订阅了哪些topic、提供了哪些service。

用Python创建一个Node

最简单的Node长这样:

import rclpy
from rclpy.node import Node

class SensorNode(Node):
    def __init__(self):
        super().__init__('sensor_node')
        self.get_logger().info('传感器节点启动了')
        
        # 创建一个定时器,每0.1秒触发一次
        self.timer = self.create_timer(0.1, self.read_sensor)
    
    def read_sensor(self):
        # 这里读取传感器数据
        sensor_value = 42.0  # 模拟数据
        self.get_logger().info(f'传感器读数: {sensor_value}')

def main():
    rclpy.init()
    node = SensorNode()
    try:
        rclpy.spin(node)    # 阻塞,持续处理回调
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        rclpy.shutdown()

if __name__ == '__main__':
    main()

几个关键点:

rclpy.init()初始化ROS2通信。这必须在创建任何Node之前调用。

super().__init__('sensor_node')给Node起名字。这个名字在ros2 node list里显示的就是它。同一个namespace下不能有同名的Node。

rclpy.spin(node)进入事件循环,持续处理回调(定时器、订阅消息等)。没有这行,Node创建完就退出了。

node.destroy_node()和rclpy.shutdown()是清理工作。虽然Python有垃圾回收,但显式清理是好习惯,特别是在C++中这是必须的。

Node的参数和命名空间

Node可以设置命名空间(namespace),用来隔离不同机器人的节点:

node = Node('sensor_node', namespace='robot1')
# 完整名字变成 /robot1/sensor_node

在launch文件中可以方便地给所有节点设置namespace,这样同一套代码可以跑多台机器人。

Node还可以声明参数(parameters),运行时可以动态修改:

self.declare_parameter('sensor_rate', 10.0)
rate = self.get_parameter('sensor_rate').value

运行时可以用命令行修改参数:

ros2 param set /sensor_node sensor_rate 20.0

这让不用重启节点就能调整行为成为可能,在调试的时候特别方便。

一个Node里通常有什么

一个典型的机器人Node包含这些要素:

订阅者(Subscriber)——接收其他Node发来的数据。比如导航Node订阅激光雷达的点云数据。

发布者(Publisher)——向其他Node发送数据。比如导航Node发布计算出的速度指令。

定时器(Timer)——周期性执行某些操作。比如控制Node每10毫秒计算一次PID输出。

Service服务端——提供某种服务给其他Node调用。比如一个标定Node提供"获取当前标定参数"的服务。

这些要素在Node的构造函数中初始化,在回调函数中处理逻辑。rclpy.spin()负责调度这些回调,你不需要自己管理线程。

Node的生命周期

ROS2引入了生命周期节点(Lifecycle Node)的概念,把Node的运行状态分成了几个阶段:Unconfigured、Inactive、Active、Finalized。

为什么要搞这么复杂?因为在机器人系统中,你需要确保所有节点按照正确的顺序启动。比如导航系统,你得先让地图加载完成,再启动路径规划。生命周期节点通过状态转换来协调这个启动顺序。

在Active状态之前,节点虽然存在但不会处理数据。这样你可以先配置好所有参数,确认一切就绪后再激活。面试的时候提到这个概念会加分,说明你了解ROS2的工程化设计。

面试中怎么聊

面试官问Node,你可以说:"Node是ROS2系统中的最小执行单元,每个Node负责一个独立的功能模块。Node之间通过topic、service、action通信,形成松耦合的分布式系统。一个Node通常运行在独立进程中,但也可以用组件的方式把多个Node放在一个进程里减少通信开销。Node支持参数声明和动态修改,支持命名空间隔离。"

Node设计的最佳实践

设计ROS2节点时有几个重要原则:一是单一职责,一个节点只做一件事;二是合理划分节点粒度,太粗会影响模块化,太细会增加通信开销;三是注意回调函数中的耗时操作,长时间阻塞会影响其他回调的执行。面试时如果被问到怎么划分节点,可以结合具体项目场景来讲你的设计思路和权衡过程。

节点间通信的设计原则

在实际项目中,节点间的通信设计很关键。一个常见的反模式是把所有逻辑都塞在一个节点里,虽然减少了通信开销但丧失了模块化。好的做法是:每个节点职责单一,通过Topic/Service/Action通信,这样单个节点可以独立测试和替换。面试时如果能画出你项目的节点通信图并解释设计理由,会非常有说服力。另外,命名空间的使用也很重要,在多机器人场景中用命名空间隔离不同机器人的节点和话题是标准做法。

给你的建议

写Node的时候遵循单一职责原则。一个Node只做一件事,做好它。不要写一个"超级Node"什么都干,那样代码又难维护又难调试。

学会用ros2 node info和ros2 topic info排查问题。节点启动后没有预期的通信,先看看节点是否正常运行、topic是否创建成功、QoS是否匹配。用ros2 node list可以快速确认节点是否成功注册到网络中。

还有,Node的名字要起得有含义。node1、test_node这种名字在调试的时候会让你崩溃。用lidar_driver、path_planner、motor_controller这种一看就知道干什么的名字。在大型项目中,给Node加上命名空间前缀也是好习惯,比如/perception/lidar_driver。


上一篇:第111篇 ROS2工作空间与colcon编译——开发流程的第一步

下一篇预告:第113篇 ROS2 Topic通信——发布订阅的底层机制

Logo

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

更多推荐