尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

RIOT IPC Pingpong 示例深度解析:从 `msg_send_receive()` 到内核消息机制

RIOT IPC Pingpong 示例深度解析:从 `msg_send_receive()` 到内核消息机制 RIOT IPC Pingpong 示例深度解析从msg_send_receive()到内核消息机制【免费下载链接】RIOTRIOT - The friendly OS for IoT项目地址: https://gitcode.com/GitHub_Trending/riot/RIOT导读本文围绕 RIOTThe friendly OS for IoT官方基础示例 examples/basic/ipc_pingpong 展开完整讲解如何使用 RIOT 内核自带的 IPCInter-Process Communication进程间通信消息系统在两个线程之间传递数据。读完本文你将掌握msg_t消息结构、thread_create()线程创建、msg_send_receive()/msg_reply()同步请求-应答模式的用法并能从 core/msg.c 源码层面理解消息在调度器中的传递路径。一、示例概览两个线程的乒乓球游戏IPC Pingpong 示例的核心思想非常直观应用启动一个额外线程second thread也就是pong与main 线程也就是ping之间反复互发消息形成类似乒乓球来回击打的循环。整个流程如下main 线程创建第二个线程main 线程进入死循环反复调用msg_send_receive()把自己持有的消息发给 2 号线程然后阻塞等待回复2 号线程收到消息后打印2nd: Got msg from xx 为发送方 PID把消息内容中的计数值加 1再调用msg_reply()将消息回发给 main 线程main 线程收到回复后打印1st: Got msg with content N然后进入下一轮循环计数值继续累加循环往复。示例的正确输出如下出自 README.mdThis is RIOT! (Version: xxx) kernel_init(): jumping into first task... Starting IPC Ping-pong example... 1st thread started, pid: 1 2nd thread started, pid: 2 2nd: Got msg from 1 1st: Got msg with content 2 2nd: Got msg from 1 1st: Got msg with content 3 2nd: Got msg from 1 1st: Got msg with content 4 2nd: Got msg from 1 1st: Got msg with content 5 2nd: Got msg from 1 1st: Got msg with content 6 2nd: Got msg from 1 1st: Got msg with content 7 2nd: Got msg from 1 1st: Got msg with content 8 2nd: Got msg from 1 1st: Got msg with content 9 2nd: Got msg from 1 1st: Got msg with content 10 [...]可以看到main 线程先打印自己启动pid 1随后 2 号线程打印自己启动pid 2。由于第 2 个线程的优先级被设置为比 main 更高它会立即抢占执行并先运行起来。之后两个线程按照发送—应答的节奏交替打印内容计数值从 1 开始一路递增——这正是msg_reply()回复时把m.content.value的结果带给发送方的直接体现。二、完整源码逐行拆解示例的全部代码位于 examples/basic/ipc_pingpong/main.c仅依赖两个核心头文件#include stdio.h #include thread.h #include msg.h其中thread.h提供线程创建与 PID 查询接口msg.h提供消息收发 API。1. 第二个线程接收、增值、回复void *second_thread(void *arg) { (void) arg; printf(2nd thread started, pid: % PRIkernel_pid \n, thread_getpid()); msg_t m; while (1) { msg_receive(m); printf(2nd: Got msg from % PRIkernel_pid \n, m.sender_pid); m.content.value; msg_reply(m, m); } return NULL; }要点说明thread_getpid()返回当前线程的 PID打印时使用% PRIkernel_pid格式符。该宏在 core/include/sched.h 中定义为PRIi16kernel_pid 是 16 位有符号整数是跨平台打印 PID 的正确姿势。msg_receive(m)会阻塞当前线程直到收到一条消息。收到消息后m.sender_pid会被内核自动填充为发送方的 PID见 core/include/msg.h 中msg_t结构定义所以接收方能直接打印来自谁。m.content.value把消息内容无符号 32 位整数加 1这正是输出中计数递增的来源。msg_reply(m, m)是点睛之笔回复消息和原消息复用同一个msg_t缓冲区合法且高效。从 core/include/msg.h 的注释可以看到msg_send_receive()允许reply与m指向同一块内存。2. 栈分配与线程创建char second_thread_stack[THREAD_STACKSIZE_MAIN];THREAD_STACKSIZE_MAIN是 RIOT 为 main 线程提供的默认栈大小在thread.h/ 各 CPU 的thread_arch.h中按平台定义。这里直接为第二个线程分配同样大小的静态栈保证其有足够的调用深度——这是线程创建前必须预先准备的内存。kernel_pid_t pid thread_create(second_thread_stack, sizeof(second_thread_stack), THREAD_PRIORITY_MAIN - 1, 0, second_thread, NULL, pong);thread_create()的原型位于 core/include/thread.h参数依次为参数本示例取值含义stacksecond_thread_stack预分配的线程栈内存stacksizesizeof(...)栈大小字节priorityTHREAD_PRIORITY_MAIN - 1线程优先级数值越小优先级越高flags0创建标志如THREAD_CREATE_SLEEPING、THREAD_CREATE_WOUT_YIELD、THREAD_CREATE_NO_STACKTEST0 表示立即加入就绪队列funcsecond_thread线程入口函数argNULL传给入口函数的参数namepong线程名称调试用关于优先级THREAD_PRIORITY_MAIN - 1RIOT 使用固定优先级调度算法优先级数值越小优先级越高0 为最高。THREAD_PRIORITY_MAIN - 1意味着 2 号线程的优先级略高于main 线程参考 core/include/thread.h 的说明因此它创建后会立刻运行。这也解释了输出顺序先打印1st thread started, pid: 1main 在创建线程前打印随后立即切到 2 号线程打印2nd thread started, pid: 2再进入msg_receive()阻塞等待。3. main 线程发送并等待应答m.content.value 1; while (1) { msg_send_receive(m, m, pid); printf(1st: Got msg with content %u\n, (unsigned int)m.content.value); }初始计数值设为 1每一轮把消息m发送给 2 号线程pid阻塞等待回复回复内容直接写回同一个m因此第一轮回复内容为 2与输出1st: Got msg with content 2完全对应。三、内核 IPC 消息模型msg_t详解要从底层理解这个示例需要先看消息的结构。msg_t定义于 core/include/msg.htypedef struct { kernel_pid_t sender_pid; /** PID of sending thread. Will be filled in by msg_send. */ uint16_t type; /** Type field. */ union { void *ptr; /** Pointer content field. */ uint32_t value; /** Value content field. */ } content; /** Content of the message. */ } msg_t;三个字段的作用sender_pid发送方 PID由内核在msg_send()时自动填充用户无需设置示例中 2 号线程打印m.sender_pid用的就是它type消息类型帮助接收方区分不同消息内核本身不读取它含义完全由用户定义content联合体既可以存放 32 位整数value也可以存放指针ptr。Pingpong 示例使用的是整数语义而在 tests/core/msg_send_receive/main.c 的测试中则用content.ptr传递指向计数器的指针两种用法都合法。四、同步请求-应答msg_send_receive()与msg_reply()的配对msg_send_receive()是 RIOT IPC 中典型的**同步请求-应答synchronous request-reply**模式API 定义如下core/include/msg.hint msg_send_receive(msg_t *m, msg_t *reply, kernel_pid_t target_pid);行为是向target_pid发送消息m然后阻塞当前线程直到对方回复回复内容写入reply。配套要求是接收方必须用msg_reply()而非msg_send()来回复。其实现位于 core/msg.c 的msg_send_receive()先把当前线程状态切换为STATUS_REPLY_BLOCKED并把reply指针存入me-wait_data复用reply缓冲区承载待发送消息源码注释明确说明这是故意的因为如果目标线程不处于接收阻塞态wait_data可能被覆盖调用内部_msg_send()发送。若发送失败如目标 PID 非法会恢复之前的线程状态避免线程永久卡在阻塞态。而msg_reply()的实现core/msg.c通过m-sender_pid找到发送方线程检查其状态是否为STATUS_REPLY_BLOCKED若不是则返回-1表示对方并不在等待回复将回复消息直接拷贝到发送方的wait_data即msg_send_receive()传入的reply缓冲区并把发送方状态置为STATUS_PENDING重新加入调度。这就解释了 Pingpong 示例中为什么发出去的内容是 N收回来的内容是 N1回复时m.content.value修改的是同一个msg_t内核将整个结构体拷贝回发送方发送方打印的自然就是增值后的数值。五、两种线程优先级关系带来的调度行为RIOT 的调度基于固定优先级fixed priority规则总结core/include/thread.h优先级数值越小越高0 为最高相同优先级的多个线程是协作式调度的一个线程会一直运行直到主动让出thread_yield()或更高优先级线程就绪为不同线程分配不同优先级是推荐做法。在 Pingpong 示例中THREAD_PRIORITY_MAIN - 1使 2 号线程优先级高于 main。由于thread_create()的flags为 0不带THREAD_CREATE_WOUT_YIELD新线程创建后立即被调度运行它打印启动信息后进入msg_receive()阻塞。随后 main 线程得以继续进入msg_send_receive()发送第一条消息并阻塞等待回复此时 2 号线程被唤醒处理消息、回复、再阻塞……两个线程就这样在高优先级等待 低优先级发送的节奏下被调度器轮转驱动形成稳定的 ping-pong 循环。六、构建与运行1. 编译在示例目录下直接调用make即可cd examples/basic/ipc_pingpong makeMakefile 关键配置APPLICATION ipc_pingpong应用名BOARD ? native默认目标板为nativeRIOT 的模拟器可在普通 Linux 主机上直接运行RIOTBASE ? $(CURDIR)/../../..指向 RIOT 仓库根目录的绝对路径DEVELHELP ? 1开启开发辅助检查如断言、越界检测生产环境可注释关闭QUIET ? 1默认隐藏编译命令置 0 可显示完整命令行。内存受限提示示例的 Makefile.ci 列出了 CI 中判定内存不足的板卡如nucleo-l011k4、stm32f030f4-demo。这些板载 RAM 较小的平台可能放不下两个线程的完整栈每线程THREAD_STACKSIZE_MAIN如需在这些板上运行可考虑缩小线程栈或裁剪功能。2. 运行nativemake termmake term会启动 native 模拟器并接入终端你将看到与 README 中一致的输出内核初始化信息、两个线程的启动信息然后是交替进行的 ping-pong 打印内容数值持续递增[...]表示输出会一直持续因为两个线程都是死循环。按CtrlC退出。3. 在其他真实硬件上运行指定任意支持的板卡即可例如make BOARDsamr21-xpro flash termBOARD变量覆盖默认的nativeflash负责烧录term打开串口终端查看输出。RIOT 的 IPC 是内核模块core/msg所有支持 RIOT 的板卡行为一致示例输出中仅版本号与 PID 可能因平台而异PID 恒为 1 与 2因为内核启动时 main 总是第一个线程。七、进阶从 Pingpong 出发认识 RIOT IPC 全貌Pingpong 只用了 IPC 的冰山一角。msg.h与msg.c还提供了完整的 IPC 原语理解它们能帮你写出更健壮的多线程应用阻塞 vs 非阻塞msg_send()/msg_receive()是阻塞版msg_try_send()/msg_try_receive()是非阻塞版不会阻塞调用方core/include/msg.h。示例 2 号线程的msg_receive()就是阻塞接收的典型用法。同步 vs 异步默认接收线程无消息队列为同步模式发送方可能被阻塞调用msg_init_queue()初始化消息队列大小必须是 2 的幂后即进入异步模式投递到队列的消息不会丢msg_send()也不会阻塞core/include/msg.h 的Blocking vs Non-blocking与Synchronous vs Asynchronous章节。中断上下文ISR 中发送消息应使用msg_send_int()此时m-sender_pid会被设为KERNEL_PID_ISR可用msg_sent_by_int()判断消息是否来自中断。队列状态查询msg_avail()/msg_avail_thread()/msg_queue_capacity()可查询线程消息队列的积压与容量便于做背压控制。测试佐证仓库的 tests/core/msg_send_receive/main.c 用两个线程反复执行msg_send_receive()并校验回复内容包括指针内容一致性、计数器累加是理解该 API 契约的权威参考tests/core/thread_msg、tests/core/thread_msg_block_w_queue 等测试则覆盖了带/不带消息队列的阻塞收发场景。另外RIOT 还提供了发布-订阅风格的msg_bus见 core/msg_bus.c 与 core/msg.c 中的msg_send_bus()适合一对多广播网络协议栈 gnrc 也大量复用msg_t作为驱动与协议层之间的消息载体如 tests/net/gnrc_udp/udp.c。八、小结通过 IPC Pingpong 示例我们完整走通了 RIOT 多线程 IPC 的一条主线用thread_create() 静态栈创建新线程并通过优先级THREAD_PRIORITY_MAIN - 1控制调度时机用msg_send_receive()发起同步请求并阻塞等待用msg_reply()应答通过msg_t的content.value在往返过程中携带并累加数据理解sender_pid由内核自动填充的机制。这套发送—阻塞—回复—唤醒的机制在 core/msg.c 中由STATUS_REPLY_BLOCKED/STATUS_RECEIVE_BLOCKED/STATUS_PENDING等线程状态机驱动是理解 RIOT 内核调度与通信设计的绝佳切入点。无论你后续要写传感器数据采集、网络协议栈驱动还是 GUI 事件循环这套 IPC 原语都是 RIOT 应用开发的地基。【免费下载链接】RIOTRIOT - The friendly OS for IoT项目地址: https://gitcode.com/GitHub_Trending/riot/RIOT创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表