一、什么是 Launch?
简单说,Launch 就是 ROS2 的"一键启动器"。你写一个 Python 脚本,把要启动的节点、参数、话题重映射全声明在里面,一条 ros2 launch 命令全部拉起来。
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(package='robot_driver', executable='odom_node'),
Node(package='cartographer_ros', executable='cartographer_node'),
Node(package='rviz2', executable='rviz2'),
])
本质上,ros2 launch 启动后会在系统层面创建一个进程组,所有子节点都挂在这个进程组下面,由 Launch 统一管理生命周期。
当我们按下ctrl+c时:
Ctrl+C (SIGINT)
│
▼
┌─────────────────────────────────────────────────┐
│ ① Python signal handler 被触发 │
│ signal.signal(SIGINT, handler) │
│ → 不直接退出,而是向 asyncio 事件循环 │
│ 投递一个 "shutdown" 事件 │
└──────────────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────────────┐
│ ② LaunchService._shutdown() 被调用 │
│ → 停止接受新的 Action │
│ → 遍历所有【正在运行的子进程】列表 │
└──────────────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────────────┐
│ ③ 向每个子进程发送 SIGINT(第一轮) │
│ process.send_signal(signal.SIGINT) │
│ → 给子进程一个"优雅退出"的机会 │
│ → 子进程可以:保存地图、关闭设备、flush日志 │
└──────────────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────────────┐
│ ④ 等待子进程退出(带超时) │
│ asyncio.wait_for(proc.wait(), timeout) │
│ → 默认超时约 5~10 秒(版本不同有差异) │
│ → 期间持续监控每个子进程的退出状态 │
└──────────────────────┬──────────────────────────┘
▼
子进程是否全部退出?
/ \\
是 否(超时了还没死)
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────────────┐
│ ⑤a 清理完毕 │ │ ⑤b 升级:发送 SIGTERM │
│ 关闭事件循环 │ │ process.terminate() │
│ Launch 退出 │ │ → 再等一小段时间 │
└──────────────────┘ │ → 还不行?SIGKILL 强杀 │
│ process.kill() │
└──────────────────────────┘
二、什么情况下杀死 Launch 会导致孤儿节点?
直接 kill -9 <pid> 把 launch 进程杀了,结果后台还跑着三四个节点,ros2 node list 一看全是"幽灵"。
原因:kill -9 发的是 SIGKILL,这个信号不可捕获、不可拦截。 Launch 进程瞬间暴毙,根本来不及向子进程转发退出信号,子进程就成了孤儿,被系统 init 进程接管,继续在那跑。
除了 kill -9,还有几种情况也会产生孤儿:
- Launch 进程自己段错误崩了(父进程异常退出)
- 子进程通过 shell=True 或 setsid() 启动,脱离了进程组
- 节点代码里自己屏蔽了 SIGINT/SIGTERM
正确姿势: 永远用 Ctrl+C(SIGINT)或 kill -15(SIGTERM),给 Launch 一个"善后"的机会。
三、什么是流控反压(Backpressure)?
一句话:消费端太慢 → 缓冲区堆满 → 压力反向传导回生产端 → 生产端被迫停发。
生活类比:你拿水管往杯子里灌水,杯子满了水溢出来。如果杯子有个密封盖(缓冲区有限),水灌不进去,你就被迫关水龙头——这个"被迫关"就是反压。
在 DDS 里,Reliable 模式 + 有限队列深度 可能导致流控反压:
- Publisher 发数据进队列
- Subscriber 消费后回 ACK
- 如果 Subscriber 太慢,ACK 迟迟不来,队列堆满
- Publisher 的 publish() 被阻塞(或返回失败)
- 生产端被消费端拖死了
Best Effort 模式没有 ACK 机制,队列满了直接丢最旧的,不存在反压,但也意味着可能丢数据。
四、Cartographer 崩溃问题
(ROS机器人-从零开始每日日志记录day3-CSDN博客)文中问题
我的 odom 发送端配置:
auto qos = rclcpp::QoS(rclcpp::KeepLast(10)).reliable();
odom_pub_ = create_publisher<nav_msgs::msg::Odometry>("/odom", qos);
odom发送端的发送模式是reliable且队列长度为10,当carto对odom的消费不及时时,odom数据积压,导致odom的发送端停发odom数据,odom数据停发又会导致carto拿不到odom数据进而形成恶性循环。
Cartographer 内部计算量大(大角度转弯后扫描匹配、子图构建)
→ 消费 /odom 速度下降
→ DDS ACK 延迟
→ Publisher 队列(depth=10)堆满
→ Reliable 模式触发反压,odom 停发
→ Cartographer 拿不到新 odom
→ 处理更加停滞 → 消费更慢 → 队列更满
→ 恶性循环
解决
将odom 发送模式改成 Best Effort:
auto qos = rclcpp::QoS(rclcpp::KeepLast(5))
.best_effort()
.durability_volatile();
五、DDS 两节点互通的三个前提
当上层UDP消息无法到达小车时,要考虑是不是消息在转换的时候DDS不通。
| 同 ROS_DOMAIN_ID | 不同 domain 逻辑隔离,互相不可见(默认 0) |
| 同 RMW 实现 | FastDDS 和 CycloneDDS 之间不互通 |
| QoS Profile 兼容 | Best Effort 的 Pub 无法被 Reliable 的 Sub 订阅 |
排查命令:
echo $ROS_DOMAIN_ID
echo $RMW_IMPLEMENTATION
ros2 topic info /odom –verbose
网硕互联帮助中心



评论前必须登录!
注册