一、什么是 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=Truesetsid() 启动,脱离了进程组
  • 节点代码里自己屏蔽了 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
Logo

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

更多推荐