第4章:性能基准测试方法论
性能基准测试,说白了就是给RTOS做一次全面体检。我做了这么多年嵌入式,见过太多项目在选型时拍脑袋,结果上线后才发现性能不达标,那叫一个痛苦。
这一章,咱们就聊聊怎么科学地给NuttX和Zephyr做性能测试。我会把测试指标、测试工具、还有我踩过的坑,都掰开揉碎了讲清楚。
4.1 核心测试指标
我个人习惯把RTOS性能指标分成四大类。你想想看,一个实时系统最怕什么?怕任务切换太慢、怕中断响应不及时、怕内存不够用、怕时间抖动太大。嗯,就是这四样。
4.1.1 上下文切换时间
上下文切换,就是CPU从一个任务切到另一个任务的过程。这个时间越短越好。
我在项目中遇到过一个问题:一个工业控制设备,要求任务切换时间不超过5微秒。当时用的某款RTOS,测出来平均6.5微秒,差点翻车。
测试方法其实不复杂:
- 创建两个同等优先级的任务
- 任务A通过信号量唤醒任务B
- 任务B再唤醒任务A
- 用高精度定时器记录来回切换的时间
- 除以2就是单次上下文切换时间
关键点:测试时要关掉所有中断,否则测出来的时间会被中断处理干扰。我曾经因为这个原因,测出来的数据忽高忽低,排查了整整两天。
4.1.2 中断延迟
中断延迟,就是从硬件中断触发到中断服务程序开始执行的时间。这个指标对实时性至关重要。
为什么会这样?因为中断是RTOS响应外部事件的最高优先级手段。如果中断延迟太大,系统可能错过关键事件。
测试中断延迟,我推荐用GPIO引脚法:
// 测试中断延迟的典型方法
1. 外部信号触发GPIO中断
2. 中断服务程序入口立即翻转另一个GPIO
3. 用示波器测量两个GPIO信号的时间差
4. 这个时间差就是中断延迟
避坑指南:我曾经在测试Zephyr的中断延迟时,发现数据异常大。后来排查发现,是中断嵌套导致的。NuttX和Zephyr对中断嵌套的处理方式不同,测试时一定要明确是否允许嵌套。
4.1.3 内存占用
内存占用,说白了就是RTOS内核本身吃了多少RAM和Flash。对于资源受限的MCU,这个指标直接决定了你能不能跑起来。
我习惯分三块来看:
- 内核代码大小:编译后.text段的大小
- 内核数据大小:.data + .bss段的大小
- 运行时开销:任务控制块、信号量、队列等内核对象占用的内存
| 内存类型 | NuttX(典型值) | Zephyr(典型值) |
|---|---|---|
| 内核代码(Flash) | 30-50 KB | 40-70 KB |
| 内核数据(RAM) | 8-15 KB | 10-20 KB |
| 每个任务控制块 | 约 200 字节 | 约 300 字节 |
我的经验:这些数值会因配置选项差异很大。比如Zephyr如果开启所有功能,内核代码能到100KB以上。所以测试时一定要基于你的实际配置,别拿最小配置的数据去估算最大配置的场景。
4.1.4 任务切换抖动
任务切换抖动,就是每次切换时间的波动范围。这个指标很多人忽略,但它恰恰是衡量RTOS实时性的关键。
你想想看,如果一个系统平均切换时间1微秒,但最大抖动达到10微秒,那这个系统就不适合做高精度控制。
测试方法:
- 连续测量1000次上下文切换时间
- 记录最大值、最小值、平均值
- 计算标准差和最大偏差
- 抖动 = 最大值 - 最小值
4.2 测试工具
工欲善其事,必先利其器。我常用的测试工具有三个:Lmbench、RhealStone,还有自己写的测试用例。
4.2.1 Lmbench
Lmbench是一套经典的性能测试工具集,原本是Linux下的,但可以移植到RTOS上。
它主要能测:
- 上下文切换时间
- 信号量操作时间
- 内存带宽
- 系统调用开销
移植Lmbench到NuttX或Zephyr需要做些适配工作。我记得第一次移植时,光是改编译脚本就花了两天。不过一旦跑起来,数据非常可靠。
4.2.2 RhealStone
RhealStone是专门为RTOS设计的基准测试。它模拟了实时系统的典型工作负载。
它的测试模型包括:
- 周期性任务
- 中断处理
- 任务间通信
- 资源同步
注意:RhealStone的测试结果是一个综合评分,不是单个指标。我个人习惯用它做横向对比,比如NuttX和Zephyr在相同硬件上跑RhealStone,看谁的分数高。
4.2.3 自定义测试用例
说实话,标准工具只能测通用场景。真正要摸清RTOS的底细,还得自己写测试用例。
我一般会写这几类:
- 压力测试:创建100个任务,看系统会不会崩溃
- 边界测试:信号量超时时间为0、任务优先级相同等情况
- 长时间运行测试:连续跑72小时,看有没有内存泄漏
- 混合负载测试:同时处理中断、任务切换、消息队列
// 一个简单的自定义测试框架示例
void test_context_switch(void)
{
uint32_t start, end;
int i;
// 预热,让缓存热起来
for (i = 0; i < 10; i++) {
trigger_switch();
}
// 正式测试
start = get_cycle_count();
for (i = 0; i < 1000; i++) {
trigger_switch();
}
end = get_cycle_count();
// 计算平均时间
avg_time = (end - start) / 1000;
printf("平均上下文切换时间: %d cycles\n", avg_time);
}
我的建议:自定义测试用例一定要考虑缓存的影响。第一次运行和第二次运行的结果可能差很多。我习惯先跑几轮预热,再取后面的数据。
4.3 测试注意事项
最后,分享几个我踩过的坑:
- 硬件一致性:NuttX和Zephyr必须在同一块板子上测试,否则数据没有可比性
- 编译器版本:不同编译器优化等级会影响结果,统一用-O2
- 时钟精度:确保使用高精度定时器,别用系统滴答定时器
- 测试环境:关掉调试输出、日志打印等干扰因素
我曾经在测试NuttX时,忘了关掉串口打印,结果上下文切换时间多了30%。排查了半天才发现是printf在作怪。嗯,这种低级错误,犯一次就够了。
好了,这一章的内容就是这些。下一章咱们会实际跑一遍这些测试,看看NuttX和Zephyr到底谁更胜一筹。