RIOT 调度器空转基准测试sched_nop解析thread_yield 性能测量原理与实现【免费下载链接】RIOTRIOT - The friendly OS for IoT项目地址: https://gitcode.com/GitHub_Trending/riot/RIOT本篇文章围绕 RIOT 操作系统中的调度器基准测试应用tests/bench/sched_nop展开深入剖析它如何通过在一个无竞争线程的循环中反复调用thread_yield()测量出原始上下文保存/恢复性能 调度器空转判定耗时的合成指标并输出每秒thread_yield()调用次数作为结果。读者读完后将理解该基准的测量模型、thread_yield()在内核中的调用链、如何编译运行与解读输出以及它与其他tests/bench应用刻意重复代码的原因。基准测试的设计目标与测量模型tests/bench/sched_nop是 RIOT 仓库中用于量化调度器空转成本scheduler no-op cost的微基准测试。其核心思想非常直接正如 README 所述该测试在一个循环中调用thread_yield()。由于系统中不存在其他优先级更高或相同且就绪的线程因此每次调用测得的实际上是原始上下文保存/恢复性能加上调度器判断当前没有其他活动线程所需的短暂时间。换句话说这个基准刻意构造了一个yield 后没有任何可切换对象的最简场景从而把以下两部分开销从复杂的多线程切换中剥离出来上下文保存与恢复CPU 寄存器现场、栈指针等状态的保存与恢复调度器空转判定sched_run()在就绪队列中查找下一个线程发现目标线程仍是当前线程时跳过实际切换的开销。最终结果以每秒能够完成的thread_yield()调用次数calls per second呈现。这一指标可以直接反映目标平台调度路径的裸速度是评估内核调度开销、比较不同 CPU 架构如 Cortex-M 系列、RISC-V、native 模拟器调度实现效率的重要参考。值得说明的是这个应用有意与一些类似的 benchmark 应用重复代码README 原文This test application intentionally duplicates code with some similar benchmark applications其目的是便于直接比较各基准的代码大小code size。重复而非共享代码意味着每个基准都可以独立编译、独立统计自身 ROM 占用从而在不互相污染的情况下对比不同测量场景的开销差异。主程序实现定时驱动的 yield 计数循环核心实现位于 main.c完整逻辑只有约 60 行。其测量流程分为三个阶段。阶段一注册定时器回调xtimer_t timer; timer.callback _timer_callback; xtimer_set(timer, TEST_DURATION);程序首先使用xtimer设置一个一次性定时器时长由宏TEST_DURATION决定默认值为1000000U单位为微秒即默认测量窗口为1 秒#ifndef TEST_DURATION #define TEST_DURATION (1000000U) #endif该宏可在编译时通过CFLAGS覆盖例如将测量窗口缩短为 100ms 以加快验证。定时器到期后触发回调函数_timer_callback它所做的只是将一个全局 volatile 标志置位volatile unsigned _flag 0; static void _timer_callback(void *arg) { (void)arg; _flag 1; }_flag声明为volatile确保编译器不会将循环中对它的读取优化掉——这是嵌入式基准测试中防止编译器聪明过头消除空循环的标准手段。阶段二无竞争 yield 计数循环uint32_t n 0; xtimer_set(timer, TEST_DURATION); while (!_flag) { thread_yield(); n; }循环体的每一次迭代执行一次thread_yield()并递增计数器n直到定时器触发、_flag变为 1 才退出。由于该基准测试运行在 main 线程上下文中且系统中不存在优先级更高或相同、且处于就绪状态的线程因此每次thread_yield()都会经历完整的保存上下文 → 进入调度器 → 发现无需切换 → 恢复上下文路径但永远不会真正切换到其他线程——这正是空转nop的含义。阶段三结果输出退出循环后程序打印 JSON 格式的结果printf({ \result\ : %PRIu32, n); printf(, \ticks\ : %PRIu32, (uint32_t)((TEST_DURATION/US_PER_MS) * (coreclk()/KHZ(1)))/n); puts( });result测量窗口内完成的thread_yield()调用次数即每秒调用次数默认窗口为 1 秒ticks平均每次thread_yield()调用消耗的 CPU 时钟周期数。其计算方式为窗口时长换算为毫秒TEST_DURATION/US_PER_MS乘以 CPU 频率千赫兹为单位coreclk()/KHZ(1)再除以调用次数n。例如某平台coreclk()返回 64 MHz、默认 1 秒窗口内完成 100 万次调用则每次调用约耗 64 个时钟周期。ticks字段让结果不再受 CPU 主频影响便于跨平台对比调度路径的相对效率。PRIu32来自inttypes.h风格的格式化宏用于可移植地打印 32 位无符号整数。这里有两个值得注意的实现细节测量窗口的计时依赖xtimer而xtimer本身运行在定时器中断上下文中因此基准期间系统必须保持中断使能thread_yield()路径恰好不长期关闭中断见下文源码分析测量是有效的ticks字段是可选的测试脚本的正则允许其存在或缺失兼容不同固件版本。源码级的调用链thread_yield → sched_run要理解这个基准到底在量什么需要进入内核源码追踪thread_yield()的完整路径。thread_yield 的实现core/thread.c 中thread_yield()的实现如下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(); }其语义是短暂关闭中断将当前线程在其优先级对应的就绪队列中**轮转advance**到队尾然后恢复中断并调用thread_yield_higher()触发一次调度决策。sched_run发现无事可做的关键路径thread_yield_higher()最终会调用 core/sched.c 中的核心调度函数sched_run()。在无其他就绪线程的场景下sched_run()的关键行为如下thread_t *__attribute__((used)) sched_run(void) { thread_t *active_thread thread_get_active(); thread_t *previous_thread active_thread; /* ... */ sched_context_switch_request 0; unsigned nextrq _get_prio_queue_from_runqueue(); thread_t *next_thread container_of(sched_runqueues[nextrq].next-next, thread_t, rq_entry); /* ... */ next_thread-status STATUS_RUNNING; if (previous_thread next_thread) { /* 当前线程即下一个线程无需真正切换上下文 */ /* ... */ return active_thread; } /* 否则才执行真正的上下文切换context switch */ }从源码结构可以看出sched_run()通过_get_prio_queue_from_runqueue()从就绪队列位图runqueue bitcache中取出当前最高优先级队列的队首线程。在本基准场景下队列中只有 main 线程自己因此previous_thread next_thread成立调度器跳过真正的上下文切换直接返回当前线程。这正是 README 所述调度器意识到没有其他活动线程所需的短时间的源码级印证这一小段判定逻辑位图查询、队首取出、相等比较、状态置位就是基准要测量的调度器空转成本。而真正的上下文保存/恢复开销则由thread_yield()的完整调用路径进入调度器前的现场保护、退出调度器后的现场恢复贡献。此外core/include/sched.h 中的注释也明确了 RIOT 内核的调度触发模型sched_context_switch_request标志位一旦置位调度器会在此处或返回路径中执行上下文切换thread_yield()与thread_sleep()是两种典型的主动触发方式。sched_run()返回后架构相关代码依据sched_context_switch_request决定是否真正进行上下文切换——在 sched_nop 场景下该标志在sched_run()开头即被清零且后续没有其他就绪线程置位因此最终不切换。构建与运行从编译到解析输出Makefile 与依赖Makefile 内容极简include ../Makefile.bench_common USEMODULE xtimer include $(RIOTBASE)/Makefile.include引入 Makefile.bench_common后者设置了RIOTBASE指向仓库根目录并引入通用测试构建框架Makefile.tests_common显式声明USEMODULE xtimer因为基准依赖xtimer提供定时测量窗口其余模块线程、调度器、时钟等由 RIOT 的默认模块机制自动引入。内存受限板的豁免Makefile.ci 中声明了因内存不足而豁免的板卡BOARD_INSUFFICIENT_MEMORY : \ atmega8 \ #即atmega8由于 ROM/RAM 过小无法容纳该基准CI 不会在其上构建。其他 AVR、Cortex-M、RISC-V 及 native 平台均可运行。编译与烧录在任意支持的目标板上编译并运行以 native 模拟器为例最方便在 PC 上体验# 在 tests/bench/sched_nop 目录下 make BOARDnative flash term或将测量窗口缩短为 100ms 快速验证make BOARDnative CFLAGS-DTEST_DURATION100000U flash term也可以为真实板卡如nucleo-f401re、samr21-xpro指定BOARD交叉编译并烧录输出通过串口终端查看。输出解读程序启动先打印main starting测量结束后打印一行 JSON{ result : 1234567, ticks : 51 }result越大说明调度路径每秒能承受的 yield 次数越多调度空转开销越低ticks越小说明每次 yield 消耗的 CPU 周期越少。ticks是一个与主频无关的归一化指标跨平台对比时更有意义。自动化测试tests/01-run.py 使用 RIOT 的 testrunner 框架自动验证输出格式def testfunc(child): child.expect(r{ \result\ : \d(, \ticks\ : \d)? })该正则要求输出严格匹配{ result : 数字 }或带ticks字段的完整形式用于 CI 中自动校验基准应用能够正常完成测量并产生合法结果。定位与延伸sched_nop 在基准族中的角色sched_nop属于 tests/bench 目录下的微基准应用族。它刻意与同族应用重复代码正是为了在相同代码规模的前提下对比不同测量维度——例如专门测量上下文切换成本的基准如thread_switch类、测量线程创建/退出成本的基准等。通过sched_nop测得的空转调度成本可以与其他基准组合使用帮助开发者分解出调度开销中的各项组成调度器就绪队列查询与判定的固定开销上下文保存/恢复的架构相关开销两者之和在无竞争场景下的下限值。因此sched_nop的数值可视为该平台调度路径开销的下界参考任何真实的多线程切换场景其单次切换成本都不会低于这里的ticks值。小结tests/bench/sched_nop用约 60 行代码实现了一个精准的调度器空转微基准通过xtimer设定 1 秒测量窗口在一个无竞争线程的循环中反复调用thread_yield()并计数最终输出每秒调用次数与每调用时钟周期数。其测量模型、输出格式、Makefile 依赖与测试脚本均在仓库中有完整可验证的实现任何 RIOT 开发者都可以在数分钟内编译运行它得到本平台调度路径开销的第一手数据。【免费下载链接】RIOTRIOT - The friendly OS for IoT项目地址: https://gitcode.com/GitHub_Trending/riot/RIOT创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考