
RIOT 递归互斥锁 C 测试详解tests/core/rmutex_cpp 的五线程并发场景、预期输出与 rmutex 实现原理【免费下载链接】RIOTRIOT - The friendly OS for IoT项目地址: https://gitcode.com/GitHub_Trending/riot/RIOT本文以 RIOT 内核核心测试tests/core/rmutex_cpp的 README 为主体完整讲解该测试的场景设计、构建运行方式与预期输出并结合 core/lib/rmutex.c 与 core/lib/include/rmutex.h 的源码深入解析递归互斥锁recursive mutex的引用计数、属主线程与阻塞等待队列机制帮助你既会用这个测试又懂它验证的内核同步原语。测试目标验证 rmutex 在多线程争用下的加锁顺序tests/core/rmutex_cpp/README.md 的 Background 一节明确写道该测试应用通过让多个线程在同一个互斥锁上等待对递归互斥锁进行压力测试This test application stresses a mutex with a number of threads waiting on it。其验证目标有三点阻塞与唤醒正确锁被持有时所有其他线程必须阻塞在rmutex_lock()上直到锁释放等待队列按优先级出队解锁后应由优先级最高的线程先获得锁RIOT 中优先级数值越小优先级越高同优先级按线程 ID 决胜两个线程优先级相同时线程 ID 较小者应先获得锁递归语义正确持锁线程可以反复对同一把锁加锁重入每次加锁打印递增的 depth解锁则按栈式顺序逐层回退。对应地README 的 Expected result 一节给出成功判据应看到 5 个不同线程打印各自的 PID、优先级与递归深度数值上最低即优先级最高的线程应第一个完成加锁与解锁其余线程按优先级依次跟进同优先级时线程 ID 较小者先获得锁。测试程序走读main.cpp 的完整逻辑整个测试的源码就是单文件 tests/core/rmutex_cpp/main.cpp核心结构定义在文件头部L26-L33#define THREAD_NUMOF (5U) static char stacks[THREAD_NUMOF][THREAD_STACKSIZE_MAIN]; static const char prios[THREAD_NUMOF] {THREAD_PRIORITY_MAIN - 1, 4, 5, 2, 4}; static const char depth[THREAD_NUMOF] {5, 3, 3, 4, 5}; static rmutex_t testlock;5 个线程各自分配一块THREAD_STACKSIZE_MAIN大小的栈C 版用主线程栈大小比 C 版的THREAD_STACKSIZE_SMALL更大内存占用更高主线程先创建线程线程号PID从 3 开始顺延即输出中的 T3T7优先级数组按创建顺序对应 T36THREAD_PRIORITY_MAIN - 1、T44、T55、T62、T74——注意T4 与 T7 同优先级恰好构成 README 所述同优先级按线程 ID 决胜的用例depth 数组规定每个线程的重入递归层数如 T6 递归 4 层输出 depth 03、T7 递归 5 层depth 04。main()的执行流程L67-L90是精心设计的让锁在恰当时刻被释放rmutex_init(testlock); /* lock mutex, so that spawned threads have to wait */ rmutex_lock(testlock); /* create threads */ for (unsigned i 0; i THREAD_NUMOF; i) { thread_create(stacks[i], sizeof(stacks[i]), prios[i], 0, lockme, (void *)(intptr_t)depth[i], t); } /* allow threads to lock the mutex */ printf(main: unlocking recursive mutex\n); rmutex_unlock(testlock); rmutex_lock(testlock); /* 主线程随后也阻塞等 5 个线程全部跑完 */ puts(\nTest END, check the order of priorities above.);关键点主线程先持锁再创建线程保证 5 个线程启动后全部阻塞在同一把锁上释放后主线程立即再次抢锁它优先级最低排在队尾从而在日志中等待全部子线程依次执行完毕。每个子线程的入口lockme只做一件事把 depth 参数转成intptr_t传入递归加锁函数lock_recursive()L35-L56static void lock_recursive(char n, char depth) { thread_t *t thread_get_active(); printf(T%i (prio %i, depth %i): trying to lock rmutex now\n, (int)t-pid, (int)t-priority, (int)n); rmutex_lock(testlock); printf(T%i (prio %i, depth %i): locked rmutex now\n, (int)t-pid, (int)t-priority, (int)n); if (n 1 depth) { lock_recursive(n 1, depth); /* 重入持锁状态下再次加锁 */ } thread_yield(); rmutex_unlock(testlock); printf(T%i (prio %i, depth %i): unlocked rmutex\n, (int)t-pid, (int)t-priority, (int)n); }注意lock_recursive在已经持有锁的状态下再次调用rmutex_lock()——这正是递归可重入语义的检验点若 rmutex 实现为普通互斥锁这里会死锁正确实现则只是递增引用计数。thread_yield()保证同优先级等待者之间有机会公平出队。构建与运行cpp 特性与内存受限板卡列表测试的 tests/core/rmutex_cpp/Makefile 非常简洁但每一行都有意义include ../Makefile.core_common FEATURES_REQUIRED cpp include $(RIOTBASE)/Makefile.includeFEATURES_REQUIRED cpp要求目标平台支持 C 编译本测试源码是.cpp验证的是内核 C API 从 C 代码中调用的可用性rmutex 接口通过extern C对 C 暴露见 core/lib/include/rmutex.htests/core/Makefile.core_common 只是设置RIOTBASE ? $(CURDIR)/../../..并引入Makefile.tests_common把测试接进 RIOT 通用构建系统。构建与烧录方式与所有 RIOT 测试一致例如在 PC 上直接运行make -C tests/core/rmutex_cpp BOARDnative make -C tests/core/rmutex_cpp BOARDnative run对于 MCU 板卡则追加flash目标并配合串口观察输出。需要注意 tests/core/rmutex_cpp/Makefile.ci 用BOARD_INSUFFICIENT_MEMORY显式排除了 26 个内存不足以承载5 ×THREAD_STACKSIZE_MAIN栈 5 个等待线程的板卡包括arduino-uno、atmega8、bluepill-stm32f030c8、nucleo-f030r8、samd10-xmini、stm32c0116-dk等。如果你的板卡 RAM 很小请优先选用列表之外的板卡或改跑栈更小的 C 版 tests/core/rmutex。预期输出逐行解读加锁顺序README 给出了完整成功样例此处原样保留xxx为实际版本号main(): This is RIOT! (Version: xxx) Recursive Mutex test Please refer to the README.md for more information Recursive Mutex test Please refer to the README.md for more information T3 (prio 6, depth 0): trying to lock rmutex now T4 (prio 4, depth 0): trying to lock rmutex now T5 (prio 5, depth 0): trying to lock rmutex now T6 (prio 2, depth 0): trying to lock rmutex now T7 (prio 3, depth 0): trying to lock rmutex now main: unlocking recursive mutex T6 (prio 2, depth 0): locked rmutex now T6 (prio 2, depth 1): trying to lock rmutex now T6 (prio 2, depth 1): locked rmutex now T6 (prio 2, depth 2): trying to lock rmutex now T6 (prio 2, depth 2): locked rmutex now T6 (prio 2, depth 3): trying to lock rmutex now T6 (prio 2, depth 3): locked rmutex now T6 (prio 2, depth 3): unlocked rmutex T6 (prio 2, depth 2): unlocked rmutex T6 (prio 2, depth 1): unlocked rmutex T6 (prio 2, depth 0): unlocked rmutex T7 (prio 3, depth 0): locked rmutex now T7 (prio 3, depth 1): trying to lock rmutex now T7 (prio 3, depth 1): locked rmutex now T7 (prio 3, depth 2): trying to lock rmutex now T7 (prio 3, depth 2): locked rmutex now T7 (prio 3, depth 3): trying to lock rmutex now T7 (prio 3, depth 3): locked rmutex now T7 (prio 3, depth 4): trying to lock rmutex now T7 (prio 3, depth 4): locked rmutex now T7 (prio 3, depth 4): unlocked rmutex T7 (prio 3, depth 3): unlocked rmutex T7 (prio 3, depth 2): unlocked rmutex T7 (prio 3, depth 1): unlocked rmutex T7 (prio 3, depth 0): unlocked rmutex T4 (prio 4, depth 0): locked rmutex now T4 (prio 4, depth 1): trying to lock rmutex now T4 (prio 4, depth 1): locked rmutex now T4 (prio 4, depth 2): trying to lock rmutex now T4 (prio 4, depth 2): locked rmutex now T4 (prio 4, depth 2): unlocked rmutex T4 (prio 4, depth 1): unlocked rmutex T4 (prio 4, depth 0): unlocked rmutex T5 (prio 5, depth 0): locked rmutex now T5 (prio 5, depth 1): trying to lock rmutex now T5 (prio 5, depth 1): locked rmutex now T5 (prio 5, depth 2): trying to lock rmutex now T5 (prio 5, depth 2): locked rmutex now T5 (prio 5, depth 2): unlocked rmutex T5 (prio 5, depth 1): unlocked rmutex T5 (prio 5, depth 0): unlocked rmutex T3 (prio 6, depth 0): locked rmutex now T3 (prio 6, depth 1): trying to lock rmutex now T3 (prio 6, depth 1): locked rmutex now T3 (prio 6, depth 2): trying to lock rmutex now T3 (prio 6, depth 2): locked rmutex now T3 (prio 6, depth 3): trying to lock rmutex now T3 (prio 6, depth 3): locked rmutex now T3 (prio 6, depth 4): trying to lock rmutex now T3 (prio 6, depth 4): locked rmutex now T3 (prio 6, depth 4): unlocked rmutex T3 (prio 6, depth 3): unlocked rmutex T3 (prio 6, depth 2): unlocked rmutex T3 (prio 6, depth 1): unlocked rmutex T3 (prio 6, depth 0): unlocked rmutex Test END, check the order of priorities above.输出可以分四段理解两遍启动横幅Recursive Mutex test / Please refer to the README.md for more information 出现两次来自 RIOT 的标准启动流程先执行一次main()打印第一遍随后在测试环境如 murdock 复测或重启中再次执行五连trying to lockT3T7 按创建顺序启动并依次阻塞此时锁仍由 main 持有main: unlocking recursive mutex 之后的主体获得锁的顺序为T6prio 2→ T7 → T4prio 4→ T5prio 5→ T3prio 6完全符合数值优先级小者优先的调度规则每个线程内部 depth 从 0 递增到自身层数T6 到 3、T7 到 4、T4 到 2、T5 到 2、T3 到 4解锁则严格逆序证明引用计数成对增减结尾 Test END, check the priorities order above.主线程在队尾拿到锁后打印作为测试正常结束的锚点。一个值得注意的细节对照当前 main.cpp 中的优先级数组{THREAD_PRIORITY_MAIN - 1, 4, 5, 2, 4}T7 的优先级应为 4与 T4 相同而 README 样例中 T7 显示为 prio 3——可以推断样例输出对应的是更早的优先级配置当时 T7 数值优先级为 3故排在 T4 之前。在当前配置下 T4 与 T7 构成同优先级对按 README 声明的决胜规则线程 ID 小者优先实际运行中两者都应在 T5、T3 之前、T6 之后出现只是二者之间的先后由同优先级决胜规则决定。核对结果时建议关注整体优先级序 每线程 depth 对称回退这两个不变量而非逐字符比对。源码级原理rmutex 如何用引用计数实现递归数据结构一把普通 mutex 加两个字段core/lib/include/rmutex.h 中rmutex_t的定义是理解全部行为的关键typedef struct rmutex_t { mutex_t mutex; /* 底层等待/唤醒字段不可手动改动 */ uint16_t refcount; /* 属主线程当前持锁次数 */ kernel_pid_t owner; /* 属主线程 PID用原子读写保证一致性 */ } rmutex_t; #define RMUTEX_INIT { MUTEX_INIT, 0, KERNEL_PID_UNDEF }等待队列、阻塞/唤醒全部复用普通互斥锁见 core/mutex.c 与 core/mutex.doc.md 中解锁时按优先级出队的数据结构说明refcount表达递归深度只有属主线程读写由 mutex 本身保护无需原子操作owner会被非属主线程并发读取用于判断锁是不是我的因此用atomic_load/store_kernel_pid()以 relaxed 语义读写源码注释core/lib/rmutex.c还专门论证了为何 relaxed 足够其他线程只会把 owner 写成KERNEL_PID_UNDEF或它们自己的 pid绝不会写成当前线程的 pid因此判等结果不会漏判阻塞分支初始化推荐使用静态宏RMUTEX_INIT动态分配时用rmutex_init()L71-L76就是一个整体赋值。加锁路径_lock()的三种结局所有加锁 APIrmutex_lock、rmutex_trylock、rmutex_lock_max、rmutex_trylock_max最终都汇入 core/lib/rmutex.c 的_lock(rmutex, max, trylock)其逻辑可归纳为快路径mutex_trylock()成功——锁空闲直接成为属主慢路径锁被占原子读owner并与thread_getpid()比较Case 1 锁属于别人trylock模式直接返回 0对应测试中 5 个线程全部阻塞的形态阻塞模式调用mutex_lock()进入按优先级排序的等待队列Case 2 锁属于自己重入什么都不做直接落到计数逻辑——这就是递归的出处计数成为属主后原子写owner 自己再检查refcount max则返回 0容量超限否则refcount并返回 1。对外包装L139-L159各有侧重API语义关键行为rmutex_lock()阻塞式加锁内部以CONFIG_CORE_RMUTEX_MAX默认UINT16_MAX见 rmutex.c#L34-L38为容量上限计数溢出时触发core_panic(PANIC_ASSERT_FAIL, rmutex refcount overflow)rmutex_trylock()非阻塞加锁锁被他人持有或容量超限时返回 0成功返回 1自己重入也算成功rmutex_lock_max(r, max)带最大递归深度的阻塞加锁深度达到max时返回 0rmutex_trylock_max(r, max)带最大递归深度的非阻塞加锁同上但不等待解锁路径最后一次 unlock 才真正释放rmutex_unlock()core/lib/rmutex.c#L161-L185执行严格的引用计数回退assert(atomic_load_kernel_pid(rmutex-owner) thread_getpid()); assert(rmutex-refcount 0); rmutex-refcount--; if (rmutex-refcount 0) { atomic_store_kernel_pid(rmutex-owner, KERNEL_PID_UNDEF); mutex_unlock(rmutex-mutex); }两条assert分别拦截非属主解锁和空解锁两类编程错误只有refcount归零时才会把owner复位并调用底层mutex_unlock()唤醒等待队列中优先级最高的线程——这正对应测试输出里每个线程depth 从最大值一路减到 0后才轮到下一线程接管的现象。等待队列阻塞线程为何按优先级出队测试顺序之所以稳定取决于底层 mutex 的等待队列组织方式。从 core/mutex.c 的_block()与 core/mutex.doc.md 的说明可以看出阻塞时当前线程被置为STATUS_MUTEX_BLOCKED并插入 mutex 的等待链表thread_add_to_list()链表按线程优先级有序组织mutex_unlock()唤醒链表头部即优先级最高的线程若它是最后一个等待者则锁直接转入无等待者的持有态。可以推断同优先级下等待者保持插入顺序即创建顺序/线程 ID 顺序这与 README 中same priority → lower thread id acquires the lock的判据相吻合。另需留意默认 mutex不启用优先级继承如需防优先级翻转应启用core_mutex_priority_inheritance模块见 core/mutex.doc.md 开头的 warning而本测试恰恰依赖无继承的原始优先级来检验调度顺序。与 C 版测试的关系及 C 特性要求tests/core/rmutex_cpp与 tests/core/rmutex 是同场景姊妹测试C 版主文件 tests/core/rmutex/main.c 使用相同优先级/深度数组{6, 4, 5, 2, 4}/{5, 3, 3, 4, 5}但线程栈用的是THREAD_STACKSIZE_SMALL THREAD_EXTRA_STACKSIZE_PRINTF。cpp 版把同样的内核 API 用 C 编译单元调用#include rmutex.h在extern C块内声明保证名字不会被 C mangling并因此需要FEATURES_REQUIRED cpp。如果你的项目是 C 应用或 C/C 混合工程跑这个测试能验证 rmutex 头文件在 C 编译环境下的可用性与行为一致性纯 C 项目跑tests/core/rmutex即可且它占用的栈内存更少、可用板卡更广。小结tests/core/rmutex_cpp用主线程持锁 → 创建 5 个不同优先级线程 → 放锁的编排把递归互斥锁的阻塞、优先级出队、同优先级决胜、重入计数四种性质压缩进一份可读的串口日志预期输出的核对锚点是加锁顺序 T6→T7→T4→T5→T3当前配置下 T4/T7 同优先级先后由线程 ID 决胜且每线程 depth 严格成对递增后逆序回退实现上rmutex_t 底层mutex_trefcount 原子owner重入只增计数、末次解锁才释放CONFIG_CORE_RMUTEX_MAX与rmutex_*_max系列 API 为递归深度提供了可配置上限运行前请对照 Makefile.ci 的BOARD_INSUFFICIENT_MEMORY列表确认板卡 RAM 是否足够C 工程场景下建议与 C 版 tests/core/rmutex 一并纳入回归。【免费下载链接】RIOTRIOT - The friendly OS for IoT项目地址: https://gitcode.com/GitHub_Trending/riot/RIOT创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考