news 2026/9/19 12:24:07

RIOT 同优先级线程切换基准测试:thread_yield_pingpong 的原理、实现与运行

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
RIOT 同优先级线程切换基准测试:thread_yield_pingpong 的原理、实现与运行

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()互抛)来量化一次上下文切换的开销。读完本文,你将理解该基准的计数口径(为什么结果是"一半的上下文切换次数")、resultticks两个输出指标的含义、底层调度器实现原理,以及如何在一台真实板卡上编译、烧录、运行并验证结果。

基准测试的设计思路:同优先级双线程互抛

在实时操作系统中,上下文切换(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_pingpongmutex_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(); }

其执行路径分三步:

  1. 关闭中断irq_disable()):保证"取出当前线程并操作运行队列"这一临界区不被中断或抢占打断;
  2. 推进运行队列sched_runq_advance(me->priority)):把当前线程从其优先级的就绪队列头部移到尾部,即"让出"当前执行位置。me->status >= STATUS_ON_RUNQUEUE的检查确保线程确实在就绪队列上才做移动;
  3. 恢复中断并让位thread_yield_higher()):触发实际调度,选择运行队列头部的新线程执行,完成一次上下文切换。

从源码结构可以推断:在本基准的两个同优先级线程场景下,每次thread_yield()都会经历上述完整三步——关闭/恢复中断、就绪队列指针前移、真正切换到对方线程。因此ticks指标本质上反映的是在目标 CPU 上"关中断 + 就绪队列操作 + 线程切换 + 开中断"这一整条调度路径的开销,而不只是寄存器保存/恢复的裸切换成本。

与同族基准的横向对比:切换原语决定测量对象

thread_yield_pingpong属于 tests/bench 下的"pingpong 族"基准。对比同目录下的兄弟应用,可以更清楚地看出它在基准体系中的定位:

基准驱动切换的同步原语测量对象
thread_yield_pingpongthread_yield()纯协作式让位路径的切换开销
msg_pingpongmsg_send()/msg_receive()消息传递机制下的切换开销
mutex_pingpongmutex_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 强调的设计取舍——保证各基准除被测原语外其余条件完全一致,从而使代码体积与耗时指标的横向对比具有说服力

使用建议与注意事项

  1. 优先用 native 或主流 Cortex-M 板卡快速验证BOARD=native无需硬件即可观察输出格式与逻辑;需要真实时序数据时,选择不在BOARD_INSUFFICIENT_MEMORY列表中的板卡,例如nucleo-f401resamr21-xpronrf52840dk
  2. 调整测量窗口:默认 1 秒在慢速 MCU 上可能产生较小计数、放大抖动,可通过make CFLAGS+=-DTEST_DURATION=5000000之类的方式延长窗口以平滑结果;
  3. 解读指标时注意口径result是单线程视角的让位次数,实际上下文切换数为它的 2 倍;ticks是每次让位平均消耗的 CPU 周期,单次完整切换成本约为其 2 倍;
  4. 对比代码体积:可借助make info-buildsizesize工具分别统计本基准与 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 12:23:40

RV1126B星光全景视觉监测:破解输电线路夜间外破盲区

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 12:23:32

Sublime Text 4200 正确激活指南:官方流程、路径修复与常见误判排查

1. 项目概述&#xff1a;Sublime Text 激活注册这件事&#xff0c;到底在解决什么问题&#xff1f;Sublime Text 是一款被全球数十万开发者、写作者、系统管理员长期信赖的轻量级文本编辑器。它不是那种靠堆砌功能博眼球的“全能IDE”&#xff0c;而是像一把瑞士军刀——没有花…

作者头像 李华
网站建设 2026/9/19 12:23:04

双足机器鸭MicroDuck:50Hz神经控制闭环实现稳定步态

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 12:22:46

设备仿真中C#与Unity协同架构:通信协议与虚拟调试实战

设备仿真这件事&#xff0c;看着像是在Unity里拉个模型转一转&#xff0c;其实真正值钱的往往是C#这一侧&#xff0c;以及C#和Unity之间那根通信线。很多刚接触这块的工程师&#xff0c;要么是Unity搞了几天发现上位机逻辑写不进去&#xff0c;要么是C#老手一进Unity被GameObje…

作者头像 李华
网站建设 2026/9/19 12:22:16

ESP32蓝牙开发:从协议栈初始化到GATT通信全链路解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华