前言
在上一篇中,我们学习了消息队列——它可以传递任意大小的数据,非常灵活。但它的内部涉及链表操作和memcpy,如果只是传递一些很小的数据(比如状态码、计数值、指针地址),用消息队列就有点"杀鸡用牛刀"了。
RT-Thread 提供了另一种轻量级的线程间通信机制——邮箱(Mailbox)。
一句话总结:传递小数据用邮箱,传递大数据用消息队列。
目录
前言
一、邮箱的特性
1.1 邮箱的本质
1.2 核心特性
1.3 邮箱 vs 消息队列
二、邮箱操作 API
2.1 创建/初始化
2.2 删除/脱离
2.3 发送邮件
2.4 接收邮件
三、完整示例
四、邮箱的使用技巧
4.1 通过邮箱传递指针
4.2 邮箱满时的阻塞发送
五、邮箱 vs 消息队列 选型指南
六、总结
一、邮箱的特性
1.1 邮箱的本质
邮箱的底层实现是环形缓冲区(Ring Buffer),而非消息队列的链表结构:
写入端 读出端 ┌──────────┐ ┌────┬────┬────┬────┐ ┌──────────┐ │ 线程A │───→│ │ │ │ │───→│ 线程B │ ├──────────┤ │ ██ │ ██ │ ██ │ ██ │ ├──────────┤ │ 线程B │───→│ │ │ │ │───→│ ISR │ ├──────────┤ └────┴────┴────┴────┘ └──────────┘ │ ISR │───→ 环形缓冲区 └──────────┘ 每格 = 4 字节
1.2 核心特性
| 特性 | 说明 |
|---|---|
| 邮件大小 | 每封邮件固定4 字节(32 位系统上为一个rt_uint32_t) |
| 底层结构 | 环形缓冲区(数组),比链表的消息队列更高效 |
| 存储能力 | 可缓存一定数量的邮件 |
| 发送内容 | 任意 32 位数据,可以是整数值,也可以是指针 |
| 阻塞访问 | 空时读可阻塞,满时写可阻塞 |
| 超时机制 | 支持超时等待 |
1.3 邮箱 vs 消息队列
| 对比项 | 邮箱(Mailbox) | 消息队列(Message Queue) |
|---|---|---|
| 数据大小 | 固定 4 字节 | 任意大小 |
| 内部实现 | 环形缓冲区(数组) | 链表+ memcpy |
| 效率 | ⭐⭐⭐⭐⭐更高 | ⭐⭐⭐ 中等 |
| 内存开销 | 小(每个邮件 4 字节) | 较大(消息块 + 链表指针) |
| 适用场景 | 传递小数值 / 指针 | 传递大数据 / 结构体 |
| 紧急消息 | 不支持rt_mb_urgent() | 支持rt_mq_urgent() |
选型口诀:传数值用邮箱,传结构体用消息队列。
二、邮箱操作 API
2.1 创建/初始化
动态创建 —rt_mb_create
rt_mailbox_t rt_mb_create(const char* name, rt_size_t size, // 邮箱容量(邮件个数) rt_uint8_t flag); // RT_IPC_FLAG_FIFO / RT_IPC_FLAG_PRIO返回值:成功返回邮箱句柄,失败返回
RT_NULL邮件缓冲区由函数内部动态分配
// 示例:创建可容纳 5 封邮件的邮箱 rt_mailbox_t mb = rt_mb_create("mbox", 5, RT_IPC_FLAG_FIFO); if (mb == RT_NULL) rt_kprintf("Failed to create mailbox!\n");静态初始化 —rt_mb_init
rt_err_t rt_mb_init(rt_mailbox_t mb, // 邮箱句柄 const char* name, void* msgpool, // 预分配的缓冲区 rt_size_t size, // 邮箱容量(邮件个数) rt_uint8_t flag);返回值:成功返回
RT_EOK注意:
size是邮件个数,不是字节数。每个邮件占 4 字节,所以size = 缓冲区总字节 / 4
// 示例:静态初始化,缓冲区 20 字节 → 可容纳 5 封邮件 static rt_uint32_t mb_pool[5]; static struct rt_mailbox mb; rt_mb_init(&mb, "mbt", &mb_pool[0], sizeof(mb_pool) / 4, RT_IPC_FLAG_FIFO);2.2 删除/脱离
// 删除动态创建的邮箱 rt_err_t rt_mb_delete(rt_mailbox_t mb); // 脱离静态初始化的邮箱 rt_err_t rt_mb_detach(rt_mailbox_t mb);2.3 发送邮件
RT-Thread 提供三个发送函数:
| 函数 | 说明 | 阻塞? |
|---|---|---|
rt_mb_send(mb, value) | 发送普通邮件(放到队尾) | ❌ 满时返回-RT_EFULL |
rt_mb_send_wait(mb, value, timeout) | 等待发送(满时阻塞) | ✅ 满时阻塞等待 |
/* 普通发送 */ rt_err_t rt_mb_send(rt_mailbox_t mb, rt_uint32_t value); /* 等待发送 */ rt_err_t rt_mb_send_wait(rt_mailbox_t mb, rt_uint32_t value, rt_int32_t timeout);参数value是rt_uint32_t类型(即 32 位无符号整数),可以传递:
整数值:如状态码、计数值
指针地址:如
(rt_uint32_t)ptr
2.4 接收邮件
rt_err_t rt_mb_recv(rt_mailbox_t mb, rt_uint32_t *value, // 输出参数:接收到的邮件值 rt_int32_t timeout);返回值:
RT_EOK成功 /-RT_ETIMEOUT超时 /-RT_ERROR错误
三、完整示例
本程序创建一个邮箱、1 个发送线程、1 个接收线程:
#include <rtthread.h> /* 邮箱控制块和缓冲区(静态方式) */ static struct rt_mailbox mb; static rt_uint32_t mb_pool[10]; // 可容纳 10 封邮件 /* 线程句柄 */ static rt_thread_t thread1 = RT_NULL; static rt_thread_t thread2 = RT_NULL; #define THREAD_PRIORITY 15 #define THREAD_STACK_SIZE 512 #define THREAD_TIMESLICE 10 /* 发送线程入口函数 */ static void thread1_entry(void *parameter) { rt_err_t result; int count = 0; while(1) { count++; /* 发送邮件:数值自增 */ result = rt_mb_send(&mb, (rt_ubase_t)count); if (result != RT_EOK) { rt_kprintf("rt_mb_send ERR\n"); } else { rt_kprintf("rt_mb_send:%d\n\r", count); } rt_thread_mdelay(10); } } /* 接收线程入口函数 */ static void thread2_entry(void *parameter) { int val; while(1) { /* 接收邮件(永久等待) */ if (rt_mb_recv(&mb, (rt_ubase_t *)&val, RT_WAITING_FOREVER) == RT_EOK) { rt_kprintf("rt_mb_recv:%d\n\r", val); } rt_thread_mdelay(5); } } int main(void) { rt_err_t result; /* 1. 初始化邮箱 * mb_pool[10] 共 40 字节,每个邮件 4 字节 * 容量 = 40 / 4 = 10 */ result = rt_mb_init(&mb, "mbt", &mb_pool[0], sizeof(mb_pool) / 4, // 邮箱容量 = 10 RT_IPC_FLAG_FIFO); // 先来先得 if (result != RT_EOK) { rt_kprintf("rt_mb_init ERR\n"); return -1; } /* 2. 创建发送线程(优先级 14,较高) */ thread1 = rt_thread_create("thread1", thread1_entry, RT_NULL, THREAD_STACK_SIZE, THREAD_PRIORITY - 1, THREAD_TIMESLICE); if (thread1 != RT_NULL) rt_thread_startup(thread1); /* 3. 创建接收线程(优先级 15) */ thread2 = rt_thread_create("thread2", thread2_entry, RT_NULL, THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE); if (thread2 != RT_NULL) rt_thread_startup(thread2); return 0; }运行结果:
注意:发送线程优先级更高(14),所以它先运行写入邮箱;当邮箱满或调用
mdelay(10)后挂起,接收线程(优先级 15)才有机会读出。
四、邮箱的使用技巧
4.1 通过邮箱传递指针
由于邮箱可以传递任意 32 位值,因此也可以用来传递指针:
/* 发送端 */ char *message = "Hello RT-Thread!"; rt_mb_send(&mb, (rt_uint32_t)message); // 把指针地址当作邮件发送 /* 接收端 */ char *msg; rt_mb_recv(&mb, (rt_uint32_t *)&msg, RT_WAITING_FOREVER); rt_kprintf("Received: %s\n", msg);⚠️ 确保指针指向的内存在线程生命周期内有效,不要在接收方拿到地址后原始内存已被释放。
4.2 邮箱满时的阻塞发送
使用rt_mb_send_wait()可以在邮箱满时阻塞等待:
/* 如果邮箱满了,等待 100 Tick(约 100ms),超时则返回 */ result = rt_mb_send_wait(&mb, 42, 100); if (result == RT_EOK) rt_kprintf("Send OK\n"); else if (result == -RT_ETIMEOUT) rt_kprintf("Send timeout, mailbox full\n");五、邮箱 vs 消息队列 选型指南
| 场景 | 推荐使用 | 原因 |
|---|---|---|
| 传递数值(状态码、计数值) | 邮箱 | 效率高,内存开销小 |
| 传递指针(大块数据) | 邮箱 | 直接传 4 字节地址 |
| 传递结构体(中等大小) | 消息队列 | 消息队列支持任意大小数据 |
| 传递不定长数据 | 消息队列 | 每个消息大小可不同 |
| 需要多个数据缓冲 | 消息队列 | 链表结构更灵活 |
| 实时性要求极高 | 邮箱 | 数组操作,无动态内存分配 |
六、总结
| 知识点 | 要点 |
|---|---|
| 邮箱本质 | 环形缓冲区(数组),每封邮件 4 字节 |
| 动态创建 | rt_mb_create()— 内存由内核分配 |
| 静态初始化 | rt_mb_init()— 需用户提供缓冲区,容量 = 总字节 / 4 |
| 发送邮件 | rt_mb_send()队尾 /rt_mb_urgent()队首紧急 |
| 等待发送 | rt_mb_send_wait()满时阻塞 |
| 接收邮件 | rt_mb_recv()— 支持超时等待 |
| 传递内容 | 32 位整数值 或 指针地址 |
| 唤醒策略 | RT_IPC_FLAG_PRIO(按优先级)/RT_IPC_FLAG_FIFO(按等待时间) |
| 删除/脱离 | 动态用rt_mb_delete(),静态用rt_mb_detach() |
| 适用场景 | 小数据(数值/指针)用邮箱,大数据(结构体)用消息队列 |