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);
}
}
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);
}
22.3 一致性测试:所有核看到的必须一样
一致性测试,是SMP测试里最容易被忽视、但也是最要命的。
为什么会这样?因为多核系统里,每个核都有自己的Cache。如果核A改了某个变量,核B读到的可能是旧值——这就是缓存一致性问题。
我建议的一致性测试方案:
- 共享变量一致性:核A写一个全局变量,核B读,验证是否读到最新值
- 原子操作一致性:用原子操作累加一个计数器,所有核同时操作,验证最终结果
- 内存屏障测试:验证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测试框架,我就讲这么多。记住一句话:测试不是浪费时间,是在帮你省钱。下一章,我们来聊聊SMP的调试技巧——那才是真正考验功力的地方。