1、 Wait Set
Wait Set = Executor 用来统一等待多个 ROS2 事件的机制。
你可以类比前面 Linux 学过的:
poll / epoll
虽然实现和语义不是完全一样。
概念上都是:
我有很多事件源
↓
不要一个个死循环检查
↓
统一等谁 ready
Linux:
socket1
socket2
timerfd
eventfd
↓
epoll_wait()
ROS2:
subscription
timer
service
client
↓
Executor / Wait Set
都是:
事件驱动。
但不要说:
ROS2 Executor = epoll
它们不是同一个东西。
只是思维方式相似。
比如:
/mocap/human_pose
来了一条消息。
大概:
DDS收到
↓
RMW层标记subscription ready
↓
Executor从等待状态醒来
↓
选择这个subscription
↓
执行callback
所以中间可能存在:
Executor scheduling delay
Executor 和 worker thread 怎么配合?
很常见结构:
Executor callback
↓
快速接收数据
↓
push到queue
↓
立即返回
Worker Thread
↓
从queue取
↓
做重计算
这样 Executor 不容易被重任务堵住。
spin_some()
概念上:
spin()
= 一直运行Executor
而:
spin_some()
可以理解为:
处理当前一批 ready 的 callback,然后返回。
它适合某些:
自定义主循环
场景。
但不要为了"可控"就随便自己手写复杂 spin loop。
spin_once()
简单理解:
处理一次 Executor 工作机会,然后返回。
这种常见于:
集成其他event loop
测试
自定义主循环
但普通 Node:
rclcpp::spin()
最简单。
Executor 线程到底有几个?
取决于 Executor 类型。
SingleThreadedExecutor
→ 通常一个执行线程
MultiThreadedExecutor
→ 多个worker线程
但注意:
DDS 自己还可能有其他内部线程。
所以:
ROS2进程只有一个Executor线程
不代表:
整个进程只有一个线程
拿到 ROS2 工程时怎么检查 Executor?
你以后可以按这个顺序:
① 找 main()
↓
② 看用了什么 Executor
↓
③ 看 add_node / spin
↓
④ 找 callback
↓
⑤ 看哪些callback可能阻塞
↓
⑥ 看有没有自定义thread
↓
⑦ 看共享数据同步
这是非常实用的源码阅读习惯。
2、MutuallyExclusive
名字拆开:
Mutually
= 相互
Exclusive
= 排斥
也就是:
同一个 group 里的 callback 同一时间只允许一个执行。
例如:
Group 1: MutuallyExclusive
├── Subscription A
├── Service B
└── Timer C
即使:
MultiThreadedExecutor
有 8 个线程
A、B、C 也不会同时执行。
它们仍然是:
A
↓
B
↓
C
串行。
最大好处:
减少共享数据竞争
比如:
Subscription callback
修改 current_state_
Service callback
也修改 current_state_
放在同一个:
MutuallyExclusive Group
可以保证:
不会同时操作 current_state_
这比到处:
mutex.lock()
mutex.unlock()
有时候更简单。
3、Reentrant
简单理解:
这个 group 里的 callback 允许并发执行。
例如:
Group 2: Reentrant
├── Subscription X
├── Subscription Y
└── Timer Z
在 MultiThreadedExecutor 下,
可能:
Thread1 → X
Thread2 → Y
Thread3 → Z
同时执行。
Reentrant 更严格一点的含义是:
同一个 callback 逻辑也应该能够在尚未结束时,再次被进入。
比如某个 Subscription 消息来的很快:
Callback instance 1
还没结束
新消息又来了,
如果调度条件允许:
Callback instance 2
可能同时执行。
所以代码必须真的能承受:
并发进入
4、Callback Group
所以你可以把它理解成:
业务层并发约束
不是:
纯性能优化开关
你不是为了:
"线程越多越快"
而是表达:
哪些操作互相冲突
哪些操作彼此独立
和 mutex 有什么区别?
mutex:
保护具体共享资源
Callback Group:
控制 callback 是否同时执行
例如:
Group A MutuallyExclusive
让两个 callback 不并行。
但如果:
另一个 worker thread
访问同一资源,
Callback Group 管不到。
这时仍然需要:
mutex / atomic / lock-free
什么时候用 Callback Group,什么时候用 mutex?
可以这样理解。
如果两个 callback:
本来就不应该同时执行
可以考虑:
MutuallyExclusive Group
如果:
允许并行
但只在某一小段共享数据上冲突
更适合:
mutex
否则整个 callback 全部串行,
可能损失太多并发能力。
不要以为:
command callback
放 MutuallyExclusive group
就能保护:
RT thread
因为 RT thread 是 Executor 外部线程。RT thread 就是要求"必须在规定时间内完成"的线程。它不是跑得更快,而是"更可预测、更守时"
所以:
Callback Group
= ROS2 Executor并发控制
不是整个进程的全局线程同步机制。
CPU-bound 和 I/O-bound
CPU-bound
程序大部分时间在算,CPU 跑满。
例如:
GMR IK
图像处理
点云计算
矩阵运算
主要消耗:
CPU计算
这种任务线程太多:
很容易把CPU打满
I/O-bound
程序大部分时间在等,CPU 闲着。
例如:
等待socket
等待磁盘
等待设备
大量时间可能:
在阻塞等待
这类任务适当更多线程可能有价值。
但 ROS2 callback 里长期阻塞 I/O 本身通常就该谨慎。
怎么优化共享状态?
常见思路:
减少共享数据
缩小mutex临界区
copy snapshot
atomic counter
producer-consumer queue
double buffer
而不是一上来:
全局一把大锁
copy-under-lock
比如共享:
latest_command_
可以:
Command local;
{
std::lock_guard<std::mutex> lock(mutex_);
local = latest_command_;
}
// 锁已经释放
process(local);
这样:
锁只保护复制
重计算:
在锁外完成
能显著减少竞争。这和实时线程也非常重要
比如:
ROS2 callback
↓
更新 latest command
实时线程:
读取 command
不要让 RT thread:
长期等一个被ROS2 callback持有的mutex
更倾向:
短锁
snapshot
RealtimeBuffer
来降低阻塞。
Executor thread 和 RT thread 要区分
例如:
MultiThreadedExecutor
4 workers
这些通常仍是:
普通ROS2线程
而你的:
EtherCAT 1kHz RT Thread
可以是:
独立线程
CPU affinity
SCHED_FIFO
所以架构:
ROS2 Executor Threads
↓
非实时域
Realtime Buffer
RT Control Thread
↓
实时域
不要把:
MultiThreadedExecutor
理解成:
自动实时线程池
怎么判断是否需要 MultiThreadedExecutor?
问这几个问题:
① 有没有一个callback明显拖住其他callback?
② 哪些callback之间真的可以安全并行?
③ CPU是否有余量?
④ 是否存在大量锁竞争?
⑤ 顺序是否重要?
如果没有明确并行需求:
SingleThreadedExecutor
完全可以继续用。
5、on_configure
通常负责:
读取参数
打开设备
初始化资源
检查配置
建立连接
分配buffer
例如:
unconfigured
↓
on_configure()
↓
inactive
注意:
配置完成后,设备不一定已经开始输出控制。
它只是:
"准备好了"
6、on_activate
例如:
inactive
↓
on_activate()
↓
active
这一步才真正:
启动周期线程
开始采集
开始 publish
使能控制器
允许输出
也就是:
从"准备好"切换到"真正工作"。
7、on_deactivate
停用、取消激活。在 ROS 2 里,它最常见的场景是 生命周期节点(Lifecycle Node) 和 ros2_control 控制器,表示让一个正在运行的东西停止工作,但不销毁。
active
↓
on_deactivate()
↓
inactive
通常负责:
停止周期输出
停止采集
让设备进入安全状态
停止发布活动数据
但可以保留:
已打开的设备
已加载的配置
已分配的资源
所以之后还能:
再次 activate
而不用全部重新初始化。
deactivate 可以做什么?
比如:
停止 receive thread
停止 publish
但:
socket可以先保留
配置保留
以后:
activate
再启动。cleanup 可以真正释放
比如:
join thread
close socket
clear queue
destroy MotionInput
然后回:
unconfigured
这样资源生命周期非常明确。
8、on_cleanup
inactive
↓
on_cleanup()
↓
unconfigured
这时通常:
关闭socket
关闭设备
释放buffer
清理内部状态
回到:
像刚创建时那样
Created
↓
Configured
↓
Running
↓
Stopped
↓
Released
只是 ROS2 给你定义了一套统一生命周期语义。
9、activate
inactive
↓
activate
做:
启动 receive thread
开始解析帧
开始 publish /mocap/human_pose
于是进入:
active
这就比:
构造函数一上来直接收包
清晰很多。
这和 RAII 和Lifecycle
RAII 负责:
对象生命周期内,资源最终一定正确释放。
Lifecycle 负责:
运行过程中,资源在哪个状态被创建、启动、停止、释放。
所以:
RAII
= C++资源安全
Lifecycle
= 系统运行状态管理
两者是互补的。
10、Lifecycle transition
状态之间的切换叫:
transition
比如:
configure
activate
deactivate
cleanup
shutdown
每个 transition 会触发对应 callback。
transition callback
比如:
on_configure()
只负责:
配置阶段资源
不要里面又:
启动控制线程
开始运动
发控制命令
否则:
configure
和:
activate
语义就乱了。
Lifecycle 和 systemd
systemd 看:
进程服务生命周期
比如:
started
running
failed
Lifecycle 看:
ROS2 Node内部业务状态
比如:
unconfigured
inactive
active
所以:
systemd
↓
进程层
Lifecycle
↓
ROS2功能层
退出:
on_deactivate()
↓
stop_requested = true
↓
join receive thread
↓
确认不再访问socket
之后 cleanup:
close socket
destroy source
这就很安全。
on_init
→ 读取硬件描述
on_configure
→ 打开EtherCAT / 初始化设备
on_activate
→ 允许read/write周期运行
read
→ 读状态
write
→ 写命令
on_deactivate
→ 停止输出/安全关闭
Diagnostics = 把系统健康状态结构化地发布出来。
不是靠:
printf
RCLCPP_INFO
人工翻日志
而是让系统持续产生:
OK
WARN
ERROR
STALE
这样的健康状态。
OK
WARN
ERROR
STALE
简单理解:
OK
= 正常
WARN
= 还能运行,但已经有异常趋势
ERROR
= 功能已经明显异常
STALE
= 很久没有更新,状态可能已经过期
Diagnostics 和日志
日志:
10:00:01 disconnected
10:00:02 reconnecting
10:00:03 reconnect failed
Diagnostics:
Current Status:
ERROR
connected = false
retry_count = 3
last_error = "connection timeout"
所以:
日志
= 过程记录
Diagnostics
= 当前健康摘要
两者应该一起用。
Safety 阈值和 Diagnostics 阈值
例如:
Diagnostics:
队列使用率 > 80%
→ WARN
这只是健康告警。
而:
Safety:
command timeout > 100ms
→ safe stop
这是控制动作。
所以:
Diagnostics
= 告诉你系统状态
Safety
= 决定系统怎么保护自己
不要混在一起。
Lifecycle
= 逻辑状态
Diagnostics
= 健康状态
两者组合特别有价值。
systemd:
进程活着吗?
Lifecycle:
Node处于什么运行阶段?
Diagnostics:
业务健康吗?
可以理解:
systemd
↓
Process Alive
Lifecycle
↓
Node State
Diagnostics
↓
System Health
11、ros2_control
ros2_control = ROS2 里用于统一"控制器"和"真实/仿真硬件"的框架。
它解决的问题是:
上层控制器
不应该直接知道
底层电机/总线细节
所以会分层。
最核心关系:
Controller
↓
Command Interface
↓
Hardware Interface
↓
真实硬件
状态反过来:
真实硬件
↓
Hardware Interface
↓
State Interface
↓
Controller / broadcaster
controller_manager
hardware_interface
controller
joint_state_broadcaster
command/state interface
分别是:
controller_manager
= 管理控制器和硬件生命周期
hardware_interface
= ROS2和真实硬件之间的适配层
controller
= 根据状态生成命令
joint_state_broadcaster
= 把硬件状态发布成 /joint_states
command/state interface
= 控制器和硬件之间交换数据的接口
ROS2
│
joint_trajectory_controller
│
command interfaces
│
controller_manager
│
Hardware Interface
│
EtherCAT
│
Motors
状态反过来:
Motors
↓
EtherCAT
↓
Hardware Interface
↓
state interfaces
↓
joint_state_broadcaster
↓
/joint_states
这两条链一定要记住。
Controller
↓
标准接口
↓
Hardware Interface
↓
厂商SDK / EtherCAT
这样上层控制器不需要知道:
PDO
Slave ID
CANopen object
EtherCAT驱动细节
Hardware Interface
把你的底层硬件驱动,包装成 ros2_control 能认识的标准接口。
例如你底层已经有:
EtherCAT Master
↓
read_motor_position()
write_motor_torque()
你要封装成:
Hardware Interface
给 ros2_control 使用。
effort control
在 ROS 2 里,effort 是"力/力矩"的标准叫法,effort control 就是直接控制关节输出力矩。effort control 不直接命令关节转到哪个角度,而是命令它"用多大力气"去转。关节实际怎么动,由力矩和外部环境共同决定。
如果硬件支持 torque:
State:
position
velocity
effort
Command:
effort
控制器可以算:
τ = ...
然后:
effort command
↓
write()
↓
EtherCAT
这种更接近动力学控制。
controller_manager
controller_manager = ros2_control 的核心管理器。
它负责:
加载硬件
加载控制器
配置控制器
激活控制器
切换控制器
执行控制循环
所以你之前看到:
/controller_manager
它不是一个普通业务节点,它是整个 ros2_control 系统的核心。
joint_state_broadcaster
它也是一种 controller/broadcaster。但它不输出电机命令。
它做:
读取 state interfaces
↓
发布 /joint_states
所以:
Hardware Interface
↓
position/velocity/effort
↓
joint_state_broadcaster
↓
/joint_states
电机编码器
↓
EtherCAT
↓
Hardware Interface read()
↓
State Interfaces
↓
joint_state_broadcaster
↓
/joint_states
↓
robot_state_publisher
↓
/tf
↓
RViz
现在你之前所有知识已经彻底串起来了。
joint_trajectory_controller
上层给它:
JointTrajectory
里面有:
joint_names
positions
velocities
time_from_start
然后它按照轨迹:
不断生成关节命令
再写到 command interface。
不是:
JointTrajectory
↓
EtherCAT
而是:
JointTrajectory
↓
joint_trajectory_controller
↓
command interface
↓
Hardware Interface
↓
EtherCAT
中间有 controller。
forward_command_controller
它比 trajectory controller 简单。
可以理解:
上层给什么 command,它就直接往对应 command interface 写。
比如:
position command
直接:
forward_command_controller
↓
Hardware Interface
适合:
简单测试
上层已经算好目标
Hardware Interface 生命周期
on_init()
↓
on_configure()
↓
on_activate()
↓
read()/write()
↓
on_deactivate()
on_init()
一般做:
读取 hardware info
解析 joint/interface 配置
初始化成员变量
不要马上:
使能电机
on_configure()
适合做:
打开 EtherCAT master
扫描 slave
检查设备
配置 PDO
分配 buffer
如果失败:
configure失败
不要进入 active。
on_activate()
真正:
允许周期控制
同步初始状态
使能输出
启动RT相关资源
进入工作状态。
read()
周期性:
EtherCAT receive
↓
解析PDO
↓
position/velocity/effort
↓
state interfaces
write()
周期性:
读取 command interfaces
↓
Safety Check
↓
构造PDO
↓
EtherCAT send
你之前的 Safety Layer 就非常适合在这里附近发挥作用。
Safety 应该在哪?
比如:
Controller command
↓
Hardware Interface
↓
Validity Check
↓
Joint Limit
↓
Velocity Limit
↓
Command Timeout
↓
Fault State
↓
EtherCAT write
这样即使上层 Controller 出了异常:
底层仍然有最后一道软件保护
这和你之前的目标设计完全一致。
Command Timeout 放哪里合适?
如果上层命令:
长期没更新
底层不应该:
永远重复最后一个command
可以维护:
last_command_time
如果:
now - last_command_time > timeout
则:
进入safe state
这可以放在:
controller
或者:
hardware/safety layer
更严格场景通常底层还要自己兜底。
插件机制
Hardware Interface 通常会做成:
plugin
比如:
MyEthercatSystem
controller_manager 在运行时加载。
这意味着:
controller_manager
不需要重新编译成只支持你的某一块硬件,它通过插件机制加载具体实现。
如果插件加载失败,怎么排?
比如:
Failed to load hardware plugin
你可以从几层看:
plugin XML配置
↓
.so有没有生成
↓
动态库路径
↓
依赖库
↓
符号
工具:
ldd libmy_hardware.so
甚至:
nm -D
这就是 C++ 工程能力和 ROS2 结合。
标准排查可以这样:
Controller active?
↓
Command topic 有数据吗?
↓
Controller有没有收到?
↓
Command interface值有没有变化?
↓
Hardware write()有没有被调用?
↓
EtherCAT有没有发?
↓
Drive状态正确吗?
不要只停在:
controller active