RIOT 同优先级线程切换基准测试:thread_yield_pingpong 的原理、实现与运行
【免费下载链接】RIOTRIOT - The friendly OS for IoT项目地址: https://gitcode.com/GitHub_Trending/riot/RIOT
导读
本文围绕 RIOT 操作系统测试套件中的 thread_yield_pingpong 基准测试应用展开,剖析其如何通过两个同优先级线程互相让出 CPU(pingpong 式thread_yield()互抛)来量化一次上下文切换的开销。读完本文,你将理解该基准的计数口径(为什么结果是"一半的上下文切换次数")、result与ticks两个输出指标的含义、底层调度器实现原理,以及如何在一台真实板卡上编译、烧录、运行并验证结果。
基准测试的设计思路:同优先级双线程互抛
在实时操作系统中,上下文切换(context switch)开销是衡量内核调度效率的核心指标之一。RIOT 的thread_yield_pingpong采用了一种简洁而巧妙的测量方法,其核心思想记录在 README.md 中:
This test measures the amount of context switches between two threads of the same priority. The result amounts to the number of thread_yield() calls inonethread (half the number of actual context switches).
即:测量两个同优先级线程之间的上下文切换数量,其结果等于"一个"线程中thread_yield()的调用次数,也就是实际上下文切换次数的一半。
这一设计的关键在于"同优先级":
- 两个线程优先级相同,因此谁都不比谁高,
thread_yield()不会触发"让位给更高优先级线程"的抢占路径,而是通过调度器把当前线程移到同优先级运行队列的队尾,让出 CPU; - 每次
thread_yield()调用都会导致一次真实的线程切换,两个线程轮流让位,因此每个线程各自经历一半的切换,主线程记录的调用次数恰好等于总切换次数的一半。
README 还特别说明:该测试应用有意与其他类似的 benchmark 应用重复部分代码,以便在不同基准之间对比代码体积(code size)。这一点在 tests/bench 目录下可以得到印证——这里集中存放了 msg_pingpong、mutex_pingpong、thread_flags_pingpong 等结构高度相似的"pingpong"系列基准,它们共享相同的计时、计数与输出骨架,区别只在于驱动线程切换的同步原语(thread_yield、互斥锁、消息队列、线程标志等)。
源码逐段解读:main.c 的完整测量流程
基准的完整实现位于 main.c,全文仅约 80 行,包含四个核心部分。
1. 依赖与默认测量时长
#include "macros/units.h" #include "clk.h" #include "thread.h" #include "xtimer.h" #ifndef TEST_DURATION #define TEST_DURATION (1000000U) #endif- 依赖 RIOT 的线程抽象(
thread.h)、xtimer 定时器(xtimer.h)、时钟频率查询(clk.h)以及单位换算宏(macros/units.h); TEST_DURATION默认取1000000U,单位为微秒,即默认测量窗口为 1 秒。该宏允许通过编译期-DTEST_DURATION=...覆盖,用于延长或缩短采样窗口。
2. 全局状态与第二线程
volatile unsigned _flag = 0; static char _stack[THREAD_STACKSIZE_MAIN]; static void *_second_thread(void *arg) { (void)arg; while (1) { thread_yield(); } return NULL; }_flag是测量窗口的结束标志,由定时器回调置位,被主线程循环轮询;- 第二线程的栈直接复用
THREAD_STACKSIZE_MAIN大小(静态分配,避免动态内存分配带来的不确定性); - 第二线程进入死循环后只做一件事:无限调用
thread_yield()。主线程同样如此——两者在 1 秒内像打乒乓球一样互相让出 CPU,这正是"pingpong"名字的由来。
3. 主线程:创建对线程、设定窗口、循环计数
int main(void) { printf("main starting\n"); thread_create(_stack, sizeof(_stack), THREAD_PRIORITY_MAIN, 0, _second_thread, NULL, "second_thread"); xtimer_t timer; timer.callback = _timer_callback; uint32_t n = 0; xtimer_set(&timer, TEST_DURATION); while (!_flag) { thread_yield(); n++; } ... }值得注意的两个细节:
- 第二线程的优先级显式使用
THREAD_PRIORITY_MAIN,与主线程完全一致——这是整个基准成立的前提。若第二线程优先级不同,thread_yield()的调度行为就会改变(例如高优先级线程会让低优先级线程饿死),测量结果将失去"纯上下文切换开销"的意义; thread_create的 flags 参数为 0,即创建后不立即主动让出 CPU(对比 mutex_pingpong 中使用的THREAD_CREATE_WOUT_YIELD)。RIOT 默认在线程创建完成后调用thread_yield_higher(),因此主线程会先运行并启动定时器,随后在首次thread_yield()时切换到第二线程,测量随之开始;- 主线程在
xtimer_set(&timer, TEST_DURATION)之后进入循环,每调用一次thread_yield()就对计数器n加一。1 秒后_timer_callback将_flag置 1,循环退出,测量结束。
4. 结果输出:result 与 ticks
printf("{ \"result\" : %"PRIu32, n); printf(", \"ticks\" : %"PRIu32, (uint32_t)((TEST_DURATION/US_PER_MS) * (coreclk()/KHZ(1)))/n); puts(" }");输出是一行类 JSON 文本,包含两个指标:
| 字段 | 含义 | 计算方式 |
|---|---|---|
result | 测量窗口内主线程的thread_yield()调用次数 | 直接计数n |
ticks | 平均每次thread_yield()调用消耗的 CPU 周期数(tick) | (TEST_DURATION/US_PER_MS) * (coreclk()/KHZ(1)) / n |
对ticks公式做单位拆解:
TEST_DURATION/US_PER_MS:将微秒换算为毫秒(默认 1 秒 → 1000 ms);coreclk()/KHZ(1):将coreclk()返回的 CPU 主频(Hz)换算为 kHz 数值;- 二者相乘得到测量窗口内理论上的总 tick 数,再除以
n,即为每次thread_yield()调用平均消耗的 CPU 周期数。
因此,若想估算单次完整上下文切换的开销,应将ticks乘以 2(因为一次实际切换对应两次线程让位,每次让位只算半个切换),这与 README 中"result 是实际切换次数的一半"的说明自洽。
测试运行与自动化验证
构建与运行
该基准是一个标准 RIOT 测试应用,其 Makefile 极其精简:
include ../Makefile.bench_common USEMODULE += xtimer include $(RIOTBASE)/Makefile.include其中 Makefile.bench_common 负责定位RIOTBASE并引入公共测试构建规则;USEMODULE += xtimer显式启用定时器模块(msg_pingpong、mutex_pingpong等兄弟基准同样依赖它)。
在任意受支持板卡上,例如使用 native 或某块 Cortex-M 板:
cd tests/bench/thread_yield_pingpong make BOARD=native -j make BOARD=native term程序启动后首先打印main starting,约 1 秒后输出测量结果,例如:
main starting { "result" : 1234567, "ticks" : 48 }(具体数值取决于 CPU 主频与调度器实现。)
自动化回归:tests/01-run.py
与 RIOT 其余测试一致,该基准配有 tests/01-run.py,基于testrunner框架做自动回归:
def testfunc(child): child.expect(r"{ \"result\" : \d+(, \"ticks\" : \d+)? }")它只校验输出格式是否符合{ "result" : <数字>(, "ticks" : <数字>)? }的正则——注意ticks部分是可选的,说明该字段在部分平台上可能被省略或打印为空,自动化测试对两种输出形态都兼容。运行方式:
make BOARD=... flash test板卡适用性限制
Makefile.ci 声明了内存不足以运行本基准的板卡:
BOARD_INSUFFICIENT_MEMORY := \ atmega8 \ nucleo-l011k4 \ stm32f030f4-demo \ #这是因为本测试同时需要静态线程栈(THREAD_STACKSIZE_MAIN)、xtimer 模块以及 printf 输出,对 RAM/Flash 较小的 AVR 或入门级 STM32 板卡而言空间紧张。CI 会据此跳过这些板卡。
底层原理:thread_yield() 在调度器中的实现
要真正理解这个基准测的是什么,需要下沉到内核。thread_yield()的实现位于 core/thread.c:
void thread_yield(void) { unsigned old_state = irq_disable(); thread_t *me = thread_get_active(); if (me->status >= STATUS_ON_RUNQUEUE) { sched_runq_advance(me->priority); } irq_restore(old_state); thread_yield_higher(); }其执行路径分三步:
- 关闭中断(
irq_disable()):保证"取出当前线程并操作运行队列"这一临界区不被中断或抢占打断; - 推进运行队列(
sched_runq_advance(me->priority)):把当前线程从其优先级的就绪队列头部移到尾部,即"让出"当前执行位置。me->status >= STATUS_ON_RUNQUEUE的检查确保线程确实在就绪队列上才做移动; - 恢复中断并让位(
thread_yield_higher()):触发实际调度,选择运行队列头部的新线程执行,完成一次上下文切换。
从源码结构可以推断:在本基准的两个同优先级线程场景下,每次thread_yield()都会经历上述完整三步——关闭/恢复中断、就绪队列指针前移、真正切换到对方线程。因此ticks指标本质上反映的是在目标 CPU 上"关中断 + 就绪队列操作 + 线程切换 + 开中断"这一整条调度路径的开销,而不只是寄存器保存/恢复的裸切换成本。
与同族基准的横向对比:切换原语决定测量对象
thread_yield_pingpong属于 tests/bench 下的"pingpong 族"基准。对比同目录下的兄弟应用,可以更清楚地看出它在基准体系中的定位:
| 基准 | 驱动切换的同步原语 | 测量对象 |
|---|---|---|
thread_yield_pingpong | thread_yield() | 纯协作式让位路径的切换开销 |
| msg_pingpong | msg_send()/msg_receive() | 消息传递机制下的切换开销 |
| mutex_pingpong | mutex_lock()/mutex_unlock() | 互斥锁争用下的切换开销 |
thread_flags_pingpong | 线程标志(thread flags) | 标志唤醒机制下的切换开销 |
从代码上看,四个应用共享几乎相同的骨架:_flag结束标志 + xtimer 定时窗口 +uint32_t n计数 + 相同的result/ticks输出格式。以 mutex_pingpong 为例,其区别仅在于:第二线程循环mutex_lock(&_mutex),主线程循环mutex_unlock(&_mutex)并计数,且线程创建时使用THREAD_CREATE_WOUT_YIELD以避免提前切换。这种"有意重复代码"正是 README 强调的设计取舍——保证各基准除被测原语外其余条件完全一致,从而使代码体积与耗时指标的横向对比具有说服力。
使用建议与注意事项
- 优先用 native 或主流 Cortex-M 板卡快速验证:
BOARD=native无需硬件即可观察输出格式与逻辑;需要真实时序数据时,选择不在BOARD_INSUFFICIENT_MEMORY列表中的板卡,例如nucleo-f401re、samr21-xpro或nrf52840dk; - 调整测量窗口:默认 1 秒在慢速 MCU 上可能产生较小计数、放大抖动,可通过
make CFLAGS+=-DTEST_DURATION=5000000之类的方式延长窗口以平滑结果; - 解读指标时注意口径:
result是单线程视角的让位次数,实际上下文切换数为它的 2 倍;ticks是每次让位平均消耗的 CPU 周期,单次完整切换成本约为其 2 倍; - 对比代码体积:可借助
make info-buildsize或size工具分别统计本基准与 mutex_pingpong、msg_pingpong 的镜像大小,验证 README 所述"重复代码以便对比 code size"的设计意图——这也是本基准区别于一般功能测试、被归类到tests/bench的原因。
小结
thread_yield_pingpong是 RIOT 内核调度性能测量体系中最小巧、最直接的一个基准:用两个同优先级线程互抛thread_yield(),在固定时间窗口内统计让位次数,并换算成每次让位平均消耗的 CPU 周期数。它验证了 RIOT 调度器在"同优先级轮转 + 协作式让位"这一核心路径上的真实开销,其"结果等于一半上下文切换次数"的口径、result/ticks输出格式、xtimer 计时骨架以及有意的代码重复策略,共同构成了 RIOT 基准测试家族的标准范式,可直接作为评估新硬件平台或内核改动对调度性能影响的参考工具。
【免费下载链接】RIOTRIOT - The friendly OS for IoT项目地址: https://gitcode.com/GitHub_Trending/riot/RIOT
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考