10、调试与测试:SMP调试技巧、竞态条件检测、压力测试、性能剖析工具
多核调试,说实话比单核麻烦不少。单核时代你打个断点,整个世界都停了。到了SMP环境下,一个核停了,其他核还在疯跑。你想想看,这画面多混乱。
我个人习惯,先把调试环境搭稳了,再谈别的。今天这一讲,我把这些年踩过的坑、用过的工具,一股脑倒出来。希望能帮你少走弯路。
10.1 SMP调试技巧:别让断点坑了你
先说说最基础的——断点。在SMP上打断点,有个大坑:硬件断点资源是共享的。
ARM Cortex-A系列通常只有6个硬件断点寄存器。你一个核用了4个,另一个核就只能用2个。我曾经在调试时,明明打了断点就是不触发,查了半天才发现是另一个核把断点寄存器占满了。
另一个技巧是核亲和性断点。GDB支持只让特定核停下:
(gdb) thread 1
(gdb) break scheduler.c:123 thread 1
(gdb) continue
这样只有核0会停在调度器那行代码上,其他核继续运行。嗯,这个用法我是在排查一个优先级反转问题时学会的,当时真是救了大命。
还有一招,非对称断点。比如你想看核1和核2谁先进入临界区,可以在入口处打两个断点,分别绑定到不同核。谁先触发,一目了然。
10.2 竞态条件检测:那些年我们追过的Bug
竞态条件,说白了就是两个核同时操作同一份数据,结果乱套了。这类Bug最难复现,也最难定位。
我推荐三种检测手段:
- 静态代码分析:用工具扫描全局变量,看哪些被多个核访问却没有加锁。RT-Thread的
smp_check.py脚本就能干这个活。 - 运行时检测:在临界区前后插入检查点,记录当前核ID。如果发现同一个变量被不同核连续访问,就报警。
- 锁状态监控:每个自旋锁加一个owner字段,记录当前持有者。死锁时直接打印出来。
我曾经遇到一个Bug:两个核同时调用rt_malloc,内存池的free list被搞坏了。查了三天,最后用锁状态监控才发现——有个地方忘记释放锁了。从那以后,我写代码必加锁状态检查。
RT_DEBUG_SMP宏,它会自动检查一些常见的竞态条件。比如检测是否在中断中调用了可能引起调度的函数。
10.3 压力测试:把系统往死里整
压力测试的目的很简单:让系统在高负载下暴露问题。我一般分三步走:
- CPU压测:每个核跑一个死循环计算任务,看调度器是否稳定。
- 中断压测:用定时器以最高频率触发中断,看中断嵌套是否出问题。
- 混合压测:同时跑CPU任务、中断、DMA传输,模拟真实场景。
这里给一个简单的压测脚本思路:
/* 每个核创建一个线程,疯狂申请释放内存 */
static void stress_test(void *parameter)
{
int cpu_id = rt_hw_cpu_id();
while (1) {
void *p = rt_malloc(rand() % 1024);
if (p) {
rt_free(p);
}
/* 加点计算,增加CPU压力 */
for (int i = 0; i < 1000; i++) {
__asm__ volatile("nop");
}
}
}
/* 主函数中为每个核创建线程 */
for (int i = 0; i < CONFIG_SMP_CORE_NUM; i++) {
rt_thread_t tid = rt_thread_create("stress",
stress_test, NULL,
2048, 10, 10);
if (tid) {
/* 绑定到指定核 */
rt_thread_control(tid, RT_THREAD_CTRL_BIND_CPU, (void*)i);
rt_thread_startup(tid);
}
}
跑个24小时,如果没死机、没内存泄漏,基本就稳了。我一般还会在压测期间用ps命令观察线程状态,看有没有线程卡在某个地方出不来。
10.4 性能剖析工具:找到瓶颈在哪
性能剖析,说白了就是找出系统最慢的地方。多核环境下,瓶颈可能出现在:
- 锁竞争:某个锁被频繁争用,导致其他核空转。
- 缓存伪共享:两个核操作相邻的内存地址,导致缓存行频繁失效。
- 中断负载不均:所有中断都涌向核0,其他核闲得发慌。
我常用的工具是perf和ftrace。在RT-Thread上,可以自己实现一个简单的采样剖析器:
/* 定时器中断中采样PC指针 */
void profiler_tick(void)
{
int cpu_id = rt_hw_cpu_id();
uint32_t pc;
__asm__ volatile("mov %0, pc" : "=r"(pc));
/* 记录到环形缓冲区 */
sample_buffer[cpu_id][sample_count[cpu_id]++] = pc;
}
跑一段时间后,把采样数据导出来,统计每个函数出现的次数。出现次数最多的,就是热点函数。
我记得有一次,发现rt_hw_spin_lock占了30%的CPU时间。一看代码,原来是某个驱动在中断里用了自旋锁,导致其他核频繁自旋等待。改成无锁队列后,性能直接翻倍。
- GDB + TUI模式:实时查看多核寄存器状态
- SystemView:SEGGER的图形化追踪工具,支持多核
- RT-Thread的
list_thread命令:快速查看各核线程运行情况
10.5 实战经验总结
调试SMP系统,我总结了三句话:
- 先隔离,再复现:把问题缩小到单个核或单个模块,再尝试复现。
- 日志为王:多打日志,尤其是进入和离开临界区的日志。我习惯用不同颜色区分不同核的输出。
- 不要相信直觉:多核下的Bug往往反直觉。你以为是指令执行顺序问题,结果可能是缓存一致性问题。
最后说一句:调试工具再好,也不如写代码时多想一步。加锁前想清楚:这个变量真的需要保护吗?用自旋锁还是互斥量?中断里能不能用?这些想明白了,调试时能省一半时间。
嗯,这一讲就到这里。下一讲我们聊聊SMP系统的性能优化,到时候会讲一些更高级的调优技巧。