22、SMP测试框架:单元测试、压力测试、一致性测试、长稳测试

说到SMP的测试,我估计很多同学第一反应是:「能跑起来不就行了?」

嗯,我以前也这么想。直到有一次,一个双核系统跑了三天三夜,突然在凌晨三点挂了。从那以后,我对SMP测试的态度就彻底变了——没有经过系统性测试的SMP,本质上就是个定时炸弹

22.1 单元测试:把每个锁都敲一遍

单元测试,说白了就是验证每个「小零件」是否正常工作。在SMP里,这些小零件包括:自旋锁、互斥量、信号量、消息队列、调度器入口等。

我个人习惯把SMP的单元测试分成三类:

  • 锁原语测试:验证spin_lock/spin_unlock是否真的互斥
  • 调度器测试:验证smp_schedule是否能在多核间正确切换
  • IPC测试:验证核间通信是否丢数据、乱序

举个例子,自旋锁的单元测试可以这样写:

/* 自旋锁单元测试 - 验证多核互斥 */
static rt_spinlock_t test_lock;
static volatile int shared_counter = 0;

static void test_spinlock_thread_entry(void *param)
{
    int cpu_id = rt_hw_cpu_id();
    int i;
    
    for (i = 0; i < 10000; i++) {
        rt_spin_lock(&test_lock);
        shared_counter++;
        rt_spin_unlock(&test_lock);
    }
    
    rt_kprintf("CPU%d done, counter=%d\n", cpu_id, shared_counter);
}

void test_spinlock_smp(void)
{
    rt_spin_lock_init(&test_lock);
    shared_counter = 0;
    
    /* 在两个核上同时跑 */
    for (int i = 0; i < 2; i++) {
        rt_thread_t tid = rt_thread_create("tst", 
            test_spinlock_thread_entry, NULL, 1024, 20, 10);
        rt_thread_startup(tid);
    }
}
我的经验:单元测试跑完后,shared_counter 必须等于 20000。如果少了,说明锁没锁住。我曾经在一个国产芯片上遇到过自旋锁实现有bug,就是靠这个测试抓出来的。

22.2 压力测试:把系统往死里怼

单元测试通过,只能说明「功能对」。但SMP系统最怕的是「偶尔不对」——也就是竞态条件。

压力测试的目的,就是用高并发、高频率的操作,把隐藏的竞态问题逼出来

我常用的压力测试场景:

  • 高频创建/销毁线程:两个核同时创建和销毁线程,看调度器会不会崩
  • 密集IPC通信:核A发消息给核B,核B回消息给核A,循环百万次
  • 中断+任务混合:一个核跑中断处理,另一个核跑任务调度,看会不会死锁

这里有个经典的「压力测试模板」:

/* 压力测试:多核同时创建线程 */
#define THREAD_COUNT 1000

static void stress_create_thread(void)
{
    int cpu_id = rt_hw_cpu_id();
    rt_thread_t tids[THREAD_COUNT];
    int created = 0;
    
    for (int i = 0; i < THREAD_COUNT; i++) {
        tids[i] = rt_thread_create("stress", 
            stress_entry, NULL, 512, 20, 10);
        if (tids[i] != RT_NULL) {
            rt_thread_startup(tids[i]);
            created++;
        }
    }
    
    rt_kprintf("CPU%d created %d threads\n", cpu_id, created);
}
注意:压力测试一定要跑在真实硬件上,别在QEMU上糊弄自己。QEMU的时序和真实芯片差太远,很多竞态条件根本复现不出来。

22.3 一致性测试:所有核看到的必须一样

一致性测试,是SMP测试里最容易被忽视、但也是最要命的。

为什么会这样?因为多核系统里,每个核都有自己的Cache。如果核A改了某个变量,核B读到的可能是旧值——这就是缓存一致性问题。

我建议的一致性测试方案:

  1. 共享变量一致性:核A写一个全局变量,核B读,验证是否读到最新值
  2. 原子操作一致性:用原子操作累加一个计数器,所有核同时操作,验证最终结果
  3. 内存屏障测试:验证smp_mb()是否真的能保证顺序

来看一个原子操作一致性的测试:

/* 原子操作一致性测试 */
static rt_atomic_t atomic_counter = 0;

static void test_atomic_consistency(void *param)
{
    int cpu_id = rt_hw_cpu_id();
    
    for (int i = 0; i < 100000; i++) {
        rt_atomic_add(&atomic_counter, 1);
    }
    
    rt_kprintf("CPU%d finished, atomic=%d\n", 
        cpu_id, rt_atomic_get(&atomic_counter));
}

/* 期望结果:atomic_counter == 核数 * 100000 */

核心要点:如果原子操作测试失败,别急着怀疑代码。先检查芯片的缓存一致性协议是否正常工作。我遇到过一款RISC-V芯片,它的L2 Cache一致性实现有硬件bug,折腾了我整整两周。

22.4 长稳测试:跑它个七天七夜

长稳测试,说白了就是「让系统一直跑,看它什么时候挂」。

我个人习惯把长稳测试分成三个阶段:

阶段 时长 测试内容
短期 24小时 基本功能循环,检查内存泄漏
中期 72小时 混合负载,检查调度器稳定性
长期 168小时(7天) 极限负载,检查硬件可靠性

长稳测试的关键指标:

  • 内存使用率:不能持续增长,否则就是内存泄漏
  • 调度延迟:不能出现偶发的大延迟(超过100ms就算异常)
  • 核间负载:各核的CPU占用率应该相对均衡
  • 系统日志:不能出现任何assert失败或panic

我曾经在一个项目里,长稳测试跑到第5天,系统突然打印了一行「scheduler: CPU0 stuck!」。查了三天才发现,是某个驱动在中断里调用了rt_thread_mdelay(),导致调度器死锁。嗯,这种问题,短时间测试根本发现不了。

22.5 测试框架的自动化集成

最后,我建议把这些测试集成到一个自动化框架里。RT-Thread的smp_test.c就是一个很好的参考:

/* SMP测试框架入口 */
void smp_test_run_all(void)
{
    rt_kprintf("=== SMP Test Suite ===\n");
    
    /* 1. 单元测试 */
    test_spinlock_smp();
    test_mutex_smp();
    test_semaphore_smp();
    
    /* 2. 压力测试 */
    test_stress_thread_create();
    test_stress_ipc_heavy();
    
    /* 3. 一致性测试 */
    test_cache_coherency();
    test_atomic_consistency();
    test_memory_barrier();
    
    /* 4. 长稳测试(需要单独启动) */
    rt_kprintf("Run 'smp_stress_longterm' for 7-day test\n");
}
我的建议:每次修改SMP相关代码后,至少跑一遍单元测试和压力测试。长稳测试可以放在CI里,每周自动跑一次。别等到产品上线了才发现问题——那时候的代价,你懂的。

好了,关于SMP测试框架,我就讲这么多。记住一句话:测试不是浪费时间,是在帮你省钱。下一章,我们来聊聊SMP的调试技巧——那才是真正考验功力的地方。