10、调试与测试:SMP调试技巧、竞态条件检测、压力测试、性能剖析工具

多核调试,说实话比单核麻烦不少。单核时代你打个断点,整个世界都停了。到了SMP环境下,一个核停了,其他核还在疯跑。你想想看,这画面多混乱。

我个人习惯,先把调试环境搭稳了,再谈别的。今天这一讲,我把这些年踩过的坑、用过的工具,一股脑倒出来。希望能帮你少走弯路。

10.1 SMP调试技巧:别让断点坑了你

先说说最基础的——断点。在SMP上打断点,有个大坑:硬件断点资源是共享的

ARM Cortex-A系列通常只有6个硬件断点寄存器。你一个核用了4个,另一个核就只能用2个。我曾经在调试时,明明打了断点就是不触发,查了半天才发现是另一个核把断点寄存器占满了。

注意: 硬件断点资源是所有核共享的。调试多核时,建议每个核只保留1-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-Thread中,可以开启RT_DEBUG_SMP宏,它会自动检查一些常见的竞态条件。比如检测是否在中断中调用了可能引起调度的函数。

10.3 压力测试:把系统往死里整

压力测试的目的很简单:让系统在高负载下暴露问题。我一般分三步走:

  1. CPU压测:每个核跑一个死循环计算任务,看调度器是否稳定。
  2. 中断压测:用定时器以最高频率触发中断,看中断嵌套是否出问题。
  3. 混合压测:同时跑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%,内存使用量不持续增长,所有线程都能得到调度。

10.4 性能剖析工具:找到瓶颈在哪

性能剖析,说白了就是找出系统最慢的地方。多核环境下,瓶颈可能出现在:

  • 锁竞争:某个锁被频繁争用,导致其他核空转。
  • 缓存伪共享:两个核操作相邻的内存地址,导致缓存行频繁失效。
  • 中断负载不均:所有中断都涌向核0,其他核闲得发慌。

我常用的工具是perfftrace。在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系统的性能优化,到时候会讲一些更高级的调优技巧。