ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

RIOT 内核消息传递回归测试解析:thread_msg_block_w_queue 与 STATUS_REPLY_BLOCKED 阻塞语义

RIOT 内核消息传递回归测试解析:thread_msg_block_w_queue 与 STATUS_REPLY_BLOCKED 阻塞语义 RIOT 内核消息传递回归测试解析thread_msg_block_w_queue 与 STATUS_REPLY_BLOCKED 阻塞语义【免费下载链接】RIOTRIOT - The friendly OS for IoT项目地址: https://gitcode.com/GitHub_Trending/riot/RIOT导读thread_msg_block_w_queue是 RIOT 操作系统内核层core中一个精悍而重要的回归测试它针对历史 issue 中发送方线程正处于STATUS_REPLY_BLOCKED等待回复阻塞状态时仍被错误地排入接收方消息队列这一内核缺陷编写了最小复现场景并验证修复没有再次失效。通过阅读本文你将理解 RIOT 消息传递机制中队列投递、msg_send_receive同步请求-应答流程与线程调度状态机之间的交互关系掌握如何阅读、运行和扩展这一类内核回归测试。测试背景Issue 背后的内核缺陷原始问题描述该测试源于 RIOT 历史 issue #100。文档描述的异常场景如下通常情况下当一个线程sender_thread向另一个线程main发送消息而main的消息队列中已持有消息时sender_thread的消息会被复制进main的消息队列同时sender_thread被置为STATUS_PENDING。然而当sender_thread正处于STATUS_REPLY_BLOCKED模式时不应该发生上述行为——因为它正处于另一个阻塞式发送的中间过程等待回复此时再次被置为STATUS_PENDING会破坏其等待回复的语义。简单说一个线程在调用msg_send_receive()等待回复期间如果它顺路向目标投递的消息被目标的消息队列接收发送线程就会被调度器误判为任务完成从而脱离STATUS_REPLY_BLOCKED状态。该缺陷已被修复本测试用于确保其不会再次回归。线程状态机中的关键状态要理解上述缺陷需要先认识 RIOT 内核中与消息传递相关的线程状态定义见 core/include/sched.h状态含义说明STATUS_RECEIVE_BLOCKED阻塞等待接收线程调用msg_receive()且队列为空时进入STATUS_SEND_BLOCKED阻塞等待发送线程调用阻塞式msg_send()而目标无法立即接收时进入STATUS_REPLY_BLOCKED阻塞等待回复线程调用msg_send_receive()后、收到msg_reply()之前的整个阶段STATUS_PENDING就绪线程可被调度器选中运行这些状态对应的可读字符串由 core/thread.c 中的state_names数组给出bl reply、bl send、bl rx等可用于调试输出。测试程序逐步拆解测试主体位于 tests/core/thread_msg_block_w_queue/main.c包含两个线程sender_thread发送方和main接收方。整个测试通过三个步骤精确构造出队列已满 发送方处于 REPLY_BLOCKED的临界场景。全局数据结构char t1_stack[THREAD_STACKSIZE_MAIN]; kernel_pid_t p_send KERNEL_PID_UNDEF, p_recv KERNEL_PID_UNDEF; static msg_t _msg_q[1];发送线程使用THREAD_STACKSIZE_MAIN大小的栈与主线程栈尺寸相同保证栈空间充足。接收方即main线程只初始化了容量为 1的消息队列_msg_q这是整个测试的关键前提队列空间极小便于迅速被填满。sender_thread两次发送构造临界状态void *sender_thread(void *arg) { (void) arg; printf(sender_thread start\n); msg_t msg, reply; memset(msg, 1, sizeof(msg_t)); /* step 1: send non-blocking to fill up the msg_queue of p_recv */ msg_try_send(msg, p_recv); /* step 2: send message. This puts sender_thread into msg_waiters and turns its status into STATUS_REPLY_BLOCKED. It should block forever, since the second message is never read by p_recv. */ msg_send_receive(msg, reply, p_recv); /* If this is printed, sender_thread did *not* block as expected. */ printf(ERROR: sender_thread should be blocking\n); return NULL; }step 1调用msg_try_send()非阻塞发送见 core/msg.c向main发送一条消息。此时main尚未调用msg_receive()且其队列容量为 1这条消息恰好把队列填满。step 2调用msg_send_receive()再发送第二条消息。由于main的队列已满消息无法入队sender_thread被加入main的msg_waiters等待者链表状态切换为STATUS_REPLY_BLOCKED等待回复。由于测试中main永远不会对这条消息调用msg_reply()理论上sender_thread应永久阻塞。若它非正常地从msg_send_receive()返回并打印出ERROR: sender_thread should be blocking则说明回归缺陷重现。main线程接收第一条消息并存活int main(void) { msg_t msg; p_recv thread_getpid(); msg_init_queue(_msg_q, 1); p_send thread_create(t1_stack, sizeof(t1_stack), THREAD_PRIORITY_MAIN - 1, THREAD_CREATE_WOUT_YIELD, sender_thread, NULL, nr1); /* step 3: receive first msg from sender_thread*/ msg_receive(msg); printf(main thread alive\n); return 0; }msg_init_queue(_msg_q, 1)为main建立容量为 1 的消息队列。thread_create()以THREAD_PRIORITY_MAIN - 1比主线程更高优先级创建sender_thread并传入THREAD_CREATE_WOUT_YIELD标志——创建时不立即让出 CPU因此sender_thread要等main主动阻塞调用msg_receive()后才有机会运行。这一点保证了执行顺序可控main先创建发送线程不切换main调用msg_receive()阻塞等待调度器切换至sender_thread它执行 step 1 填满队列、step 2 进入REPLY_BLOCKEDsender_thread阻塞后main被唤醒从队列中取出第一条消息打印main thread alive。若回归缺陷存在sender_thread会在 step 2 中虚假成功打印错误信息并抢占运行导致输出顺序错乱。源码级原理为什么修复后的行为是正确的队列投递路径与状态保护在 core/msg.c 的_msg_send()中当目标线程不在STATUS_RECEIVE_BLOCKED时内核尝试调用queue_msg()将消息投递到目标队列if (queue_msg(target, m)) { DEBUG(msg_send(): Target % PRIkernel_pid has a msg_queue. Queueing message.\n, ...); irq_restore(state); if (me-status STATUS_REPLY_BLOCKED || (IS_USED(MODULE_CORE_THREAD_FLAGS) sched_context_switch_request) ) { thread_yield_higher(); } return 1; }注意投递成功后的分支如果当前发送方自身正处于STATUS_REPLY_BLOCKED则调用thread_yield_higher()主动让出 CPU。这保证了发送方不会假装任务完成继续执行而是保持在回复阻塞的等待链中。这正是针对 issue #100 的核心修复逻辑之一。入队失败后的阻塞分支若queue_msg()返回 0队列已满或目标没有队列且发送是阻塞式的则me-wait_data m; int newstatus; if (me-status STATUS_REPLY_BLOCKED) { newstatus STATUS_REPLY_BLOCKED; /* 保持 REPLY_BLOCKED而非降级为 SEND_BLOCKED */ } else { newstatus STATUS_SEND_BLOCKED; } sched_set_status(me, newstatus); thread_add_to_list((target-msg_waiters), me);这一分支显式保留了STATUS_REPLY_BLOCKED状态一个已经处于REPLY_BLOCKED的线程即使发送被阻塞也不会被降级为STATUS_SEND_BLOCKED从而避免丢失等待回复的语义。若这里被错误地覆盖为STATUS_PENDING或STATUS_SEND_BLOCKEDmsg_reply()将无法找到正确的回复目标。msg_send_receive()的完整流程core/msg.c 中的msg_send_receive()是实现同步请求-应答的关键 APIint msg_send_receive(msg_t *m, msg_t *reply, kernel_pid_t target_pid) { assert(thread_getpid() ! target_pid); ... unsigned state irq_disable(); thread_t *me thread_get_active(); thread_status_t prev_status thread_get_status(me); sched_set_status(me, STATUS_REPLY_BLOCKED); me-wait_data reply; /* we reuse (abuse) reply for sending, because wait_data might be * overwritten if the target is not in RECEIVE_BLOCKED */ *reply *m; int res _msg_send(reply, target_pid, true, state); if (res -1) { /* 发送失败时恢复线程原状态避免线程永久滞留阻塞态 */ state irq_disable(); me-wait_data NULL; sched_set_status(me, prev_status); irq_restore(state); } return res; }几个值得注意的实现细节预先置为REPLY_BLOCKED在真正调用_msg_send()之前线程状态就被切换为STATUS_REPLY_BLOCKEDwait_data指向reply。这正是_msg_send()中投递成功但发送方处于REPLY_BLOCKED时让出 CPU以及阻塞时保持REPLY_BLOCKED两个分支能够生效的前提。复用reply缓冲区发送代码注释明确指出wait_data可能因目标不在RECEIVE_BLOCKED而被_msg_send()覆盖因此借用reply空间作为发送缓冲区保证数据安全。失败恢复若发送返回-1例如目标线程不存在会恢复prev_status并清空wait_data防止线程永远卡在阻塞态。应答路径msg_reply()只认REPLY_BLOCKEDcore/msg.c 的msg_reply()通过m-sender_pid找到目标线程并校验其状态必须是STATUS_REPLY_BLOCKED否则返回-1if (target-status ! STATUS_REPLY_BLOCKED) { DEBUG(msg_reply(): % PRIkernel_pid : Target \% PRIkernel_pid \ not waiting for reply., ...); irq_restore(state); return -1; } /* copy msg to target */ msg_t *target_message (msg_t *)target-wait_data; *target_message *reply; sched_set_status(target, STATUS_PENDING);由此可见STATUS_REPLY_BLOCKED是请求-应答协议的枢纽只有处于该状态的线程才是合法的应答对象。一旦这个状态被队列投递路径错误破坏issue #100 的缺陷后续msg_reply()将直接失败导致系统级联异常。本测试正是守住了这条状态链的完整性。期望输出与回归判定文档给出了明确的判定标准。正常运行时应输出sender_thread start main thread alive如果看到ERROR: sender_thread should be blocking则说明回归缺陷重现sender_thread未如预期阻塞。这一判定逻辑同时被自动化测试脚本固化在 tests/core/thread_msg_block_w_queue/tests/01-run.py 中def testfunc(child): child.expect(sender_thread start\r\n) child.expect(main thread alive\r\n)脚本基于 RIOT 的testrunner框架通过 pexpect 风格的expect按顺序匹配两行输出。只要输出顺序或内容不符例如插入了ERROR:行测试即失败。构建与运行方式该测试通过 Makefile 纳入核心测试套件include ../Makefile.core_common include $(RIOTBASE)/Makefile.includeMakefile.core_common提供核心模块core相关的公共构建配置$(RIOTBASE)/Makefile.include引入 RIOT 全局构建系统。运行方式与所有 RIOT 应用一致# 在测试目录下指定 BOARD 编译并烧录 make BOARDnative -C tests/core/thread_msg_block_w_queue flash term # 或使用统一的测试框架需要 board 支持 make BOARDnative -C tests/core/thread_msg_block_w_queue test其中BOARDnative是 RIOT 的宿主机模拟板无需真实硬件即可运行也可替换为任意受支持的真实开发板如BOARDnrf52dk、BOARDsame54-xpro等。Makefile.ci列出了持续集成环境中覆盖该测试的板卡集合。测试设计的工程启示从本测试可以提炼出 RIOT 内核测试的几条通用实践用最小场景复现历史缺陷仅用两个线程、一个容量为 1 的消息队列就完整构造出队列满 发送方 REPLY_BLOCKED的临界条件未引入任何无关模块。以输出顺序断言行为通过sender_thread start与main thread alive的先后顺序以及ERROR行是否出现即可判定内核状态机行为是否正确——这也是嵌入式系统测试中低成本、高可移植性的典型手法。将回归用例固化为自动化脚本01-run.py使该场景进入 CI 持续回归确保修复不会因后续改动而失效。若要进一步深入可从 core/include/msg.h消息 API 声明、core/include/sched.h线程状态枚举以及msg_send_receive/msg_reply的完整实现core/msg.c入手理解 RIOT 消息传递与调度器的完整协作机制。【免费下载链接】RIOTRIOT - The friendly OS for IoT项目地址: https://gitcode.com/GitHub_Trending/riot/RIOT创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表