ROS 2 的软件问题可以先分成四类:节点退出了、节点卡住了、节点收不到数据、节点运行得太慢。 它们需要不同的证据。GDB 看进程停下来的调用栈和变量,日志记录业务经过,ros2 tracing 记录运行过程中的事件时间线;strace 看系统调用,perf 找 CPU 热点。先问清楚想证明什么,再选工具,会比同时打开所有工具更容易定位问题。
本文以 Ubuntu 22.04 + ROS 2 Humble + C++ / rclcpp 为操作基线,资料核对日期为 2026-09-27。命令在 Linux 的 Bash 中执行;macOS 用户应进入 Linux 容器或虚拟机。Jazzy、Rolling 的 tracing 子命令和默认配置可能不同,不能把新版本文档的命令直接当作 Humble 命令。
随文提供一个包含“正常、异常、慢回调”三种模式的最小示例包。示例编译与 GDB 验证结果见验证记录;tracing 部分给出官方配置流程及教学时间线,教学数字不是本站机器的 tracing 实测结果。
一、按现象选择工具
| 现象 | 先做什么 | 工具能回答的问题 | 不能据此直接判断 |
|---|---|---|---|
| 节点突然退出、segfault、abort | 保存日志、GDB / core | 停在哪一行?调用链和变量是什么? | 崩溃位置一定是最早写坏内存的位置 |
| 进程还在,但没有输出 | CPU / 线程状态、所有线程栈 | 在计算、睡眠、等锁,还是等响应? | 栈里出现 futex 就是死锁 |
| topic 有名字,但收不到消息 | 图、类型、QoS、域、发现 | 发布者与订阅者是否真的匹配? | topic list 有名字就说明数据链路正常 |
| 频率下降、偶尔卡顿 | tracing,再按证据用 perf | 哪个回调耗时?何时开始和结束? | 回调耗时长一定是 CPU 计算多 |
| 随机内存错误 | ASan / UBSan,再回到 GDB | 越界、释放后使用等发生在哪里? | 一次无报错就证明没有内存问题 |
| 相同输入偶发不同结果 | rosbag 固定输入、保留参数和版本 | 改动前后是否能稳定比较? | bag 重放能复刻原来的线程调度 |
backtrace 与 tracing 不是同一件事。 前者是暂停时的函数调用栈;后者是不断记录事件,结束后再按时间分析。ROS 2 提供了日志、CLI 和 tracing 的集成入口;GDB、LTTng、perf 则是更通用的工具。
对真实机器人,GDB 断点会暂停控制进程;练习时先在仿真或脱离执行机构的环境进行。测性能时撤掉断点,并记录日志和跟踪配置,避免把调试开销误当成系统原有的延迟。
二、先把现场保存下来
2.1 环境与版本
在启动目标程序的终端中执行。这里假设自己的工作区叫 ~/diagnostics_ws;实际工程替换成自己的路径。
source /opt/ros/humble/setup.bash |
环境变量没有输出,通常表示未显式设置,不等于相关功能没有默认值。pkg prefix 则能检查是否加载了错误的 overlay:你刚改了源码,却一直在运行另一份安装结果,是常见的“修复无效”。ros2 doctor 是环境检查入口,不是应用逻辑正确性的证明。ROS 2 doctor 教程
每次复现至少记下:代码提交、ROS 包版本、完整启动命令、参数 YAML、输入数据、故障时间、实际进程 PID。不要只保存最后一行 process has died。
2.2 图正常,不代表通信正常
以下命令针对后面的小实验,换成自己的节点名和 topic 名即可:
ros2 node list |
echo 和 hz 会持续运行,检查后按 Ctrl+C,再执行下一条。按这个顺序看:
- 节点名、命名空间、重映射是否与你预期一致。
- topic 类型和发布/订阅端点是否正确。
- Reliability、Durability 等 QoS 是否兼容。Best Effort 发布者不能满足要求 Reliable 的订阅者。
- 两端 ROS_DOMAIN_ID、网络可达性和发现配置是否一致。
- 回调有没有真正得到执行;进程存在不代表 Executor 在正常推进。
订阅传感器的 Best Effort 数据时,可显式试验:
ros2 topic echo /scan --qos-reliability best_effort |
ros2 topic hz 测到的是这个 CLI 订阅者的接收节奏,会受它的 QoS、系统负载和网络影响,不能直接当成发布者内部定时器频率。需要核对目标进程自身时间线时用 tracing。官方 topic 教程、QoS 兼容性
只有怀疑 CLI 图缓存异常,或修改了域/RMW 后,才考虑 ros2 daemon stop、ros2 daemon start;它们重启的是 CLI daemon,不会重启你的业务节点,也不会修复 QoS。
三、日志:先知道程序走到了哪里
3.1 配置级别和输出位置
export ROS_LOG_DIR="$HOME/ros2-debug-logs" |
diagnostic_demo:=debug 指定 logger 名称;不确定名称时先用 --log-level debug。logger 名称通常与节点名相关,代码也可以创建子 logger。没有在编译产物中保留的日志,运行时调高等级也无法恢复。
默认日志目录通常是 ~/.ros/log,也会受 ROS_HOME 影响;这里通过 ROS_LOG_DIR 显式指定。控制台、磁盘日志和 /rosout 是不同输出路径;ros2 launch 还会管理自己的进程日志,不应假定所有信息都在同一个文件。图形环境可用 ros2 run rqt_console rqt_console 按节点和级别筛选 /rosout。日志配置文档
3.2 读日志时找“第一处偏离预期”
例如“开始处理请求 42”之后再无“请求 42 处理完毕”,这个区间就值得打断点或加测量。推荐带上输入编号、状态、关键尺寸和错误码,不要只打印“到了这里”。
RCLCPP_DEBUG(get_logger(), "request=%u size=%zu", request_id, values.size()); |
这段是插入业务代码的片段,变量由业务提供;2000 是毫秒。节流能减少刷屏,但使用的 clock 若为仿真时钟且停止推进,也会影响节流表现。计量代码执行时长优先用 std::chrono::steady_clock;消息业务时间仍按你的 ROS 时间语义处理。
四、准备一个能亲手排查的小程序
包中只有一个 100 ms 定时器,每次往 /heartbeat 发布字符串。参数 mode 在启动时读取:
| 模式 | 行为 | 学习目标 |
|---|---|---|
normal |
直接发布 | 建立基线 |
crash |
第五次回调用 values.at(5) 访问长度为 3 的 vector |
捕获 C++ 异常、读调用栈 |
slow |
每次回调睡眠 80 ms 后发布 | 区分墙钟耗时与 CPU 时间 |
下载 package.xml、CMakeLists.txt、diagnostic_node.cpp,按下面的目录存放。网站仓库中已有同样的目录,可直接复制。
~/diagnostics_ws/src/diagnostic_demo/ |
Ubuntu 中先确保已安装 ROS 2 Humble,并配置其 apt 源,再安装调试和构建工具:
sudo apt update |
看到递增的 count 后按 Ctrl+C。每个新终端都需要 source;--symlink-install 也不意味着改 C++ 后无需重新编译。
实际工程里,Debug 适合单步和看变量;RelWithDebInfo 更接近日常优化构建,并保留调试信息,但可能出现 <optimized out>、内联、单步跳行。定位问题与性能对比最好使用独立构建目录。只重编自己的包,不会自动给 /opt/ros 中已经安装的第三方库补上符号。ROS 2 GDB 接入教程
五、GDB:崩溃后怎么看
5.1 从 ros2 run 启动
ros2 run --prefix 'gdb -q --args' diagnostic_demo diagnostic_node \ |
出现 (gdb) 后,下面输入的是 GDB 命令:
set pagination off |
catch throw 让程序在 C++ 异常抛出时就停下来,而不是等最终 abort。本例会抛出 std::out_of_range;它不是故意制造 SIGSEGV。若想直接运行到崩溃,可省略 catch throw,或启动时使用 gdb -q -ex run --args。
catch throw 也会捕获第三方库随后自行处理的异常。本次容器验证中,Fast DDS 初始化就曾先触发 catchpoint,所以第一次停住不一定是目标故障。先看 bt;若在初始化阶段,可 continue。为了直接聚焦本例,也可以在全新 GDB 会话中先执行 break DiagnosticNode::tick if count_ == 4、run,到达业务回调后 delete 1、catch throw、continue。
不要写 gdb --args ros2 run ... 来调试 C++ 节点:ros2 是 Python CLI 入口,真正需要调试的是它启动的 ELF 程序。--prefix 把 GDB 放在节点可执行文件前面。
5.2 栈帧的正确方向
下面是结构示意,实际帧号和库函数层数随环境变化:
#0 __cxa_throw (...) |
#0 是当前最内层执行位置,#1 是它的调用者,编号更大则更靠近外层调用者。 先从 #0 往外找自己的源码帧,再看是谁把什么参数传进来。可以从 main 往回理解调用路径,但不能把最底部当成当前故障指令。GDB backtrace 手册
假设你实际输出中 DiagnosticNode::tick 是第 4 帧:
frame 4 |
本例应该得到 count_ = 5,values 只有三个元素。由此能形成可验证结论:第五次定时器回调访问了非法下标 5,抛出异常,且没有被捕获。 “GDB 停在 libstdc++ 里面,所以 libstdc++ 有 bug”则不成立。
SIGSEGV 往往提示非法内存访问;SIGABRT 可能来自断言失败、未捕获异常、显式 abort 或分配器检测到内存破坏。信号是线索,需要结合栈和此前日志。
5.3 条件断点、单步和记录
另起一次调试,用下面这些命令在第五次回调进入前观察状态:
break DiagnosticNode::tick if count_ == 4 |
条件是 4,因为 tick() 进入后才执行 ++count_。next 通常跨过函数调用,step 进入函数,finish 跑到当前函数返回;遇到标准库模板不要机械地一直 step。
保存一次完整现场:
set logging file /tmp/ros2-gdb.txt |
不知道变量时优先缩小到业务栈帧再 print。No symbol 先查编译符号及程序版本;No source file 查源码路径;<optimized out> 查优化选项。容器内构建、宿主机看源码时,可以用 set substitute-path /build/ws /local/ws 映射路径,前提是两边确实是同一份源码。
5.4 Launch、组件与插件
对于无 GUI 的 launch,最省事的自动取栈方式是给目标 Node加批处理前缀。以下是完整 launch 结构,保存成 debug.launch.py 后可用 ros2 launch ./debug.launch.py 执行:
from launch import LaunchDescription |
批处理会在崩溃停止后输出栈并退出,不提供交互提示符。要交互调试,图形 Linux 可改为 prefix=['xterm -e gdb -q -ex run --args'](需安装 xterm 和可用 DISPLAY),无 GUI 则在独立终端 attach。
Composable Node 是共享库,被装入 component container;ros2_control 的控制器和硬件插件也通常在承载进程中运行。给容器/manager 进程加 GDB,或 attach 到它的 PID,不能拿插件 .so 当可执行文件。动态加载前可设 set breakpoint pending on,然后对插件类的方法下断点;调试符号必须包含该插件。ros2_control 调试说明
六、卡死与 core:没有正常退出提示怎么办
6.1 进程还活着:观察线程,再 attach
Linux 中先用 ps -eo pid,ppid,pcpu,stat,args 找到真正的节点或承载进程。不要把 ros2 launch 的 Python 父进程当成目标。
TARGET_PID=1234 # 改成核对过的实际 PID |
在 GDB 中:
set pagination off |
attach 通常会暂停目标。detach 放开进程,使其继续运行;采第二次快照前必须先让程序运行一段时间,不能对暂停的进程连续看两次栈就宣称“栈一直不变”。
| 栈/CPU 现象 | 下一步核查 |
|---|---|
| 高 CPU,栈常在自己的循环或算法 | 查循环边界、输入规模;用 perf 找热点 |
sleep、poll、epoll_wait |
可能正常等待;核对预期输入和超时 |
futex、mutex、condition variable |
找等待对象、持有者和唤醒条件;一次采样不能证明死锁 |
回调里 future.wait(),响应一直不处理 |
检查 Executor 线程和 callback group |
ROS 2 常见自锁链路是:定时器回调持有互斥 callback group → 同步等待 service 结果 → 处理结果又需要该 group → 两边无法推进。只换 MultiThreadedExecutor,但所有回调仍在同一个默认互斥组,通常解决不了。优先改成异步状态机;需要并行时再设计不同回调组和共享数据保护。Callback Groups 官方说明
若提示 ptrace: Operation not permitted,检查用户、Yama、容器 capability 和 seccomp。不要默认关掉整机安全限制。Linux Docker 中可在专用调试容器增加 --cap-add=SYS_PTRACE;仅在仍受 seccomp 阻止且已理解范围时增加 --security-opt seccomp=unconfined。不要给日常服务无条件加 --privileged。
6.2 已经退出:读 core
在启动目标的同一 shell 中设置,并先检查系统收集方式:
ulimit -c unlimited |
若系统使用 systemd-coredump,可通过 coredumpctl list 找崩溃记录,用 coredumpctl info PID 核对时间和可执行文件,然后 coredumpctl debug PID。这里的 PID 来自崩溃记录,不是随手选一个当前 PID。coredumpctl 手册
若拿到普通 core 文件:
gdb /exact/build/diagnostic_node /path/to/core |
进入后仍然用 thread apply all bt full。必须保留对应版本的可执行文件、共享库和符号;重新编译后的“同名文件”不一定匹配。ulimit 只控制限制,不保证当前目录出现 core;Ubuntu 的 Apport、systemd 配置、容器环境都可能改变去向。
七、ros2 tracing:把运行过程录成时间线
7.1 先分清四层
rclcpp / rcl / rmw 等代码中的 tracepoint |
ros2 trace 用来配置并控制采集,不会自动给任意 C++ 业务函数插桩。回调的开始/结束可以由 ROS 2 埋点提供;想知道回调内部某个矩阵求解阶段的耗时,需进一步采样或增加自己的测量点。
先学用户态 tracing,不需要一上来就安装内核模块。后续若要证明线程何时被抢占、何时进入 runnable 状态,再考虑 sched_switch、sched_wakeup 等内核事件以及相应的权限和分析工具。ros2_tracing Humble 说明
7.2 安装,并验证是否真的启用
在配置好 ROS apt 源的 Ubuntu 22.04 中:
sudo apt update |
有命令、库已启用、采到事件,是三个不同的检查。 status 应输出 Tracing enabled。若输出 Tracing disabled,仅安装 LTTng 命令还不够,需要重新构建启用了 LTTng 的 tracetools。
Humble 的二进制安装可使用独立 overlay,避免与自己的项目源码混在一起:
sudo apt install git python3-rosdep python3-colcon-common-extensions |
若 rosdep 尚未初始化,按报错先执行一次 sudo rosdep init;已初始化的系统不要重复。若你自己编译的 ROS 核心曾使用 TRACETOOLS_DISABLED=ON,只替换接口库不能恢复被删掉的调用点,需要重新配置、重编相关核心包;不要在这种情况下仅凭 status 就认为完整可用。
采集终端和应用终端都使用同一套 overlay。已经构建好的业务工作区可以在干净终端按以下顺序加载:
source /opt/ros/humble/setup.bash |
这里用 local_setup.bash 追加当前层,避免业务工作区的 setup.bash 自动重放旧 underlay 链。环境改变后重启业务节点;进程已经加载的共享库不会因为另一个终端 source 了脚本而改变。
7.3 第一次采集:一定先启动跟踪
终端 A 加载上述环境后:
ros2 trace --session-name demo-slow --list |
读提示并按 Enter 开始录制。--list 在这里展示启用事件清单,并非单纯查询后立即退出。Humble 的参数定义可查对应分支源码。入门命令不启用 kernel 事件;使用时仍以本机 --help 和启动清单为准。
终端 B 加载同样环境,再启动节点:
ros2 run diagnostic_demo diagnostic_node --ros-args -p mode:=slow |
录约 10 秒,在 B 按 Ctrl+C 停节点,再到 A 按 Enter 结束采集。记录 A 打印的实际路径;默认通常是 ~/.ros/tracing/demo-slow,也可能受 ROS_TRACE_DIR 或 ROS_HOME 影响。下一次换一个新 session 名。
babeltrace "$HOME/.ros/tracing/demo-slow" | less |
先确认存在 ros2:callback_start 和 ros2:callback_end,再确认初始化、节点、定时器与回调关联事件。只在节点已经运行之后才开始采集,可能错过初始化映射,出现“有时间戳却不知道对应哪个节点”的问题。官方采集与分析教程
不要照搬新版本的 ros2 trace start/stop/pause 工作流;先看 Humble 安装版本是否支持相应子命令。上面的交互方式足够完成第一次实验。
7.4 怎么读一条事件
真实格式会随版本变化,以下是手工简化的教学示意:
timestamp event vpid vtid callback |
时间戳是事件发生时间;vpid/vtid 区分进程和线程,callback 是进程内回调对象标识,不是 topic 名。字段是否出现取决于事件和采集 context。地址不能跨进程或跨次运行当成同一个对象使用。
这组数字可以计算:
- 第一次回调的墙钟持续时间:
10.0804 - 10.0000 = 80.4 ms。 - 两次开始之间的间隔:
10.1001 - 10.0000 = 100.1 ms。 - 回调结束后到下一次开始:
19.7 ms,但这不是“调度延迟”。
若没有记录预期就绪/截止时间,就不能凭两个回调边界算出 Executor 的排队延迟。若没有关联某条消息的发送与处理,也不能把这 80.4 ms 称为端到端延迟。
分析时必须按进程、线程、回调标识及事件顺序正确配对,不能把整个文件里相邻两行机械相减;嵌套或可重入回调尤其需要处理。检查开始/结束是否成对,有无 discarded/lost events 提示,采集是否在回调中间结束,再计算统计量。
7.5 打开分析 notebook,怎么看图
优先采用与 Humble 对应的 tracetools_analysis 分支及其示例 notebook,避免混用主分支 API:
cd ~/tracing_ws/src |
用安装了 ROS Python 包的解释器/kernel 运行 notebook;随手切到另一个虚拟环境,可能找不到 tracetools_analysis、babeltrace 或版本匹配的依赖。浏览器打开后,将 notebook 的 path 改成实际 trace 的绝对路径,按顺序执行单元格。Humble 分析示例目录
官方教程用订阅回调演示分析,Humble 的 callback_duration.ipynb 会遍历多种回调。为了聚焦本例的 timer,运行到 notebook 创建 data_util 的单元格后,新增一个单元格,先列出回调归属,再筛选本例的定时器:
matches = [] |
这段按 Humble 的 Ros2DataModelUtil 接口 编写;它适用于本例单节点、单定时器的采集,不应直接拿来合并多次运行或地址复用的复杂记录。想先按官方订阅回调示例学习,可用 demo_nodes_cpp 的 talker 和 listener 替代本例,同样先采集后启动两个节点,再按输出清单选择订阅回调。
阅读图表时先核对轴和单位,再看分布:
| 看什么 | 解释与下一步 |
|---|---|
| 每次调用耗时的时间曲线 | 是稳定偏慢,还是某段时间突然变慢? |
| p50、p95、p99、最大值及样本数 | 平均值可能掩盖尾部;少量样本的 p99 不稳定 |
| timer 的开始间隔 | 100 ms 的目标节奏是否稳定?是否出现长空档? |
| 与 CPU 采样的对应关系 | 墙钟 80 ms 且 CPU 很低,优先查等待、睡眠或抢占 |
先采 normal,再以同样窗口、环境、日志级别采 slow。本例 slow 模式的睡眠决定了回调墙钟时间应至少接近 80 ms,调度和其他操作还会增加时间;它并不需要消耗 80 ms CPU。100 ms 定时器仍可能表现为约 10 Hz,所以“频率看着正常”也不等于回调有充足余量。
跨进程消息时延还需要关联消息身份;跨机器则进一步需要时钟同步和误差分析。ROS 仿真时间、系统墙钟、LTTng 时钟域不能随意直接相减。
7.6 没有数据时按层排查
| 现象 | 检查顺序 |
|---|---|
ros2 trace 不存在 |
是否装了 ros2trace,当前 shell 是否加载正确环境 |
Tracing disabled |
是否检测到 LTTng,是否重编并加载启用后的 tracetools |
| enabled 但没有 ROS 事件 | 两端环境、节点是否重新启动、session 是否开始、事件是否启用 |
| 有事件但关联不出节点 | 是否漏了初始化事件;是否混了进程或不同运行的地址 |
| Python 节点信息不完整 | Humble 的核心分析主要围绕 rclcpp 埋点,不要假定 rclpy 完全相同 |
| 容器中 session / shared memory 异常 | 先在同一容器、同一用户运行 tracer 与节点,检查 session daemon 和 /dev/shm |
| 提示丢事件 | 缩小范围/缩短窗口,评估缓冲区和磁盘开销,重采后再做结论 |
Docker Desktop 的 Linux 内核属于虚拟机。容器用户态跟踪可用于理解 ROS 回调,但不能把它当成 macOS 宿主的完整调度轨迹;内核 tracing 是否可用要另行确认。无需为了入门用户态 tracing 直接安装 DKMS 或改宿主内核。
八、其余工具:什么时候接着用
8.1 strace 看系统调用
Ubuntu 可先安装 strace,然后对核对过的 PID 操作:
sudo apt install strace |
采样一小段后 Ctrl+C 结束跟踪并脱离目标。-f 跟踪线程/后续子进程,-tt 打印时间,-T 打印每次系统调用耗时。重点找反复失败的文件访问、长时间读写、连接失败和等待。futex 不是死锁证明;它也用于正常线程同步。strace 手册
8.2 perf 找 CPU 花在哪里
在有匹配内核 perf 工具和采样权限的 Linux 中:
TARGET_PID=1234 |
先看热点函数,再看调用路径。示例中睡眠 80 ms 不会自然成为 on-CPU 热点;perf 的 CPU 采样和 tracing 的墙钟持续时间回答的是不同问题。没有符号先补符号;权限报错先检查 perf_event_paranoid 与容器能力,不要把禁用所有限制当成固定步骤。Ubuntu 内核工具包、Docker VM 内核和发行版内核未必一一匹配。perf record 手册
8.3 Sanitizer 把内存错误提前暴露
对于自己的 CMake target,可在专用调试构建中同时加编译和链接选项,位置放在 add_executable() 之后。下面以示例 target 为名:
target_compile_options(diagnostic_node PRIVATE |
若采用 target_link_options,将项目最低 CMake 版本设为至少 3.13(Ubuntu 22.04 的系统 CMake 满足)。用独立目录构建并加载:
colcon build --build-base build-asan --install-base install-asan \ |
ASan 报告里先看错误类型、非法访问栈,再看分配/释放栈。它适合排查越界、use-after-free 等,运行开销使其不适合作为控制频率基准;本例 vector::at() 是主动检查并抛异常,不是一个验证 ASan 抓越界能力的实验。数据竞争另用 TSan,TSan 与 ASan 分开构建运行。AddressSanitizer 官方文档
8.4 rosbag 固定输入
ros2 bag record -o input_case /scan /tf /tf_static |
按你的故障更换 topic,录完后 Ctrl+C。离线重放时隔离真实输入发布者,并让目标节点使用 use_sim_time:=true;核查 /tf_static 等 transient-local 数据与录制/播放 QoS。bag 固定的是消息输入及其记录时间,无法重现原来的线程竞争、网络丢包和外设状态;没有录到的参数、配置及服务交互也不会自动补齐。rosbag2 的 QoS 配置
九、按这三次练习学下来
第一次:找准异常位置。 构建示例,跑 normal 建立基线,再用 catch throw 跑 crash。独立写下“哪个回调、第几次、哪个容器、下标是多少”,而不是只抄整个 backtrace。改正访问逻辑后重新构建、重跑相同输入。
第二次:识别慢回调。 采 normal 与 slow 两份 trace,分别检查事件完整性、回调标识和耗时。用时间差解释 80 ms,用 CPU 观察解释为什么“慢”不一定“忙”。再把 sleep 改成 150 ms,预测它超过 100 ms 周期后开始间隔会怎样变化,然后实测;不要假定错过的 timer 会无限补发。
第三次:迁移到自己的节点。 选择一个已知故障,先保存命令、参数、版本和输入,只启用能回答当前问题的工具。修改后比较同一条件下的错误次数、回调耗时分布或吞吐,补一个能防止复发的测试。
每次最终留下一张“证据卡”:现象 → 复现条件 → 最小证据 → 原因 → 修改 → 相同条件复测。例如“日志停了”只是现象;“线程 A 在互斥组内等 future,完成回调需要同组,重复复现且改为异步后恢复”才形成可审核的原因链。
继续阅读:GDB 实战、Linux 进程与系统调用排障、ROS 2 QoS、ROS 2 Control、Executor 与硬件接口。
坐标树断开、变换方向不对或出现 extrapolation 报错时,接着看 TF2 实战:坐标树、变换方向与时间排查。