【ROS2】基础概念-进阶篇
1 Domain ID 的作用
DDS 使用 Domain ID 来隔离不同的通信域(Domain)。 每个 Domain ID 会映射到一组 UDP 端口,用于节点之间的 发现(Discovery) 和 数据通信。 这样,不同的 DDS Domain 可以在同一网络中共存而互不干扰。
UDP 端口计算
UDP 端口是 16 位无符号整数,取值范围 0–65535。DDS 使用一个公式把 Domain ID 映射到具体的端口号。
通用公式示意:
UDP_Port=Base_Port+(Domain_ID×Offset)
其中 Base_Port 和 Offset 是 DDS 规范中定义的常量。
因为 UDP 端口不能超过 65535,所以 Domain ID 的最大值受到限制。
按照你提到的计算,最大可分配的 Domain ID 为 232。最小 Domain ID 为 0。超出这个范围的 Domain ID 可能会导致端口号溢出或冲突。
选择 Domain ID 时避开会映射到 临时端口 范围的 ID,(因为操作系统的其他服务可能也会分配这些端口,可能导致端口冲突,ROS 2 节点通信异常。)
系统 | 默认临时端口范围 | 说明 |
---|---|---|
Windows | 49152–65535 | 可通过注册表或 netsh 配置修改 |
Linux | 32768–60999 | 可通过 /proc/sys/net/ipv4/ip_local_port_range 查看或修改 |
macOS | 49152–65535 | - |
Windows/Linux 下Domain ID 的范围:
操作系统 | 临时端口范围 (默认) | 安全 Domain ID 范围 | 备注 |
---|---|---|---|
Linux | 32768 – 60999 | 0 – 101、215 – 232 | 临时端口范围可通过 /proc/sys/net/ipv4/ip_local_port_range 修改,修改后安全范围需重新计算 |
Windows | 49152 – 65535 | 0 – 166 | 临时端口范围可通过 netsh 修改,修改后安全范围需重新计算 |
1.1.1 ROS 2 / DDS 的 Participant 限制
每个 ROS 2 节点在底层 DDS 中都会创建一个 “participant”, Participant 是 DDS 的核心实体,用于管理该节点的 主题订阅/发布、服务、动作等通信。
Participant 对端口的占用
每个 Participant 至少会占用 2 个 UDP 端口(实际ROS2 会预留4个):
- 一个用于 发现(Discovery)
- 一个用于 数据传输(Data Communication)
例如,如果一个计算机上启动了 10 个 ROS 2 节点(每个节点 1 个 Participant),就会占用 10 × 2 = 20 个 UDP 端口。
以 Linux 为例,安全的 Domain ID 范围对应的端口数是有限的,如果同时运行太多 ROS 2 节点,超过约 120 个 ROS 2 进程,可能导致:
- UDP 端口分配溢出到其他 Domain ID
- UDP 端口落入操作系统的临时端口范围 → 端口冲突
这会导致节点通信失败或不稳定。
2 不同厂商实现的ROS2 DDS
2.1 什么是 DDS
DDS (Data Distribution Service) 是 OMG(对象管理组织)制定的标准中间件。
它定义了一套 发布-订阅通信模型,以及在网络上传输数据的协议(RTPS)。
DDS 由多家厂商实现(RTI Connext DDS、eProsima Fast DDS、Eclipse Cyclone DDS、GurumDDS 等)。
它本质上是 一个通用的分布式通信中间件标准,不局限于 ROS。
2.2 什么是 RMW?
RMW (ROS Middleware Interface) 是 ROS 2 定义的一层 抽象接口。
它的作用是把 ROS 2 的 API 和 底层中间件(如 DDS) 解耦。
ROS 2 的核心代码 不会直接调用 DDS API,而是通过 RMW 层来调用。
例如, 如果你使用 Fast DDS,实际就是通过 rmw_fastrtps_cpp 这个包实现的。
在 ROS 2 里,通信层是通过 DDS (Data Distribution Service) 实现的,而 DDS 本身有多个不同的厂商(vendor)提供实现。ROS 2 不直接写死某个 DDS,而是通过 RMW (ROS Middleware Interface) 作为抽象层对接不同的实现。
#所以RMW 就是DDS 的接口层。#
下面是一些常见的 ROS 2 middleware vendors:
Middleware Vendor | 对应 RMW 实现 | 特点 |
---|---|---|
eProsima Fast DDS(原 Fast RTPS) | rmw_fastrtps_cpp / rmw_fastrtps_dynamic_cpp | ROS 2 默认支持的 DDS 实现,开源,跨平台,性能不错,支持静态发现和动态发现。 |
RTI Connext DDS | rmw_connextdds | 商业版 DDS,RTI 公司提供,功能完整,支持 QoS 丰富,实时性和可靠性较强,广泛应用于工业领域。 |
ADLINK OpenSplice DDS | rmw_opensplice_cpp | 开源/商业版本,曾经是 ROS 2 的早期默认实现,现在用得少。 |
Eclipse Cyclone DDS | rmw_cyclonedds_cpp | Eclipse 基金会维护的开源 DDS,轻量级、嵌入式友好,资源占用小,近年来在 ROS 2 社区越来越流行。 |
GurumDDS | rmw_gurumdds_cpp | 韩国 GurumNetworks 提供的 DDS 实现,针对嵌入式和资源受限设备优化。 |
为什么有这么多厂商?
DDS 是 OMG(对象管理组织)定义的标准协议,各家厂商可以提供自己的实现。
ROS 2 通过 RMW 接口,把上层 ROS API(rclcpp, rclpy 等)和下层 DDS 解耦。这样用户可以在不同 DDS 实现之间切换。
3 ROS 2 的日志子系统
ROS 2 的日志子系统旨在将日志消息发送到多种目标,包括:
- 控制台(如果有连接的终端)
- 磁盘上的日志文件(如果有本地存储可用)
- ROS 2 网络上的 /rosout 主题
默认情况下,ROS 2 节点中的日志消息会同时输出到 控制台(stderr)、磁盘上的日志文件,以及 ROS 2 网络上的 /rosout 主题。所有这些日志输出目标都可以在 每个节点的粒度上 单独启用或禁用。
日志消息具有与之关联的严重性级别:DEBUG、INFO、WARN、ERROR 或 FATAL,按严重程度递增排列。
一个 logger 只会处理其配置的级别 或更高级别 的日志消息。
每个节点都有一个与之关联的 logger,它会自动包含节点的 名称和命名空间。如果节点名称在外部被重新映射为与源码中定义的不一样,logger 名称也会随之反映这种变化。除了节点 logger 之外,还可以创建 使用特定名称的非节点 logger。
3.1 配置
由于 rclcpp
和 rclpy
使用相同的底层日志基础设施,因此它们的配置选项是相同的。
环境变量
以下环境变量可以控制 ROS 2 日志器的一些行为。需要注意的是,这些环境变量是 进程范围设置,即它们会影响该进程中的所有节点。
-
ROS_LOG_DIR
控制写入磁盘日志的目录(如果启用)。- 非空时,使用该变量指定的目录。
- 为空时,使用
ROS_HOME
的内容构建$ROS_HOME/.log
目录。 - 无论哪种情况,
~
会扩展为用户的 HOME 目录。
-
ROS_HOME
控制 ROS 用于各种文件的主目录,包括日志和配置文件。- 在日志上下文中,该变量用于构建日志文件的目录路径。
- 非空时,使用该路径。
~
会扩展为用户的 HOME 目录。
-
RCUTILS_LOGGING_USE_STDOUT
控制日志输出使用的流。- 未设置或为
0
时,使用stderr
。 - 为
1
时,使用stdout
。
- 未设置或为
-
RCUTILS_LOGGING_BUFFERED_STREAM
控制日志流(由RCUTILS_LOGGING_USE_STDOUT
配置)是否行缓冲或无缓冲。- 未设置时,使用流的默认值(
stdout
默认行缓冲,stderr
默认无缓冲)。 - 为
0
时,强制无缓冲。 - 为
1
时,强制行缓冲。
- 未设置时,使用流的默认值(
-
RCUTILS_COLORIZED_OUTPUT
控制输出是否使用颜色。- 未设置时,根据平台和控制台是否为 TTY 自动决定。
- 为
0
时,禁用颜色。 - 为
1
时,强制启用颜色。
-
RCUTILS_CONSOLE_OUTPUT_FORMAT
控制每条日志消息的输出字段。可用字段包括:{severity}
- 严重性级别{name}
- logger 名称(可能为空){message}
- 日志消息(可能为空){function_name}
- 调用的函数名(可能为空){file_name}
- 调用的文件名(可能为空){time}
- 自 epoch 以来的秒数{time_as_nanoseconds}
- 自 epoch 以来的纳秒数{line_number}
- 调用的行号(可能为空)
如果未设置格式,则使用默认格式:
[{severity}] [{time}] [{name}]: {message}
节点创建
初始化 ROS 2 节点时,可以通过 节点选项 控制日志行为。由于这些是 每节点设置,因此不同节点即使在同一进程中,也可以使用不同配置。
-
log_levels
为该节点的某个组件设置日志级别。例如:ros2 run demo_nodes_cpp talker --ros-args --log-level talker:=DEBUG
-
external_log_config_file
指定外部文件来配置底层日志器。如果为NULL
,使用默认配置。
文件格式依赖于底层日志器(默认使用spdlog
的后端尚未实现)。
示例:ros2 run demo_nodes_cpp talker --ros-args --log-config-file log-config.txt
-
log_stdout_disabled
是否禁用将日志写入控制台。例如:ros2 run demo_nodes_cpp talker --ros-args --disable-stdout-logs
-
log_rosout_disabled
是否禁用将日志写入/rosout
。
可以显著节省网络带宽,但外部观察者将无法监控日志。
示例:ros2 run demo_nodes_cpp talker --ros-args --disable-rosout-logs
-
log_ext_lib_disabled
是否完全禁用外部日志库。
在某些情况下速度更快,但日志将不会写入磁盘。
示例:ros2 run demo_nodes_cpp talker --ros-args --disable-external-lib-logs
3.2 日志系统的设计
4. Qos 设置
ROS 2 提供了丰富的 QoS 策略,用于调节节点之间的通信。通过合理配置 QoS 策略,ROS 2 可以实现像 TCP 一样可靠的通信,也可以像 UDP 一样的尽力而为(best-effort),以及介于两者之间的多种状态。与主要支持 TCP 的 ROS 1 不同,ROS 2 利用底层 DDS 传输的灵活性,在存在丢包的无线网络环境中可以选择“尽力而为”的策略,也可以在实时计算系统中选择合适的 QoS 配置以满足时限要求。
一组 QoS 策略(policies) 组合起来形成一个 QoS 配置文件(profile)。由于为特定场景选择正确的 QoS 策略比较复杂,ROS 2 提供了一些预定义的 QoS 配置文件用于常见用途(例如传感器数据)。同时,开发者也可以灵活地控制 QoS 配置文件中的具体策略。
QoS 配置文件可以针对发布者(publisher)、订阅者(subscription)、服务端(service server)和客户端(client)进行指定。每个实体实例可以独立应用 QoS 配置文件,但如果不同实例使用了不兼容的 QoS 配置文件,可能会导致消息无法传递。
4.1 QoS 策略 (QoS Policies)
当前基础 QoS 配置文件包含以下策略设置:
-
History(历史记录)
- Keep last:仅存储最近的 N 条样本,可通过队列深度(queue depth)选项配置。
- Keep all:存储所有样本,但受底层中间件的资源限制约束。
-
Depth(队列深度)
- 队列大小,仅在 history 策略设置为 keep last 时有效。
-
Reliability(可靠性)
- Best effort(尽力而为):尝试传递样本,但在网络不稳定时可能丢失。
- Reliable(可靠):保证样本传递成功,可能会进行多次重试。
-
Durability(持久性)
- Transient local(本地暂存):发布者负责保存样本,以便“后加入”的订阅者获取。
- Volatile(易失性):不尝试持久化样本。
-
Deadline(截止时间)
- Duration(持续时间):两个连续消息发布到主题的最大预期时间间隔。
-
Lifespan(生命周期)
- Duration(持续时间):从消息发布到接收的最大时间,如果超时,消息被视为过期并被丢弃。
-
Liveliness(存活性)
- Automatic(自动):当节点的任一发布者发布消息时,系统认为所有发布者在“租约时间”内仍然存活。
- Manual by topic(手动按主题):发布者需手动调用 API 来声明自己仍然存活,系统才认为其在“租约时间”内存活。
-
Lease Duration(租约持续时间)
- Duration(持续时间):发布者必须在此时间内声明自己存活,否则系统认为其失去存活性(可能意味着故障)。
- 对于非持续时间(duration)的策略,还可以选择 system default(系统默认),使用底层中间件的默认值。
- 对于持续时间策略,也存在 default(默认) 选项,表示时间未指定,底层中间件通常会将其解释为无限长。
4.2 QoS 配置文件(QoS Profiles)
QoS 配置文件让开发者可以专注于应用本身,而不必关心每一个可能的 QoS 设置。一个 QoS 配置文件定义了一组策略,这些策略在特定使用场景下通常可以很好地协同工作。
当前定义的 QoS 配置文件包括:
-
默认 QoS(Default QoS)
-
针对发布者(publisher)和订阅者(subscription)的默认设置。
-
为了方便从 ROS 1 迁移到 ROS 2,希望网络行为尽量相似。默认情况下,ROS 2 的发布者和订阅者:
- history 为 “keep last”,队列大小为 10
- reliability 为 “reliable”
- durability 为 “volatile”
- liveliness 为 “system default”
- deadline、lifespan、lease duration 都设置为 “default”
-
-
服务(Services)
- 服务的 QoS 与发布/订阅类似,都使用可靠性(reliable)。
- 特别重要的是,服务使用 volatile 持久性,否则重启的服务服务器可能接收到过期请求。
- 客户端被保护,避免收到多个响应;但服务器不受过期请求的副作用保护。
-
传感器数据(Sensor data)
- 对于传感器数据,通常更关注及时接收数据,而不是保证全部数据都到达。
- 开发者希望尽快获取最新样本,即使可能丢失一些。
- 因此,传感器数据配置文件使用 best effort 可靠性和较小队列深度。
-
参数(Parameters)
- ROS 2 的参数基于服务,因此有类似的 QoS 配置。
- 不同的是,参数使用更大的队列深度,以防在参数客户端无法访问参数服务服务器时请求丢失。
-
系统默认(System default)
- 使用 RMW 实现的默认值。
- 不同 RMW 实现的默认值可能不同。
4.3 QoS 兼容性(QoS compatibilities)
注意:本节内容主要针对发布者(publishers)和订阅者(subscriptions),但同样适用于服务服务器(service servers)和客户端(clients)。
QoS 配置可以独立地为每个发布者和订阅者设置。
只有当发布者和订阅者的 QoS 配置兼容时,才会建立连接。
QoS 兼容性的判断模型:
基于 “请求 vs 提供(Request vs Offered)” 模型。
- 订阅者请求的 QoS 配置表示它可接受的 最低质量(minimum quality)。
- 发布者提供的 QoS 配置表示它能够提供的 最高质量(maximum quality)。
- 当订阅者请求的每一项 QoS 策略不高于发布者提供的策略时,才会建立连接。
如果发布者提供的 QoS 低于订阅者的要求,消息可能无法满足订阅者的最低需求,例如:
- 发布者是 Best Effort(尽力发送)而订阅者需要 Reliable(可靠) → 可能丢消息。
- 发布者保存的历史消息数量太少,订阅者要求更多 → 订阅者无法获取所有想要的数据。
因此,ROS 2 只在发布者能满足订阅者需求时建立连接,保证数据的可靠性和完整性。
其他说明:
- 一个发布者可以同时与多个订阅者连接,即使它们请求的 QoS 配置不同。
- 发布者和订阅者之间的兼容性不会受到其他发布者或订阅者的存在影响。
4.3 QoS 事件
某些 QoS 策略可能会触发相关事件。开发者可以为每个发布者和订阅者提供回调函数,当这些 QoS 事件发生时触发,并按照开发者定义的方式处理,就像处理主题接收到的消息一样。
与发布者相关的 QoS 事件
- 提供的截止时间错过(Offered deadline missed)
发布者在规定的截止时间内未发布消息,这个截止时间由 Deadline QoS 策略设置。 - 生命信号丢失(Liveliness lost)
发布者未在租约时间(lease duration)内指示其仍然活跃。 - 提供的 QoS 不兼容(Offered incompatible QoS)
发布者在同一主题上遇到一个订阅者请求的 QoS 配置无法被满足,导致该发布者与该订阅者之间无法建立连接。
与订阅者相关的 QoS 事件
-
请求的截止时间错过(Requested deadline missed)
订阅者在规定的截止时间内未收到消息,这个截止时间由 Deadline QoS 策略设置。 -
生命信号变化(Liveliness changed)
订阅者注意到一个或多个在所订阅主题上的发布者未在租约时间内指示其仍然活跃。 -
请求的 QoS 不兼容(Requested incompatible QoS)
订阅者在同一主题上遇到一个发布者提供的 QoS 配置无法满足订阅者请求的 QoS 配置,导致该订阅者与该发布者之间无法建立连接。
5. 执行器(Executors)
ROS 2 中的执行管理由 Executors(执行器)负责。执行器使用操作系统的一个或多个线程来调用订阅(subscriptions)、定时器(timers)、服务服务器(service servers)、动作服务器(action servers)等回调函数,以处理接收到的消息和事件。
相比 ROS 1 中的 spin 机制,ROS 2 提供的显式 Executor 类(在 C++ 的 rclcpp 中位于 executor.hpp,在 Python 的 rclpy 中位于 executors.py,在 C 的 rclc 中位于 executor.h)可以提供更多的执行管理控制,尽管基本的 API 使用方式与 ROS 1 类似。
5.1 基本用法(Basic use)
在最简单的情况下,主线程用于处理节点的接收消息和事件,通过如下方式调用 rclcpp::spin(…):
int main(int argc, char* argv[])
{// Some initialization.rclcpp::init(argc, argv);...// Instantiate a node.rclcpp::Node::SharedPtr node = ...// Run the executor.rclcpp::spin(node);// Shutdown and exit....return 0;
}
对 spin(node) 的调用基本上相当于实例化并调用单线程执行器(Single-Threaded Executor),它是最简单的一种 Executor:
rclcpp::executors::SingleThreadedExecutor executor;
executor.add_node(node);
executor.spin();
通过调用 Executor 实例的 spin() 方法,当前线程开始查询 rcl 和底层中间件层以获取传入的消息和其他事件,并调用相应的回调函数,直到节点关闭。为了不影响中间件的 QoS 设置,传入的消息不会存储在客户端库层的队列中,而是保留在中间件中,直到被回调函数取出处理。(这是与 ROS 1 的一个关键区别。)
一个 wait set 被用来通知 Executor 中间件层上可用的消息,每个队列对应一个二进制标志位。wait set 同样用于检测定时器是否到期。
上图描述Executor 回调执行流程:
- wait(等待)
- Executor 会等待来自中间件的事件(如消息到达、定时器触发)。
- /goal、/cmd、/odom 分别表示订阅的主题。
- 每个队列有一个二进制标志表示是否有新消息。
- take(取消息)
- 当 wait 集合检测到消息可用时,Executor 会从中间件队列中取出消息(take)。
- 取出的消息暂存在 Executor 层,等待回调执行。
- execute(执行回调)
- Executor 调用用户代码(User code)提供的回调函数,例如 onGoal、nextCmd、processOdom。
- 回调处理逻辑可能会更新机器人状态或触发新的操作。
单线程执行器就是用来“安排节点工作的工具”。当你用容器来运行组件时,如果你没有自己写一个 main 函数去控制节点,系统就会自动用这个单线程执行器来帮你管理节点的运行,让它们按顺序工作。
5.2 执行器类型(Types of Executors)
目前,rclcpp 提供三种执行器类型,它们都继承自同一个父类:
Executor├── SingleThreadedExecutor├── MultiThreadedExecutor└── StaticSingleThreadedExecutor
- 单线程执行器(Single-Threaded Executor):在单一线程中顺序调度和执行节点的所有回调,包括订阅消息回调、定时器回调、服务服务器回调和动作服务器回调等。
- 多线程执行器(Multi-Threaded Executor):可以创建可配置数量的线程,以便同时处理多个消息或事件,实现并行处理。
- 静态单线程执行器(Static Single-Threaded Executor):优化了扫描节点结构(例如订阅、定时器、服务服务器、动作服务器等)的运行开销。它在节点添加时只扫描一次,而其他两种执行器会定期扫描这些变化。因此,静态单线程执行器只适用于在初始化时就创建好所有订阅、定时器等的节点。
三种执行器都可以用于管理多个节点,只需对每个节点调用 add_node(…) 即可:
rclcpp::Node::SharedPtr node1 = ...
rclcpp::Node::SharedPtr node2 = ...
rclcpp::Node::SharedPtr node3 = ...rclcpp::executors::StaticSingleThreadedExecutor executor;
executor.add_node(node1);
executor.add_node(node2);
executor.add_node(node3);
executor.spin();
在上述示例中,静态单线程执行器(Static Single-Threaded Executor)的一个线程被用来同时服务三个节点。对于多线程执行器(Multi-Threaded Executor)来说,实际的并行度取决于回调组(callback groups)的配置。
5.3 回调组(Callback Groups)
ROS 2 允许将节点的回调组织到不同的组中。在 rclcpp 中,可以通过 Node 类的 create_callback_group 函数创建这样的回调组。在 rclpy 中,则通过调用特定回调组类型的构造函数来实现。
回调组在节点执行期间必须保持有效(例如作为类成员保存),否则执行器将无法触发回调。随后,在创建订阅、定时器等时,可以指定使用该回调组——例如通过订阅选项(subscription options)进行指定。
my_callback_group = create_callback_group(rclcpp::CallbackGroupType::MutuallyExclusive);rclcpp::SubscriptionOptions options;
options.callback_group = my_callback_group;my_subscription = create_subscription<Int32>("/topic", rclcpp::SensorDataQoS(),callback, options);
所有在创建时未指定回调组的订阅、定时器等,都会被分配到默认回调组。默认回调组可以通过 rclcpp 中的 NodeBaseInterface::get_default_callback_group() 查询,在 rclpy 中则可通过 Node.default_callback_group 访问。
回调组有两种类型,类型必须在创建时指定:
- 互斥(Mutually exclusive):该组的回调不能并行执行。
- 可重入(Reentrant):该组的回调可以并行执行。
不同回调组的回调始终可以并行执行。多线程执行器(Multi-Threaded Executor)会将其线程作为线程池,根据这些条件尽可能并行处理回调。有关如何高效使用回调组的提示,请参见《使用回调组(Using Callback Groups)》。
在 rclcpp 中,执行器(Executor)基类还提供了 add_callback_group(…) 函数,用于将回调组分配到不同的执行器。通过使用操作系统调度器配置底层线程,可以对特定回调设置优先级。例如,可以将控制循环的订阅和定时器优先于节点的其他订阅和标准服务执行。examples_rclcpp_cbg_executor 包提供了该机制的演示示例。
5.4 调度语义(Scheduling Semantics)
如果回调的处理时间比消息和事件发生的周期短,执行器基本上会按照 FIFO(先进先出)顺序处理它们。然而,如果某些回调的处理时间较长,消息和事件会在底层堆栈中排队。等待集(wait set)机制仅向执行器报告这些队列的很少信息,具体来说,它只会报告某个主题是否有消息。执行器利用这些信息以**轮询(round-robin)**的方式处理消息(包括服务和动作),而不是严格按照 FIFO 顺序。下图展示了这种调度语义的流程。
这个图说明了执行器如何从等待集(wait set)中获取可执行的回调并处理消息、定时器和服务请求。具体含义如下:
-
检查定时器是否准备好(timer ready?)
- 如果定时器触发了,就取出消息,执行回调,然后在等待集中清除(Take message → Execute callback → Clear in wait-set)。
- 如果没有准备好,继续下一步检查。
-
检查主题是否有消息(topic in wait-set?)
- 如果有消息,就处理回调(同上)。
- 如果没有,就继续下一步。
-
检查服务请求是否有待处理(service in wait-set?)
- 如果有服务请求,就处理回调。
- 如果没有,就继续下一步。
-
检查服务回复是否有待处理(service reply in wait-set?)
- 如果有,就处理回调。
- 如果没有,就继续下一步。
-
收集所有实体(collect_entities())
- 这个步骤会更新等待集中的信息,例如检查新的订阅、定时器、服务等是否有事件。
-
在中间件中等待(wait in middleware)
- 如果没有任何回调可执行,执行器会阻塞等待新的消息或事件到来。
5.5 Outlook
虽然 rclcpp 的三种 Executor 对大多数应用程序来说表现良好,但它们在实时应用中存在一些问题。实时应用需要明确的执行时间、确定性以及对执行顺序的自定义控制。以下是这些问题的总结:
复杂且混合的调度语义:理想情况下,你希望有明确定义的调度语义,以便进行正式的时间分析。
回调可能出现优先级反转:高优先级的回调可能会被低优先级的回调阻塞。
无法显式控制回调的执行顺序。
无法内置控制特定主题的触发。
此外,Executor 在 CPU 和内存使用方面的开销也相当可观。静态单线程 Executor(Static Single-Threaded Executor)能大幅减少这种开销,但对于某些应用可能仍不够。
这些问题已通过以下方法得到部分解决:
rclcpp WaitSet:rclcpp 的 WaitSet 类允许直接等待订阅、定时器、服务服务器、动作服务器等,而无需使用 Executor。它可以用于实现确定性的、用户自定义的处理序列,甚至可以同时处理来自不同订阅的多条消息。examples_rclcpp_wait_set 包提供了多个示例,展示了如何使用这种用户级的 wait set 机制。
rclc Executor:来自 C 客户端库 rclc 的 Executor,专为 micro-ROS 开发,允许用户对回调的执行顺序进行精细控制,并支持自定义触发条件来激活回调。此外,它实现了逻辑执行时间(Logical Execution Time, LET)语义的理念。
6. 主题统计
ROS 2 提供了对任何订阅接收到的消息的统计信息的集成测量。允许用户收集订阅统计信息,使他们能够对系统性能进行特征化分析,或帮助诊断当前存在的问题。
提供的测量指标包括 接收到的消息的时延(age) 和 接收到的消息的周期(period)。对于每个测量指标,提供的统计信息包括 平均值、最大值、最小值、标准差 和 样本数量。这些统计信息是在一个移动窗口中计算的。
统计信息的计算方式
每组统计信息都是通过 libstatistics_collector 包中实现的工具,以 常数时间和常数内存 进行计算的。当订阅收到一条新消息时,这条消息就成为当前测量窗口中的一个新样本。计算的平均值只是一个移动平均。最大值、最小值和样本计数会在收到每个新样本时更新,而标准差则使用 Welford 在线算法 进行计算。
计算的统计类型
-
接收到的消息周期(Received message period)
- 单位:毫秒
- 使用系统时钟测量接收到消息之间的时间间隔
-
接收到的消息时延(Received message age)
- 单位:毫秒
- 需要消息在 header 字段中包含时间戳,以便计算消息从发布者发送到接收者的时延
行为说明
默认情况下,主题统计(Topic Statistics) 测量未启用。在通过订阅配置选项为特定节点启用此功能后,特定订阅的 接收到的消息时延 和 接收到的消息周期 测量都会被启用。
这些数据以 statistics_msg/msg/MetricsMessage 的形式发布,周期可配置(默认 1 秒),发布到可配置的主题(默认 /statistics
)。请注意,发布周期也作为样本收集窗口周期。
由于接收到的消息周期需要消息在 header 字段中包含时间戳,如果缺失时间戳,则会发布空数据。也就是说,如果找不到时间戳,所有统计值为 NaN。发布 NaN 值而不是完全不发布,可以避免信号缺失的问题,并明确表示测量无法进行。
对于每个窗口中接收到的消息周期统计,第一个样本不会产生测量值。这是因为计算此统计值需要知道上一条消息到达的时间,因此窗口中的后续样本才会生成测量值。
此功能目前仅在 ROS 2 Foxy 的 C++(rclcpp) 中得到支持。
7. RQt 的概述与使用
RQt 是一个图形用户界面框架,以插件的形式实现了各种工具和接口。所有现有的 GUI 工具都可以作为可停靠窗口在 RQt 中运行。这些工具仍然可以以传统的独立方式运行,但 RQt 使在单一屏幕布局中管理各种窗口变得更加方便。
你可以通过以下方式轻松运行任何 RQt 工具/插件:
rqt
该图形界面允许你选择系统上可用的任何插件。你也可以在独立窗口中运行插件。例如,RQt Python 控制台:
、
ros2 run rqt_py_console rqt_py_console
用户可以使用 Python 或 C++ 为 RQt 创建自己的插件。要查看系统中可用的 RQt 插件,请运行:
ros2 pkg list
然后查找以 rqt_ 开头的包。
通过 deb 安装
sudo apt install ros-humble-rqt*
RQt 组件结构
RQt 由两个元包(metapackage)组成:
- rqt — 核心基础模块
- rqt_common_plugins — 常用的调试工具
RQt 框架的优势
与从零构建 GUI 相比:
- 提供标准化的 GUI 通用流程(启动-关闭钩子、恢复上次状态)
- 多个控件可以停靠在同一个窗口中
- 可以轻松将现有 Qt 控件转换为 RQt 插件
- 可在 Robotics Stack Exchange(ROS 社区问答网站)获得支持
从系统架构角度看:
- 支持多平台(基本上在 QT 和 ROS 可运行的环境下)和多语言(Python、C++)
- 生命周期可管理:RQt 插件使用通用 API,使维护和重用更容易
8. Composition
在 ROS 2 中,Composition(组件化/组合)指的是 将多个节点(Node)在同一个进程中运行 的机制,而不是每个节点都独立运行在自己的进程中。它的主要目的是提高性能和资源利用率,同时方便管理。具体解释如下:
在传统 ROS 2 中,每个节点通常运行在独立进程中(standalone nodes),它们通过 DDS 中间件通信。这种方式虽然隔离性好,但存在一些性能开销:
- 进程间通信(IPC)比进程内通信慢
- 每个节点都需要占用独立的内存和 CPU 资源
- 节点管理和启动开销较大
Composition 允许将多个节点以 组件(component) 的形式加载到同一个进程中运行,通常使用 rclcpp_components(C++)或相应 Python 接口:
- 节点作为 可加载组件(Composable Node) 编译
- 使用 Component Manager 在同一个进程中动态加载或卸载这些组件
- 节点之间可以直接共享内存进行通信,减少 IPC 开销
在 ROS 1 中,你可以将代码编写为 ROS 节点(node) 或 ROS 节点插件(nodelet)。
ROS 节点 会被编译成可执行文件。ROS 节点插件(nodelet) 则被编译成共享库(shared library),由容器进程在运行时加载。
在 ROS 2 中,推荐的代码编写方式类似于 nodelet——我们称之为 Component(组件)。这种方式便于在现有代码中添加通用概念,例如生命周期(life cycle)。ROS 2 避免了 ROS 1 中的最大缺点——不同 API 的问题,因为两种方式都使用相同的 API。
8.1 组件容器(Component Container)
组件容器是一个 宿主进程,允许你在同一进程空间中 运行时加载和管理多个组件。
截至目前,以下通用组件容器类型可用:
- component_container
最通用的组件容器,使用单个 SingleThreadedExecutor 来执行所有组件。 - component_container_mt
使用单个 MultiThreadedExecutor 来执行组件的容器。 - component_container_isolated
为每个组件使用独立执行器的容器:可以是 SingleThreadedExecutor(默认)或 MultiThreadedExecutor。
8.2 编写组件(Writing a Component)
由于组件仅被构建为 共享库(shared library),因此它没有 main
函数(参见 Talker 源代码)。组件通常是 rclcpp::Node
的子类。由于组件不控制线程,因此在构造函数中不应执行任何耗时或阻塞任务。相反,可以使用 定时器(timer) 来获取周期性通知。此外,组件可以创建 发布者(publisher)、订阅者(subscription)、服务端(server) 和 客户端(client)。
将类制作成组件的一个重要方面是,该类使用 rclcpp_components
包中的宏进行注册(参见源代码的最后一行)。这使得组件在库被加载到运行中的进程时可以被发现——它充当一种入口点。
此外,一旦组件被创建,它必须在 索引(index) 中注册,以便工具能够发现它。
add_library(talker_component SHARED src/talker_component.cpp)
rclcpp_components_register_nodes(talker_component "composition::Talker")
# 若要在同一个共享库中注册多个组件,可调用多次:
# rclcpp_components_register_nodes(talker_component "composition::Talker2")
Component(组件) 和 普通 Node(节点) 的主要区别在于 运行方式、可复用性和性能,可以从以下几个方面理解:
特性 | 普通 Node | Component |
---|---|---|
编译形式 | 编译为独立可执行文件 | 编译为共享库(shared library) |
启动方式 | 直接运行可执行文件 | 由 Component Container 在运行时加载 |
是否有 main | 有 main 函数 | 没有 main 函数 |
8.3 使用组件(Using Components)
composition 包提供了几种使用组件的不同方法。其中最常用的三种是:
-
启动一个通用容器进程(generic container process),并调用容器提供的 ROS 服务
load_node
。该 ROS 服务会根据传入的包名和库名加载指定组件,并在运行中的进程内开始执行。- 除了通过编程调用 ROS 服务外,你也可以使用命令行工具,通过传入命令行参数来调用该 ROS 服务。
-
创建一个自定义可执行文件,其中包含在编译时已知的多个节点。
- 这种方法要求每个组件都有对应的头文件(header file),而第一种方法并不严格要求。
-
创建一个启动文件(launch file),使用 ros2 launch 创建一个容器进程,并加载多个组件。