RIOT OS Mutex Ping-Pong 基准测试:用互斥锁握手测量上下文切换开销

发布时间:2026/9/19 1:21:38
RIOT OS Mutex Ping-Pong 基准测试:用互斥锁握手测量上下文切换开销 RIOT OS Mutex Ping-Pong 基准测试用互斥锁握手测量上下文切换开销【免费下载链接】RIOTRIOT - The friendly OS for IoT项目地址: https://gitcode.com/GitHub_Trending/riot/RIOT导读本文以 RIOT 仓库中 tests/bench/mutex_pingpong 基准测试应用为核心深入讲解 RIOT 操作系统如何利用两个线程之间反复的 mutex lock/unlock 握手在 1 秒内统计出 unlock 次数并据此量化每次上下文切换的时钟开销。读完本文你将掌握该基准测试的完整实现原理、编译运行方法、输出解析方式以及它与thread_yield_pingpong、msg_pingpong等姊妹基准在代码结构与测量语义上的差异并了解core/mutex.c底层同步实现如何影响测量结果。测试设计为什么是 Ping-Pong 而不是自旋计数在 RIOT 的tests/bench/目录下mutex_pingpong属于一组专门用于测量内核调度与同步原语开销的基准测试家族。其设计思想在 tests/bench/mutex_pingpong/README.md 中有明确描述一个线程反复地对同一个互斥锁执行mutex_lock另一个线程反复地对它执行mutex_unlock最终结果是1 秒时间窗内完成的 unlock 次数由于每一次 unlock 都会唤醒并切换到等待中的锁持有线程该次数恰好等于期间发生的上下文切换次数的一半。之所以采用两个线程来回交接锁的方式而不是在一个线程内自旋计数是因为要测量的恰恰是 mutex 阻塞与唤醒所牵涉的跨线程调度开销锁的交接意味着当前运行线程被挂起、等待线程被置为可运行并切换出去这是 RIOT 调度器core/sched.c完整工作流的一次体现。该 README 还特意说明这个测试应用有意与其他相似基准测试重复代码目的是为了能够公平地比较各基准应用的代码体积code sizes。这解释了为什么mutex_pingpong与thread_yield_pingpong、msg_pingpong的框架代码几乎同构——它们在结构上刻意保持一致差异只在每次循环中调用的同步原语本身。核心实现逐行解析完整的测试逻辑位于 tests/bench/mutex_pingpong/main.c约 85 行结构非常紧凑。下面按功能模块拆解。全局状态与可配置参数#ifndef TEST_DURATION #define TEST_DURATION (1000000U) #endif volatile unsigned _flag 0; static char _stack[THREAD_STACKSIZE_MAIN]; static mutex_t _mutex MUTEX_INIT;TEST_DURATION以微秒为单位默认1000000U1 秒。它可以在编译期通过CFLAGS覆盖例如传入-DTEST_DURATION(500000U)即可把测量窗口缩短为 0.5 秒。_flag是测量终止标志由 xtimer 定时器回调置位volatile防止编译器优化掉轮询循环。_stack为第二线程提供栈空间直接复用主线程的栈大小常量THREAD_STACKSIZE_MAIN。_mutex使用 RIOT 提供的静态初始化宏MUTEX_INIT。从 core/include/mutex.h 可以看到该宏展开为{ .queue { .next NULL } }表示互斥锁处于未锁定状态。定时回调与第二线程static void _timer_callback(void*arg) { (void)arg; _flag 1; } static void *_second_thread(void *arg) { (void)arg; while (1) { mutex_lock(_mutex); } return NULL; }第二线程的工作极其简单无限循环地mutex_lock。由于主线程在循环中会反复占有并释放锁第二线程的mutex_lock绝大多数情况下都会立即成功不会产生阻塞——它扮演的是接住锁的角色让主线程的下一次mutex_unlock总有等待者可以唤醒。线程创建与初始握手thread_create(_stack, sizeof(_stack), THREAD_PRIORITY_MAIN - 1, THREAD_CREATE_WOUT_YIELD, _second_thread, NULL, second_thread); /* lock the mutex, then yield to second_thread */ mutex_lock(_mutex); thread_yield_higher();这里有两个关键细节第二线程的优先级被设置为THREAD_PRIORITY_MAIN - 1即高于主线程。在 RIOT 中数字越小优先级越高因此第二线程一旦可运行就会抢占主线程。创建线程时使用THREAD_CREATE_WOUT_YIELD标志不立即让出 CPU保证后续流程在主线程中按顺序执行。主线程先mutex_lock占住锁再调用thread_yield_higher()主动让位给更高优先级的第二线程。于是第二线程进入mutex_lock时发现锁已被占用随即进入阻塞队列STATUS_MUTEX_BLOCKED见 core/mutex.c等待主线程解锁。初始握手完成测量循环的乒乓状态就绪。测量主循环xtimer_t timer; timer.callback _timer_callback; uint32_t n 0; xtimer_set(timer, TEST_DURATION); while (!_flag) { mutex_unlock(_mutex); n; }主循环在 1 秒内无限执行mutex_unlock并累加计数。其执行节奏是主线程mutex_unlock唤醒阻塞中的第二线程将其置为STATUS_PENDING并调用thread_yield_higher()让出 CPU见 core/mutex.c第二线程被调度运行mutex_lock成功取得锁第二线程随即再次mutex_lock发现锁被自己持有于是阻塞等待调度器切回主线程主线程再次mutex_unlock……如此往返每一次循环对应两次上下文切换主线程→第二线程、第二线程→主线程因此最终计数n除以 2 就是上下文切换次数。当xtimer在TEST_DURATION之后触发_timer_callback将_flag置 1 时循环退出测量结束。结果输出printf({ \result\ : %PRIu32, n); printf(, \ticks\ : %PRIu32, (uint32_t)((TEST_DURATION/US_PER_MS) * (coreclk()/KHZ(1)))/n); puts( });输出为一行 JSON 风格的文本包含两个字段result1 秒内的 unlock 次数即每秒钟完成的锁交接次数ticks平均每次 unlock 消耗的 CPU 时钟周期数。ticks的计算方式为(TEST_DURATION / US_PER_MS) * (coreclk() / KHZ(1)) / nTEST_DURATION/US_PER_MS把测量窗口换算为毫秒coreclk()/KHZ(1)得到以 kHz 为单位的 CPU 核心时钟频率coreclk()声明于 core/include/clk.h两者相乘即测量窗口内的总时钟周期数再除以n即为单次 lock/unlock 握手的平均时钟开销。由于两次上下文切换对应一次 unlock单次上下文切换的时钟成本约为ticks / 2。编译、烧录与运行该基准测试是一个标准的 RIOT 测试应用编译方式与普通 RIOT 应用一致# 在 tests/bench/mutex_pingpong 目录下针对目标板编译 make BOARDnative # 在 native 平台上直接运行 make BOARDnative term # 在真实硬件上烧录并运行 make BOARDyour-board flash termnative平台cpu/native是快速验证的首选它把 RIOT 编译为宿主机程序无需真实硬件即可观察输出。以 native 为例运行后终端会打印类似如下的结果main starting { result : 1234567, ticks : 89 }其中result和ticks的具体数值会随宿主平台、编译器优化级别以及 RIOT 配置如是否启用优先级继承、mutex 调试等而变化。需要说明的是native平台测量的是宿主机上的仿真调度开销与真实 MCU 的绝对数值差异很大要评估真实硬件上的上下文切换成本应针对具体开发板烧录运行。Makefile 结构非常精简tests/bench/mutex_pingpong/Makefileinclude ../Makefile.bench_common USEMODULE xtimer include $(RIOTBASE)/Makefile.includeMakefile.bench_commontests/bench/Makefile.bench_common负责引入公共的测试构建基础设施Makefile.tests_commonUSEMODULE xtimer声明依赖 xtimer 定时器模块测量窗口由xtimer_set驱动。测试脚本与自动化验证仓库还提供了自动化测试脚本 tests/bench/mutex_pingpong/tests/01-run.py它通过 RIOT 的 testrunner 框架断言输出格式def testfunc(child): child.expect(r{ \result\ : \d(, \ticks\ : \d)? })正则表达式要求输出匹配{ result : 数字 }或带ticks字段的完整形式任何格式异常都会导致测试失败。这意味着该基准不仅可手动运行也可接入 CI 或make test流程自动校验。内存受限板卡的豁免清单tests/bench/mutex_pingpong/Makefile.ci 列出了BOARD_INSUFFICIENT_MEMORY即因 Flash/RAM 过小而不参与 CI 构建的板卡atmega8 nucleo-l011k4 stm32f030f4-demo从源码结构看该清单的存在与 README 中比较代码体积的意图相呼应——这类基准虽小但仍需容纳一个完整内核与 xtimer 模块极小内存的 8 位/入门级 MCU 无法满足。底层原理mutex 阻塞与唤醒路径要真正理解测量结果需要知道一次mutex_unlock→mutex_lock交接在 core/mutex.c 内部经历了什么。当第二线程在主线程持有锁时调用mutex_lock会进入mutex_lock_internalcore/mutex.c它先关闭中断irq_disable检查到mutex-queue.next ! NULL锁已被占用且需要阻塞时调用内部函数_block。_block将当前线程状态设为STATUS_MUTEX_BLOCKED、把该线程的rq_entry挂入 mutex 的等待队列恢复中断后调用thread_yield_higher()主动让出 CPUcore/mutex.c。当主线程调用mutex_unlock时core/mutex.c关闭中断检查锁状态若等待队列非空用list_remove_head取出队首等待线程通过sched_set_status(process, STATUS_PENDING)把等待线程重新置为可运行恢复中断并调用thread_yield_higher()让调度器决定是否切换。从这段实现可以看到一次锁交接至少包含两次thread_yield_higher()调用和两次调度器调度这正是基准测试希望计量的核心开销。此外如果启用了MODULE_CORE_MUTEX_PRIORITY_INHERITANCE优先级继承或MODULE_CORE_MUTEX_DEBUGmutex 调试模块_block与mutex_unlock中会额外执行优先级调整或调试信息打印core/mutex.c、core/mutex.c这些都会增大单次交接的ticks值——因此做横向对比时应保持内核模块配置一致。与姊妹基准测试的对比tests/bench/目录下还有若干结构高度相似的基准可通过对照理解各自的测量语义基准目录循环内核心操作测量内容tests/bench/mutex_pingpongmutex_unlock每秒锁交接次数 ≈ 上下文切换次数的一半tests/bench/thread_yield_pingpongthread_yield每秒主动让出 CPU 的次数协作式切换吞吐tests/bench/msg_pingpongmsg_send/msg_receive每秒消息发送次数即 IPC 通道吞吐以 thread_yield_pingpong/main.c 为例它同样使用xtimer_set_flag的 1 秒窗口、同样的result/ticks输出格式但循环体只是thread_yield()不涉及 mutex 队列操作。而 msg_pingpong/main.c 使用atomic_flag作为计时终止信号并通过msg_send/msg_receive在两个线程间传递消息测量的是内核消息队列core/msg.c的吞吐。三个基准共享的输出格式与代码骨架README 中有意重复代码以便比较代码大小的表述正是针对这一设计使开发者可以横向比较同一平台下 mutex 交接、主动让出、消息传递三种同步路径的开销差异纵向比较不同板卡或不同内核配置下的绝对开销。总结与使用建议mutex_pingpong是 RIOT 中一个小而精的内核性能探针测量方法两个线程通过一个 mutex 反复交接1 秒内统计 unlock 次数即上下文切换次数的一半并输出平均每次交接的 CPU 时钟数可配置性通过-DTEST_DURATION...调整测量窗口通过切换目标板卡评估不同硬件平台的调度开销自动化tests/01-run.py可接入 testrunner 自动校验输出格式可比性与thread_yield_pingpong、msg_pingpong保持同构代码骨架便于比较不同同步原语的代价与代码体积。如果你正在为 RIOT 应用做实时性调优例如评估某临界区频繁 lock/unlock 对系统响应的影响或想对比不同 CPU 移植cpu/目录下各架构的调度器实现质量这个基准都是最直接的测量起点。【免费下载链接】RIOTRIOT - The friendly OS for IoT项目地址: https://gitcode.com/GitHub_Trending/riot/RIOT创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考