邮箱
邮箱
📚 本节导读
学习时长: 约 20 分钟
难度级别: ⭐⭐⭐☆☆
前置知识: 任务管理、信号量、消息队列
邮箱(Mailbox)是 OneOS 中一种轻量级的消息传递机制。与消息队列传递变长数据块不同,邮箱传递的是固定大小的值(os_ubase_t 类型),通常用于传递指针、句柄或整数值。由于不涉及数据拷贝,邮箱的效率比消息队列更高。
🎯 学习目标
- 理解邮箱与消息队列的核心区别
- 掌握邮箱的创建、发送和接收操作
- 了解邮箱适合的使用场景(指针传递)
- 能够使用邮箱实现高性能任务间通信
一、邮箱概念
1.1 什么是邮箱
邮箱(Mailbox)维护一个固定大小的环形缓冲区,每个槽位存储一个 os_ubase_t 类型的值(指针大小)。发送方将值写入邮箱,接收方从邮箱中取出。邮箱传递的是值本身(指针/整数值),不涉及数据拷贝,因此比消息队列更高效。
┌─────────────────────────────────────────────────────────────────┐
│ 邮箱模型 │
│ │
│ ┌──────────┐ send(value) ┌──────────────────────┐ │
│ │ 任务 A │ ────────────────→ │ 邮 箱 │ │
│ │(发送者) │ 发送指针值 │ ┌───┬───┬───┬───┐ │ │
│ └──────────┘ │ │ptr│ptr│ptr│ptr│ │ │
│ │ │ 1 │ 2 │ 3 │ 4 │ │ │
│ ┌──────────┐ recv(&value) │ └───┴───┴───┴───┘ │ │
│ │ 任务 B │ ←──────────────── │ │ │
│ │(接收者) │ 取出指针值 │ 每个槽位: os_ubase_t │ │
│ └──────────┘ └──────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘邮箱内部用"读指针/写指针"维护环形缓冲区:发送时写入 write_index 指向的槽位,接收时从 read_index 指向的槽位取出,两个索引到达容量上限后回绕到 0(见 2.5 的 _k_mb_modify_write_index / 2.6 的 _k_mb_modify_read_index)。
1.2 邮箱 vs 消息队列
| 特性 | 邮箱 | 消息队列 |
|---|---|---|
| 传递内容 | os_ubase_t 值(指针大小) | 任意大小的数据块 |
| 数据拷贝 | 否(只拷贝值本身) | 是(拷贝整个数据块) |
| 每个槽位大小 | 固定 sizeof(os_ubase_t) | 用户指定 msg_size |
| 性能 | 更高(无内存拷贝开销) | 有拷贝开销 |
| 典型用途 | 传递指针、句柄、整数值 | 传递数据块、结构体 |
| 内存池大小 | mail_count * sizeof(os_ubase_t) | 见消息队列内存池计算 |
| 等待队列 | 发送者 + 接收者各一条 | 发送者 + 接收者各一条 |
| 交换数据中转 | 都用 swap_data 字段 | 都用 swap_data 字段 |
1.3 邮箱使用场景
一句话核心:邮箱适用于任务之间高效传递一个"指针大小的值"(内存块指针、句柄、小型整数值)的场景。当要传的数据只是一个指针/整数,而不是一大段需要拷贝的数据时,用邮箱比消息队列更高效。
与消息队列的本质区别决定适用场景:
| 机制 | 传递的是什么 | 是否拷贝数据 | 适合传什么 |
|---|---|---|---|
| 消息队列 | 任意大小的数据块 | 是(拷贝整块数据) | 数据块、结构体、变长报文 |
| 邮箱 | os_ubase_t 值(指针/整数) | 否(只传值) | 指针、句柄、小整数值 |
典型场景一:传递内存块指针(最典型,性能关键)
发送方分配内存、填充数据,把指针通过邮箱发给接收方;接收方取到指针后直接读数据、处理完释放内存。数据不经过拷贝,只传一个指针,高效。
生产者任务 ──os_mailbox_send(ptr)──► [邮箱] ──os_mailbox_recv(&ptr)──► 消费者任务
malloc(item) (只传指针,不拷数据) 处理 item → free(item)为什么用邮箱:如果数据较大(如一张图片、一个结构体),用消息队列需要逐字节拷贝整个数据块,开销大;用邮箱只传一个指针(4/8 字节),接收方直接访问同一块内存,零拷贝。
典型场景二:传递句柄 / ID
把设备句柄、文件句柄、任务 ID 等标识值传给其他任务。这些值天然是指针大小的整数,用邮箱最合适。
为什么用邮箱:句柄本身就是 os_ubase_t 大小的值,无需拷贝,直接用邮箱传一个整数即可。
典型场景三:传递小型命令 / 事件编码
用简单的整数值编码命令或事件类型(如 CMD_START=1、CMD_STOP=2),比事件标志位(32 位位掩码,有位数限制)更灵活——邮箱的每个槽位可存任意 os_ubase_t 值,且支持多条排队。
为什么用邮箱:命令值小(一个整数),用邮箱传不拷贝、开销低;且邮箱有缓冲,命令可排队,生产者不会被阻塞太久。
什么时候不该用邮箱(选型边界)
| 场景 | 该用什么 | 为什么 |
|---|---|---|
| 要传一大块数据 | 消息队列(或传指针后共享内存) | 邮箱只能传指针/整数值,装不下数据块 |
| 只需要"发生事件"的信号 | 信号量 / 事件 | 邮箱还要管理槽位,更重 |
| 需要传多个 32 位标志位 | 事件 | 事件一次传多个 bit |
| 需要"排他访问共享资源" | 互斥锁 | 邮箱不做所有权/优先级继承 |
| 要传变长、长度不确定的报文 | 消息队列 | 邮箱槽位大小固定(os_ubase_t) |
结合 OneOS 源码的使用注意点
- 有界性:容量由
capacity决定(_k_mailbox_init计算:(池大小) / sizeof(os_ubase_t))。满了os_mailbox_send(..., OS_NO_WAIT)返回OS_FULL。 - 指针生命周期:传指针时,发送方分配的内存不能提前释放,否则接收方取到野指针。谁分配谁负责,配合
swap_data接力机制(见 2.5)。 - 中断上下文:
send/recv需要阻塞等待(timeout != OS_NO_WAIT)时不能在中断/关中断/锁调度中使用(源码OS_ASSERT限制)。 - 唤醒策略:
wake_type可选OS_MB_WAKE_TYPE_PRIO(按优先级)或OS_MB_WAKE_TYPE_FIFO。
1.4 邮箱控制块(os_mailbox_t)字段详解
邮箱的内部控制块 os_mailbox_t(即 struct os_mailbox)定义在 kernel/source/os_prototypes.h 中:
来源:kernel/source/os_prototypes.h
struct os_mailbox
{
void *mail_pool; /* The address that doesn't do alignment for mail pool */
void *mail_pool_align; /* Aligned address of mail pool. */
os_list_node_t send_task_list_head; /* Sender tasks blocked on this mailbox. */
os_list_node_t recv_task_list_head; /* Receiver tasks blocked on this mailbox. */
os_list_node_t resource_node; /* Node in resource list */
uint16_t capacity; /* Max number of mails for this mailbox. */
uint16_t used_mails; /* Numbers of mails into mailbox. */
uint16_t read_index; /* Read index of mail pool. */
uint16_t write_index; /* Write index of mail pool. */
uint8_t object_inited; /* Indicates whether object is inited or deinited, value is
OS_KOBJ_INITED or OS_KOBJ_DEINITED */
uint8_t object_alloc_type; /* Indicates whether object is allocated dynamically or statically,
value is OS_ALLOC_TYPE_STATIC or OS_KOBJ_ALLOC_TYPE_DYNAMIC */
uint8_t wake_type; /* The type to wake up blocking tasks, value is OS_MB_WAKE_TYPE_PRIO
or OS_MB_WAKE_TYPE_FIFO */
char name[OS_NAME_MAX + 1]; /* Mailbox name. */
os_spinlock_t lock; /* Spin lock used under SMP */
};
typedef struct os_mailbox os_mailbox_t;各字段含义如下:
| 字段 | 类型 | 说明 |
|---|---|---|
mail_pool | void * | 邮件池指针(未对齐的原始地址),仅用于销毁时释放内存 |
mail_pool_align | void * | 对齐后的邮件池地址,实际读写邮件用这个 |
send_task_list_head | os_list_node_t | 发送者等待队列:因邮箱满而阻塞的发送任务挂在这里 |
recv_task_list_head | os_list_node_t | 接收者等待队列:因邮箱空而阻塞的接收任务挂在这里 |
resource_node | os_list_node_t | 资源链表节点,把邮箱挂到全局资源链表(系统可查询/防重复) |
capacity | uint16_t | 邮箱容量(最多可容纳的邮件数/槽位数) |
used_mails | uint16_t | 已使用的邮件数(当前已存入的邮件数量) |
read_index | uint16_t | 读索引:接收时从该槽位取邮件 |
write_index | uint16_t | 写索引:发送时写入该槽位 |
object_inited | uint8_t | 是否已初始化(OS_KOBJ_INITED / OS_KOBJ_DEINITED) |
object_alloc_type | uint8_t | 分配类型(动态/静态),销毁时据此决定是否释放内存 |
wake_type | uint8_t | 唤醒策略(OS_MB_WAKE_TYPE_PRIO / OS_MB_WAKE_TYPE_FIFO) |
name | char[OS_NAME_MAX+1] | 邮箱名称 |
lock | os_spinlock_t | SMP 下保护邮箱操作的自旋锁 |
关键字段与核心概念的对应表:
| 邮箱概念 | 对应字段 |
|---|---|
| 邮件存储 | mail_pool_align(对齐后的邮件池) |
| 容量上限 | capacity |
| 当前邮件数 | used_mails |
| 环形读写位置 | read_index / write_index |
| 发送阻塞等待 | send_task_list_head(挂等待的发送者) |
| 接收阻塞等待 | recv_task_list_head(挂等待的接收者) |
| 系统管理/防重复 | resource_node(挂到全局资源链表) |
| 唤醒排序 | wake_type |
| 生命周期 | object_inited / object_alloc_type |
补充说明:不要直接访问邮箱内部字段(如 used_mails、read_index),应通过 os_mailbox_get_used_mails()、os_mailbox_recv() 等 API 操作。
邮箱的两个链表头(send_task_list_head / recv_task_list_head)是从哪来的?
这两个等待队列是struct os_mailbox 结构体的成员,属于每个邮箱自己(每个邮箱各有一对)。它们不是全局变量——全局资源链表头 gs_os_mb_resource_list_head 才是模块级的全局变量:
| 项目 | send_task_list_head / recv_task_list_head | gs_os_mb_resource_list_head |
|---|---|---|
| 属于谁 | struct os_mailbox 的字段(每个邮箱各 1 对) | os_mb.c 的 static 全局变量(整个系统 1 个) |
| 数量 | 每个邮箱各 1 对 | 全局 1 个 |
| 定义源码 | os_prototypes.h 的 struct os_mailbox 内 | os_mb.c:static os_list_node_t gs_os_mb_resource_list_head = OS_LIST_INIT(gs_os_mb_resource_list_head); |
| 用途 | 管理本邮箱的阻塞发送/接收任务 | 管理所有邮箱对象(挂 resource_node),供系统查询/防重复 |
定义源码(kernel/source/os_mb.c,与项目一致):
static os_list_node_t gs_os_mb_resource_list_head = OS_LIST_INIT(gs_os_mb_resource_list_head);
static OS_DEFINE_SPINLOCK(gs_os_mb_resource_list_lock);关键点:
- 强调"每种 IPC 对象各有独立资源链表"——邮箱
gs_os_mb_resource_list_head、消息队列gs_os_mq_resource_list_head、互斥锁gs_os_mutex_resource_list_head各不相同,不要笼统说"全局资源链表"。 - 区分:全局链表头(系统 1 个,管理所有同类对象) vs 结构体内链表头(每个邮箱各 1 对,管理自己的阻塞任务)。
1.5 邮箱与多个链表的整体关系图
邮箱涉及多条链表,应在进入 API 前建立整体认知。以下是邮箱与各链表的整体关系图(含资源链表 + 发送者等待队列 + 接收者等待队列):
【链表 1】邮箱资源链表(系统全局 1 个,存于 os_mb.c 静态全局变量)
gs_os_mb_resource_list_head → 邮箱A.resource_node → 邮箱B.resource_node → ...
│
└─ 所有邮箱创建时挂入,销毁时摘除(供系统查询/防重复创建)
【链表 2】发送者等待队列(每个邮箱 1 条,存于 struct os_mailbox 内)
邮箱X.send_task_list_head → 发送任务 S1 → 发送任务 S2
│
│ (邮箱满时,等待发送的 S1/S2 被 os_mailbox_send 阻塞挂到这里)
└─ send_task_list_head 是 os_mailbox_t 的字段
【链表 3】接收者等待队列(每个邮箱 1 条,存于 struct os_mailbox 内)
邮箱X.recv_task_list_head → 接收任务 R1 → 接收任务 R2
│
│ (邮箱空时,等待接收的 R1/R2 被 os_mailbox_recv 阻塞挂到这里)
└─ recv_task_list_head 是 os_mailbox_t 的字段邮箱对象与自身字段的关系:
┌─────────────────── os_mailbox_t 邮箱X ───────────────────┐
│ │
│ resource_node ═══► 挂到全局资源链表(创建时) │
│ send_task_list_head ═══► 挂阻塞的发送任务(发送时满) │
│ recv_task_list_head ═══► 挂阻塞的接收任务(接收时空) │
│ used_mails = capacity(满)或 0(空) │
│ read_index / write_index 指示环形槽位读写位置 │
└──────────────────────────────────────────────────────────┘核心关系表:
| 链表 | 属于谁 | 挂哪个节点 | 何时挂入 |
|---|---|---|---|
邮箱资源链表(gs_os_mb_resource_list_head) | 邮箱模块(全局 1 个) | resource_node | 创建时 |
发送者等待队列(send_task_list_head) | 邮箱自己 | 阻塞的发送任务 | 发送时邮箱满(os_mailbox_send) |
接收者等待队列(recv_task_list_head) | 邮箱自己 | 阻塞的接收任务 | 接收时邮箱空(os_mailbox_recv) |
一句话:
resource_node让邮箱"被系统管理"(全局可见,可查询/防重复)。send_task_list_head记录"哪些发送者因邮箱满在等空闲槽位"(有槽位时唤醒)。recv_task_list_head记录"哪些接收者因邮箱空在等邮件"(有邮件时唤醒)。
二、API 详解
以下 API 定义在 kernel/include/os_mb.h 中:
#include <os_mb.h>注意:使用邮箱需要在
oneos_config.h中启用OS_USING_MAILBOX宏。动态创建还需要OS_USING_HEAP。
2.1 静态定义宏
静态创建邮箱时,需要用户提供控制块和邮件池。这些通过 os_mb.h 提供的宏来定义。完整宏定义(kernel/include/os_mb.h,与项目一致,含原始英文注释):
/*
* Here, actually, it is an array of mailbox control structure size.
* The purpose is to serve as a mailbox control block for
* static create of mailbox.
*/
#define OS_MAILBOX_DEFINE(name) os_mailbox_dummy_t name
/*
* OS_MB_POOL_DEFINE is used to define mailbox's memory pool and the address is aligned.
* OS_MB_POOL_ADDR is used to get address of mailbox's memory pool.
* OS_MB_POOL_SIZE is used to get size of mailbox's memory pool.
*
* These macros are used when creating a mailbox with the function os_mb_init().
*/
#define OS_MAILBOX_POOL_DEFINE(name, mail_count) os_ubase_t pool_##name[mail_count]
#define OS_MAILBOX_POOL_ADDR(name) ((void *)pool_##name)
#define OS_MAILBOX_POOL_SIZE(name) sizeof(pool_##name)各宏详细解释:
OS_MAILBOX_DEFINE(name) —— 定义邮箱控制块
#define OS_MAILBOX_DEFINE(name) os_mailbox_dummy_t name- 展开为一条变量声明:
os_mailbox_dummy_t name。 os_mailbox_dummy_t是给用户看的占位类型(typedef struct dummy_mailbox),它的内存布局、大小与内核真正的os_mailbox_t完全一致。- 用户用它定义一个全局变量
name,这块静态内存将来被内核当作真正的邮箱控制块使用。 - 用法示例:
OS_MAILBOX_DEFINE(g_work_mb); /* 展开为:os_mailbox_dummy_t g_work_mb; */OS_MAILBOX_POOL_DEFINE(name, mail_count) —— 定义邮件池
#define OS_MAILBOX_POOL_DEFINE(name, mail_count) os_ubase_t pool_##name[mail_count]- 展开为一个全局数组:
os_ubase_t pool_##name[mail_count]。 - 每个元素是
os_ubase_t(指针大小),共mail_count个槽位,用于存邮件(指针/整数值)。 - 用法示例:
OS_MAILBOX_POOL_DEFINE(g_work_mb, 16); /* 展开为:os_ubase_t pool_g_work_mb[16]; */OS_MAILBOX_POOL_ADDR(name) —— 获取邮件池地址
#define OS_MAILBOX_POOL_ADDR(name) ((void *)pool_##name)- 返回邮件池数组的起始地址(
(void *)pool_name),供os_mailbox_create_static()的mail_pool参数使用。
OS_MAILBOX_POOL_SIZE(name) —— 获取邮件池大小
#define OS_MAILBOX_POOL_SIZE(name) sizeof(pool_##name)- 返回邮件池数组的大小(
sizeof(pool_name)),供os_mailbox_create_static()的mail_pool_size参数使用。由mail_count和os_ubase_t大小决定,如16 * sizeof(os_ubase_t)。
2.2 动态创建
os_mailbox_id os_mailbox_create_dynamic(const char *name, os_size_t max_mails);| 参数 | 说明 |
|---|---|
name | 邮箱名称 |
max_mails | 邮箱能够容纳的最大邮件数(槽位数) |
| 返回值 | 成功返回邮箱 ID,失败返回 OS_NULL |
需要启用
OS_USING_HEAP。
完整源码(kernel/source/os_mb.c,与项目一致):
os_mailbox_id os_mailbox_create_dynamic(const char *name, os_size_t max_mails)
{
os_mailbox_t *mb;
void *mail_pool;
os_size_t mail_pool_size;
os_err_t ret;
OS_ASSERT(max_mails > 0U);
OS_ASSERT(OS_FALSE == os_is_irq_active());
mail_pool_size = max_mails * sizeof(os_ubase_t);
mb = (os_mailbox_t *)OS_KERNEL_MALLOC(sizeof(os_mailbox_t));
mail_pool = OS_KERNEL_MALLOC_ALIGN(sizeof(os_ubase_t), mail_pool_size);
if ((OS_NULL == mb) || (OS_NULL == mail_pool))
{
OS_KERN_LOG(KERN_ERROR, MB_TAG, "Malloc failed, mb(%p), mail_pool(%p)", mb, mail_pool);
if (OS_NULL != mb)
{
OS_KERNEL_FREE(mb);
mb = OS_NULL;
}
if (OS_NULL != mail_pool)
{
OS_KERNEL_FREE(mail_pool);
mail_pool = OS_NULL;
}
}
else
{
ret = _k_mailbox_init(mb, name, mail_pool, mail_pool_size, OS_KOBJ_ALLOC_TYPE_DYNAMIC);
if (OS_SUCCESS == ret)
{
os_spin_lock(&gs_os_mb_resource_list_lock);
os_list_add_tail(&gs_os_mb_resource_list_head, &mb->resource_node);
os_spin_unlock(&gs_os_mb_resource_list_lock);
mb->object_inited = OS_KOBJ_INITED;
}
else
{
OS_KERNEL_FREE(mb);
OS_KERNEL_FREE(mail_pool);
mb = OS_NULL;
mail_pool = OS_NULL;
}
}
return OS_TYPE_CONVERT(os_mailbox_id, mb);
}分步详解:
第一步:参数校验(条件:max_mails > 0,且不能在中断上下文)—— 不满足则断言崩溃
OS_ASSERT(max_mails > 0U);
OS_ASSERT(OS_FALSE == os_is_irq_active());进入本步的条件:调用 os_mailbox_create_dynamic() 时无条件执行(本步是入口硬性校验,不满足条件直接断言崩溃,不返回错误码)。
详细解释:
OS_ASSERT(max_mails > 0U):条件max_mails > 0——邮箱容量必须非 0,否则无法容纳任何邮件。OS_ASSERT(OS_FALSE == os_is_irq_active()):条件os_is_irq_active() == OS_FALSE——不能在中断上下文创建(OS_KERNEL_MALLOC需要进程上下文)。
关键点:这两条是断言,失败即崩溃而非返回错误码,调用方必须保证参数合法。
第二步:计算内存池大小并分配内存(条件:第一步断言通过)—— 分配控制块 + 对齐邮件池
mail_pool_size = max_mails * sizeof(os_ubase_t);
mb = (os_mailbox_t *)OS_KERNEL_MALLOC(sizeof(os_mailbox_t));
mail_pool = OS_KERNEL_MALLOC_ALIGN(sizeof(os_ubase_t), mail_pool_size);进入本步的条件:第一步两条断言全部通过(max_mails > 0、非中断上下文)。
详细解释:
mail_pool_size = max_mails * sizeof(os_ubase_t):邮件池总大小 = 槽位数 × 每个槽位大小(每个槽位存一个os_ubase_t)。OS_KERNEL_MALLOC(sizeof(os_mailbox_t)):分配控制块mb。OS_KERNEL_MALLOC_ALIGN(sizeof(os_ubase_t), mail_pool_size):分配按os_ubase_t对齐的邮件池mail_pool。
关键点:mb 和 mail_pool 都可能分配失败(返回 OS_NULL),这是下一步判断的来源。
第三步:分配失败处理 —— 回滚(条件:mb == OS_NULL 或 mail_pool == OS_NULL)—— 释放已分配部分
if ((OS_NULL == mb) || (OS_NULL == mail_pool))
{
OS_KERN_LOG(KERN_ERROR, MB_TAG, "Malloc failed, mb(%p), mail_pool(%p)", mb, mail_pool);
if (OS_NULL != mb)
{
OS_KERNEL_FREE(mb);
mb = OS_NULL;
}
if (OS_NULL != mail_pool)
{
OS_KERNEL_FREE(mail_pool);
mail_pool = OS_NULL;
}
}进入本步的条件:mb == OS_NULL 或 mail_pool == OS_NULL(第二步至少一个分配失败,内存不足)。
详细解释:
- 打印错误日志
Malloc failed,报告mb和mail_pool的实际地址。 - 子分支 A:条件
mb != OS_NULL(控制块分配成功但邮件池失败)→ 释放控制块并置OS_NULL。 - 子分支 B:条件
mail_pool != OS_NULL(邮件池分配成功但控制块失败)→ 释放邮件池并置OS_NULL。 - 注意:这里不设置
ret,因为本分支走完后mb已为OS_NULL,最终返回值自然是OS_NULL。
关键点:这是典型的部分成功回滚——两个资源只分配成功一个时,必须把已成功的也释放,避免内存泄漏。
第四步:初始化并加入资源链表(条件:分配全部成功,mb != OS_NULL 且 mail_pool != OS_NULL)—— 初始化 + 登记资源链表 + 标记已初始化
else
{
ret = _k_mailbox_init(mb, name, mail_pool, mail_pool_size, OS_KOBJ_ALLOC_TYPE_DYNAMIC);
if (OS_SUCCESS == ret)
{
os_spin_lock(&gs_os_mb_resource_list_lock);
os_list_add_tail(&gs_os_mb_resource_list_head, &mb->resource_node);
os_spin_unlock(&gs_os_mb_resource_list_lock);
mb->object_inited = OS_KOBJ_INITED;
}
else
{
OS_KERNEL_FREE(mb);
OS_KERNEL_FREE(mail_pool);
mb = OS_NULL;
mail_pool = OS_NULL;
}
}
return OS_TYPE_CONVERT(os_mailbox_id, mb);进入本步的条件:第三步的条件不成立,即 mb != OS_NULL 且 mail_pool != OS_NULL(两个资源都分配成功),走到 else 分支。
详细解释:
_k_mailbox_init(mb, name, mail_pool, mail_pool_size, OS_KOBJ_ALLOC_TYPE_DYNAMIC):初始化控制块各字段,返回ret。注意分配类型为OS_KOBJ_ALLOC_TYPE_DYNAMIC(销毁时据此决定是否释放内存)。- 子分支 A:条件
ret == OS_SUCCESS(初始化成功):os_list_add_tail(&gs_os_mb_resource_list_head, &mb->resource_node):把邮箱的resource_node登记进全局邮箱资源链表;mb->object_inited = OS_KOBJ_INITED:标记已初始化。
- 子分支 B:条件
ret != OS_SUCCESS(初始化失败):释放控制块和邮件池,mb、mail_pool置OS_NULL(回滚,不留下半初始化对象)。
最终返回值:return OS_TYPE_CONVERT(os_mailbox_id, mb)——成功返回邮箱 ID;失败时 mb 已被置 OS_NULL,故返回 OS_NULL。
使用示例:
os_mailbox_id mb_id = os_mailbox_create_dynamic("my_mb", 10); /* 容量 10 封邮件 */
if (mb_id == OS_NULL)
{
os_kprintf("create failed\r\n"); /* 检查返回值 */
return;
}2.3 静态创建
os_mailbox_id os_mailbox_create_static(os_mailbox_dummy_t *mailbox_cb,
const char *name,
void *mail_pool,
os_size_t mail_pool_size);| 参数 | 说明 |
|---|---|
mailbox_cb | 邮箱控制块指针(OS_MAILBOX_DEFINE 定义的变量地址) |
name | 邮箱名称 |
mail_pool | 邮件池地址(OS_MAILBOX_POOL_ADDR 获取) |
mail_pool_size | 邮件池大小(OS_MAILBOX_POOL_SIZE 获取) |
| 返回值 | 成功返回邮箱 ID,失败返回 OS_NULL |
完整源码(kernel/source/os_mb.c,与项目一致):
os_mailbox_id
os_mailbox_create_static(os_mailbox_dummy_t *mailbox_cb, const char *name, void *mail_pool, os_size_t mail_pool_size)
{
os_mailbox_t *iter_mb;
os_list_node_t *pos;
os_err_t ret;
os_mailbox_t *mb;
mb = OS_TYPE_CONVERT(os_mailbox_t *, mailbox_cb);
OS_ASSERT(OS_NULL != mb);
OS_ASSERT(OS_NULL != mail_pool);
OS_ASSERT(mail_pool_size > 0);
OS_ASSERT(OS_FALSE == os_is_irq_active());
os_spin_lock(&gs_os_mb_resource_list_lock);
os_list_for_each(pos, &gs_os_mb_resource_list_head)
{
iter_mb = os_list_entry(pos, os_mailbox_t, resource_node);
if (iter_mb == mb)
{
OS_KERN_LOG(KERN_ERROR, MB_TAG, "The mb(addr: %p, name: %s) has been exist", iter_mb, iter_mb->name);
mb = OS_NULL;
break;
}
}
if (OS_NULL != mb)
{
os_list_add_tail(&gs_os_mb_resource_list_head, &mb->resource_node);
os_spin_unlock(&gs_os_mb_resource_list_lock);
ret = _k_mailbox_init(mb, name, mail_pool, mail_pool_size, OS_ALLOC_TYPE_STATIC);
if (OS_SUCCESS == ret)
{
mb->object_inited = OS_KOBJ_INITED;
}
else
{
os_spin_lock(&gs_os_mb_resource_list_lock);
os_list_del(&mb->resource_node);
os_spin_unlock(&gs_os_mb_resource_list_lock);
mb = OS_NULL;
}
}
else
{
os_spin_unlock(&gs_os_mb_resource_list_lock);
}
return OS_TYPE_CONVERT(os_mailbox_id, mb);
}分步详解:
第一步:参数校验(条件:控制块/邮件池非空、mail_pool_size > 0、非中断上下文)—— 不满足则断言崩溃
OS_ASSERT(OS_NULL != mb);
OS_ASSERT(OS_NULL != mail_pool);
OS_ASSERT(mail_pool_size > 0);
OS_ASSERT(OS_FALSE == os_is_irq_active());进入本步的条件:调用 os_mailbox_create_static() 时无条件执行(本步是入口硬性校验,不满足条件直接断言崩溃)。
详细解释:
mb != OS_NULL:控制块必须非空(用户用OS_MAILBOX_DEFINE定义的变量地址)。mail_pool != OS_NULL:邮件池必须非空(用户用OS_MAILBOX_POOL_DEFINE定义的数组)。mail_pool_size > 0:邮件池大小必须非 0。os_is_irq_active() == OS_FALSE:不能在中断上下文创建。
关键点:与动态创建不同,静态创建不分配内存(控制块、邮件池都由用户提供),所以本步校验的是用户传入的指针/大小合法性。
第二步:防重复创建检查(条件:第一步断言通过)—— 遍历资源链表,已存在则置 mb = OS_NULL
os_spin_lock(&gs_os_mb_resource_list_lock);
os_list_for_each(pos, &gs_os_mb_resource_list_head)
{
iter_mb = os_list_entry(pos, os_mailbox_t, resource_node);
if (iter_mb == mb)
{
OS_KERN_LOG(KERN_ERROR, MB_TAG, "The mb(addr: %p, name: %s) has been exist", iter_mb, iter_mb->name);
mb = OS_NULL;
break;
}
}进入本步的条件:第一步四条断言全部通过(参数合法)。
详细解释:
- 加资源链表锁
gs_os_mb_resource_list_lock(保护共享链表,防止并发创建竞争)。 os_list_for_each(pos, &gs_os_mb_resource_list_head):遍历整个邮箱资源链表,逐个取出iter_mb比较。- 子分支:条件
iter_mb == mb(发现同一控制块地址已在链表中,即重复创建):- 打印错误日志
has been exist; - 置
mb = OS_NULL并break——用"置空"标记"此邮箱已存在、不可再创建"。
- 打印错误日志
关键点:静态创建需要防重复,是因为静态控制块内存由用户提供、地址固定。同一个 mailbox_cb 地址被创建两次,会重复登记进资源链表、重复初始化同一块内存,造成链表错乱。动态创建每次 malloc 新地址,天然不重复,故无此检查。
第三步:登记并初始化(条件:mb != OS_NULL,即未重复创建)—— 登记链表 + 初始化 + 标记已初始化
if (OS_NULL != mb)
{
os_list_add_tail(&gs_os_mb_resource_list_head, &mb->resource_node);
os_spin_unlock(&gs_os_mb_resource_list_lock);
ret = _k_mailbox_init(mb, name, mail_pool, mail_pool_size, OS_ALLOC_TYPE_STATIC);
if (OS_SUCCESS == ret)
{
mb->object_inited = OS_KOBJ_INITED;
}
else
{
os_spin_lock(&gs_os_mb_resource_list_lock);
os_list_del(&mb->resource_node);
os_spin_unlock(&gs_os_mb_resource_list_lock);
mb = OS_NULL;
}
}
else
{
os_spin_unlock(&gs_os_mb_resource_list_lock);
}
return OS_TYPE_CONVERT(os_mailbox_id, mb);进入本步的条件:第二步未发现重复,即 mb != OS_NULL(else 分支是重复时仅释放锁)。
详细解释:
os_list_add_tail(&gs_os_mb_resource_list_head, &mb->resource_node):把邮箱登记进邮箱资源链表(此时仍持锁)。os_spin_unlock(&gs_os_mb_resource_list_lock):登记完释放锁。ret = _k_mailbox_init(mb, name, mail_pool, mail_pool_size, OS_ALLOC_TYPE_STATIC):初始化控制块(分配类型OS_ALLOC_TYPE_STATIC,与动态的OS_KOBJ_ALLOC_TYPE_DYNAMIC区分,销毁时据此决定是否释放内存)。- 子分支 A:条件
ret == OS_SUCCESS:mb->object_inited = OS_KOBJ_INITED标记已初始化。 - 子分支 B:条件
ret != OS_SUCCESS:os_list_del(&mb->resource_node)从资源链表移除该邮箱(回滚),并把mb置OS_NULL。
最终返回值:return OS_TYPE_CONVERT(os_mailbox_id, mb)——成功返回邮箱 ID;重复创建或初始化失败时 mb == OS_NULL,返回 OS_NULL。
使用示例:
OS_MAILBOX_DEFINE(g_work_mb);
OS_MAILBOX_POOL_DEFINE(g_work_mb, 16);
os_mailbox_id mb_id = os_mailbox_create_static(&g_work_mb,
"work_mb",
OS_MAILBOX_POOL_ADDR(g_work_mb),
OS_MAILBOX_POOL_SIZE(g_work_mb));
if (mb_id == OS_NULL)
{
os_kprintf("create failed\r\n");
return;
}2.4 销毁邮箱
os_err_t os_mailbox_destroy(os_mailbox_id mailbox_id);| 参数 | 说明 |
|---|---|
mailbox_id | 要销毁的邮箱 ID |
| 返回值 | 成功返回 OS_SUCCESS |
销毁需要当前不在中断上下文。
完整源码(kernel/source/os_mb.c,与项目一致):
os_err_t os_mailbox_destroy(os_mailbox_id mailbox_id)
{
os_bool_t need_sched;
os_mailbox_t *mb;
OS_KERNEL_INIT();
mb = OS_TYPE_CONVERT(os_mailbox_t *, mailbox_id);
OS_ASSERT(OS_NULL != mb);
OS_ASSERT(OS_KOBJ_INITED == mb->object_inited);
OS_ASSERT(OS_FALSE == os_is_irq_active());
#ifndef OS_USING_HEAP
OS_ASSERT(OS_ALLOC_TYPE_STATIC == mb->object_alloc_type);
#endif
need_sched = OS_FALSE;
#if defined(OS_USING_SMP) && !defined(OS_IPC_MONOLOCK_MODE)
os_spin_lock_irq(&mb->lock, &irq_save);
#else
OS_KERNEL_ENTER();
#endif
mb->object_inited = OS_KOBJ_DEINITED;
if (OS_TRUE == k_cancel_all_blocked_task(&mb->send_task_list_head))
{
need_sched = OS_TRUE;
}
if (OS_TRUE == k_cancel_all_blocked_task(&mb->recv_task_list_head))
{
need_sched = OS_TRUE;
}
#if defined(OS_USING_SMP) && !defined(OS_IPC_MONOLOCK_MODE)
os_spin_unlock_irq(&mb->lock, irq_save);
if (OS_TRUE == need_sched)
{
OS_KERNEL_ENTER();
OS_KERNEL_EXIT_SCHED();
}
#else
if (OS_TRUE == need_sched)
{
OS_KERNEL_EXIT_SCHED();
}
else
{
OS_KERNEL_EXIT();
}
#endif
os_spin_lock(&gs_os_mb_resource_list_lock);
os_list_del(&mb->resource_node);
os_spin_unlock(&gs_os_mb_resource_list_lock);
#ifdef OS_USING_HEAP
if (OS_KOBJ_ALLOC_TYPE_DYNAMIC == mb->object_alloc_type)
{
OS_KERNEL_FREE(mb->mail_pool);
mb->mail_pool = OS_NULL;
OS_KERNEL_FREE(mb);
mb = OS_NULL;
}
#endif
return OS_SUCCESS;
}分步详解:
第一步:准备与参数校验(条件:邮箱非空、已初始化、非中断上下文、非堆模式下必须静态分配)—— 不满足则断言崩溃
OS_ASSERT(OS_NULL != mb);
OS_ASSERT(OS_KOBJ_INITED == mb->object_inited);
OS_ASSERT(OS_FALSE == os_is_irq_active());
#ifndef OS_USING_HEAP
OS_ASSERT(OS_ALLOC_TYPE_STATIC == mb->object_alloc_type);
#endif进入本步的条件:调用 os_mailbox_destroy() 时无条件执行(本步是入口硬性校验,不满足条件直接断言崩溃,不返回错误码)。
详细解释:
mb != OS_NULL:要销毁的邮箱必须非空。object_inited == OS_KOBJ_INITED:邮箱必须是"已初始化"状态,防重复销毁(销毁后object_inited变DEINITED,再次销毁会断言崩溃)。os_is_irq_active() == OS_FALSE:不能在中断中销毁(可能触发调度、释放内存)。OS_ALLOC_TYPE_STATIC == object_alloc_type:条件(仅当未启用堆OS_USING_HEAP时编译)——未启用堆时只能销毁静态创建的邮箱(动态创建需OS_KERNEL_FREE,无堆则无法释放)。
第二步:标记去初始化并唤醒所有阻塞任务(条件:第一步断言通过)—— 标记 DEINITED + 唤醒所有阻塞的收发任务
mb->object_inited = OS_KOBJ_DEINITED;
if (OS_TRUE == k_cancel_all_blocked_task(&mb->send_task_list_head))
{
need_sched = OS_TRUE;
}
if (OS_TRUE == k_cancel_all_blocked_task(&mb->recv_task_list_head))
{
need_sched = OS_TRUE;
}进入本步的条件:第一步断言全部通过(邮箱合法、已初始化)。
详细解释:
mb->object_inited = OS_KOBJ_DEINITED:先把邮箱标记为"去初始化"——防止销毁过程中其他任务(如send/recv)误操作正在销毁的邮箱。- 子分支 A:条件
send_task_list_head上有阻塞任务 →k_cancel_all_blocked_task唤醒所有因邮箱满而阻塞的发送者,返回OS_TRUE则need_sched = OS_TRUE。 - 子分支 B:条件
recv_task_list_head上有阻塞任务 → 唤醒所有因邮箱空而阻塞的接收者,返回OS_TRUE则need_sched = OS_TRUE。
关键点:销毁前必须唤醒所有仍阻塞的收发任务,否则这些任务会永远挂在一个已销毁邮箱的链表上,造成任务永久挂死。
第三步:触发调度 + 移除资源链表 + 释放内存(条件:取决于 need_sched 和 object_alloc_type)—— 收尾清理
if (OS_TRUE == need_sched)
{
OS_KERNEL_EXIT_SCHED();
}
else
{
OS_KERNEL_EXIT();
}
os_spin_lock(&gs_os_mb_resource_list_lock);
os_list_del(&mb->resource_node);
os_spin_unlock(&gs_os_mb_resource_list_lock);
#ifdef OS_USING_HEAP
if (OS_KOBJ_ALLOC_TYPE_DYNAMIC == mb->object_alloc_type)
{
OS_KERNEL_FREE(mb->mail_pool);
mb->mail_pool = OS_NULL;
OS_KERNEL_FREE(mb);
mb = OS_NULL;
}
#endif
return OS_SUCCESS;进入本步的条件:第二步已执行完,本步为收尾清理,按两个子条件分别处理。
详细解释:
- 子条件 A:
need_sched == OS_TRUE(第二步唤醒了阻塞任务)→OS_KERNEL_EXIT_SCHED()触发一次调度,让被唤醒的任务有机会运行;否则(need_sched == OS_FALSE,无唤醒)→ 仅OS_KERNEL_EXIT()退出临界区。 os_list_del(&mb->resource_node):加资源链表锁,把该邮箱的resource_node从邮箱资源链表移除(销毁后全局不可再查询到它)。- 子条件 B:
mb->object_alloc_type == OS_KOBJ_ALLOC_TYPE_DYNAMIC(该邮箱是动态创建的)→ 释放mb->mail_pool(邮件池)和mb(控制块),并置空;静态创建的邮箱不释放(控制块、邮件池都由用户静态提供,不归内核管理,释放会造成非法 free)。 - 返回
OS_SUCCESS。
关键点:动态 vs 静态创建的销毁差异——只有动态创建的才 OS_KERNEL_FREE;静态创建的不 free 用户提供的内存。
使用示例:
os_err_t ret = os_mailbox_destroy(mb_id);
if (ret != OS_EOK)
{
os_kprintf("destroy failed\r\n");
}2.5 发送邮件
os_err_t os_mailbox_send(os_mailbox_id mailbox_id, os_ubase_t value, os_tick_t timeout);| 参数 | 说明 |
|---|---|
mailbox_id | 邮箱 ID |
value | 要发送的邮件值(指针或整数值) |
timeout | 超时时间。邮箱满时等待空闲槽位的时间 |
| 返回值 | 成功返回 OS_EOK;OS_EFULL 邮箱满且不等待;OS_ETIMEOUT 超时 |
完整源码(kernel/source/os_mb.c,与项目一致):
os_err_t os_mailbox_send(os_mailbox_id mailbox_id, os_ubase_t value, os_tick_t timeout)
{
os_task_t *current_task;
os_task_t *block_task;
os_bool_t need_sched;
os_err_t ret;
os_mailbox_t *mb;
mb = OS_TYPE_CONVERT(os_mailbox_t *, mailbox_id);
OS_KERNEL_INIT();
OS_ASSERT(OS_NULL != mb);
OS_ASSERT(OS_KOBJ_INITED == mb->object_inited);
OS_ASSERT((OS_NO_WAIT == timeout) || (OS_NULL == k_task_self()) || (OS_FALSE == os_is_irq_active()));
OS_ASSERT((OS_NO_WAIT == timeout) || (OS_NULL == k_task_self()) || (OS_FALSE == os_is_irq_disabled()));
OS_ASSERT((OS_NO_WAIT == timeout) || (OS_NULL == k_task_self()) || (OS_FALSE == os_is_schedule_locked()));
OS_ASSERT((timeout < (OS_TICK_MAX / 2)) || (OS_WAIT_FOREVER == timeout));
ret = OS_SUCCESS;
need_sched = OS_FALSE;
#if defined(OS_USING_SMP) && !defined(OS_IPC_MONOLOCK_MODE)
os_spin_lock_irq(&mb->lock, &irq_save);
k_kernel_spin_lock();
#else
OS_KERNEL_ENTER();
#endif
if (!os_list_empty(&mb->recv_task_list_head))
{
/*
* If the receiving task is blocked, use swap_data of blocked task to transfer mail.
* So, when the receiving task is awakened, there is no need to get the mail from
* the mail pool of mailbox.
*/
block_task = os_list_first_entry(&mb->recv_task_list_head, os_task_t, task_node);
need_sched = k_unblock_task(block_task);
#if defined(OS_USING_SMP) && !defined(OS_IPC_MONOLOCK_MODE)
k_kernel_spin_unlock();
#endif
block_task->swap_data = value;
}
else
{
#if defined(OS_USING_SMP) && !defined(OS_IPC_MONOLOCK_MODE)
k_kernel_spin_unlock();
#endif
if (mb->used_mails == mb->capacity)
{
if (OS_NO_WAIT == timeout)
{
ret = OS_FULL;
}
else
{
/*
* If the mailbox is full, use swap_data of current task to store the mail.
* When the receiving task receive a mail, it will put this mail into mail
* pool of mailbox.
*/
current_task = _k_task_self();
OS_ASSERT((OS_NULL != current_task));
current_task->swap_data = value;
/*
* If timeout, mailbox is destroyed or other abnormal wake-up, ret is not OS_SUCCESS
* and send mail failed. Otherwise, the mail has been put into mail pool of mailbox
* by the receiving task.
*/
if (OS_MB_WAKE_TYPE_PRIO == mb->wake_type)
{
ret = k_block_task(&mb->lock, irq_save, &mb->send_task_list_head, timeout, OS_TRUE);
}
else
{
ret = k_block_task(&mb->lock, irq_save, &mb->send_task_list_head, timeout, OS_FALSE);
}
return ret;
}
}
else
{
/*
* The mailbox is not full and there is no blocked task, put the mail info the mail
* pool of mailbox
*/
*((os_ubase_t *)mb->mail_pool_align + mb->write_index) = value;
_k_mb_modify_write_index(mb);
}
}
#if defined(OS_USING_SMP) && !defined(OS_IPC_MONOLOCK_MODE)
os_spin_unlock_irq(&mb->lock, irq_save);
if (OS_TRUE == need_sched)
{
OS_KERNEL_ENTER();
OS_KERNEL_EXIT_SCHED();
}
#else
if (OS_TRUE == need_sched)
{
OS_KERNEL_EXIT_SCHED();
}
else
{
OS_KERNEL_EXIT();
}
#endif
return ret;
}辅助函数:_k_mb_modify_write_index() —— 写索引推进 + 邮件数加一
发送时把邮件写入邮件池后,调用它推进写索引:
static void _k_mb_modify_write_index(os_mailbox_t *mb)
{
mb->write_index++;
if (mb->write_index >= mb->capacity)
{
mb->write_index = 0U;
}
mb->used_mails++;
return;
}- 条件
write_index >= capacity:写索引到达容量上限 → 回绕到 0(环形缓冲区)。 used_mails++:已用邮件数加一。
分步详解:
第一步:准备与参数校验(条件:邮箱非空、已初始化、等待条件合法)—— 不满足则断言崩溃
OS_ASSERT(OS_NULL != mb);
OS_ASSERT(OS_KOBJ_INITED == mb->object_inited);
OS_ASSERT((OS_NO_WAIT == timeout) || (OS_NULL == k_task_self()) || (OS_FALSE == os_is_irq_active()));
OS_ASSERT((OS_NO_WAIT == timeout) || (OS_NULL == k_task_self()) || (OS_FALSE == os_is_irq_disabled()));
OS_ASSERT((OS_NO_WAIT == timeout) || (OS_NULL == k_task_self()) || (OS_FALSE == os_is_schedule_locked()));
OS_ASSERT((timeout < (OS_TICK_MAX / 2)) || (OS_WAIT_FOREVER == timeout));进入本步的条件:调用 os_mailbox_send() 时无条件执行(本步是入口硬性校验,不满足条件直接断言崩溃)。
详细解释:
mb != OS_NULL:邮箱非空。object_inited == OS_KOBJ_INITED:邮箱已初始化。- 三条"等待条件"断言:需要阻塞等待(
timeout != OS_NO_WAIT)时,必须有当前任务、不在中断、未关中断、未锁调度。 timeout < OS_TICK_MAX/2 || timeout == OS_WAIT_FOREVER:超时值合法。
第二步:有接收者阻塞(条件:recv_task_list_head 非空)—— 直接把邮件转交给队首接收者
if (!os_list_empty(&mb->recv_task_list_head))
{
block_task = os_list_first_entry(&mb->recv_task_list_head, os_task_t, task_node);
need_sched = k_unblock_task(block_task);
block_task->swap_data = value;
}进入本步的条件:recv_task_list_head 非空(有接收者因邮箱空而阻塞等待)。
- 取队首接收者
block_task(PRIO 取最高优先级,FIFO 取最先等待)。 k_unblock_task(block_task):唤醒它,返回need_sched。block_task->swap_data = value:把要发送的邮件值直接写入接收者的swap_data字段。
关键点:与消息队列的 swap_data 接力一致——有接收者在等时,邮件不入池,直接通过接收者的 swap_data 转交。接收者被唤醒后从自己的 swap_data 读到 value,无需再从邮箱池取。
第三步:无接收者且邮箱满(条件:recv_task_list_head 为空,且 used_mails == capacity)—— 等待或返回满
if (mb->used_mails == mb->capacity)
{
if (OS_NO_WAIT == timeout)
{
ret = OS_FULL;
}
else
{
current_task = _k_task_self();
OS_ASSERT((OS_NULL != current_task));
current_task->swap_data = value;
if (OS_MB_WAKE_TYPE_PRIO == mb->wake_type)
{
ret = k_block_task(&mb->lock, irq_save, &mb->send_task_list_head, timeout, OS_TRUE);
}
else
{
ret = k_block_task(&mb->lock, irq_save, &mb->send_task_list_head, timeout, OS_FALSE);
}
return ret;
}
}进入本步的条件:recv_task_list_head 为空(无接收者在等)且 used_mails == capacity(邮箱已满)。
- 子分支 A:条件
timeout == OS_NO_WAIT(不等待)→ret = OS_FULL,稍后返回。 - 子分支 B:条件
timeout != OS_NO_WAIT(等待):current_task = _k_task_self():获取当前发送任务;current_task->swap_data = value:先把要发的邮件值暂存到当前任务的swap_data(这样接收者取走邮件后,会把一个空闲槽位"接力"给当前任务,见 2.6);- 按
wake_type把当前任务k_block_task挂到发送者等待队列send_task_list_head并让出 CPU; - 直接
return ret(阻塞唤醒后的返回值就是k_block_task的结果)。
第四步:无接收者且邮箱未满(条件:recv_task_list_head 为空,且 used_mails != capacity)—— 写入邮件池
else
{
*((os_ubase_t *)mb->mail_pool_align + mb->write_index) = value;
_k_mb_modify_write_index(mb);
}进入本步的条件:recv_task_list_head 为空(无接收者在等)且 used_mails != capacity(邮箱未满,有空闲槽位)。
*((os_ubase_t *)mb->mail_pool_align + mb->write_index) = value:把邮件值写入写索引指向的槽位。_k_mb_modify_write_index(mb):推进写索引、used_mails++(遇容量上限回绕到 0)。
第五步:触发调度(条件:need_sched)—— 收尾
if (OS_TRUE == need_sched)
{
OS_KERNEL_EXIT_SCHED();
}
else
{
OS_KERNEL_EXIT();
}
return ret;进入本步的条件:本步从第二步/第三步子分支 A/第四步正常走到(阻塞等待的第三步子分支 B 已提前 return)。
- 子条件
need_sched == OS_TRUE(第二步唤醒过接收者)→OS_KERNEL_EXIT_SCHED()触发调度;否则仅退出临界区。
os_mailbox_send 的 if-else 条件走向总结:
os_mailbox_send() 条件走向总结:
├── OS_ASSERT 参数校验(mb 非空 / 已初始化 / 等待条件)任一失败 → 断言崩溃
├── if (recv_task_list_head 非空,有接收者在等)
│ └── 取队首接收者 → k_unblock_task 唤醒 → 把 value 写入其 swap_data
│ └── 后续按 need_sched 触发调度 → 返回 OS_SUCCESS
└── else(无接收者在等)
├── if (used_mails == capacity,邮箱满)
│ ├── if (timeout == OS_NO_WAIT,不等待) → 返回 OS_FULL
│ └── else(等待)
│ └── 把 value 存到当前任务 swap_data → 按 wake_type 阻塞挂到 send_task_list_head
│ └── 返回 k_block_task 结果(成功/超时/被销毁唤醒)
└── else(邮箱未满)
└── 写入 write_index 槽位 + _k_mb_modify_write_index → 返回 OS_SUCCESS使用示例:
os_ubase_t value = (os_ubase_t)&data; /* 传指针 */
os_err_t ret = os_mailbox_send(mb_id, value, OS_WAIT_FOREVER);
if (ret != OS_EOK)
{
os_kprintf("send failed: %d\r\n", ret);
}2.6 接收邮件
os_err_t os_mailbox_recv(os_mailbox_id mailbox_id, os_ubase_t *value, os_tick_t timeout);| 参数 | 说明 |
|---|---|
mailbox_id | 邮箱 ID |
value | 输出参数,接收到的邮件值 |
timeout | 超时时间 |
| 返回值 | OS_EOK 成功;OS_ETIMEOUT 超时;OS_EEMPTY 邮箱空且不等待 |
完整源码(kernel/source/os_mb.c,与项目一致):
os_err_t os_mailbox_recv(os_mailbox_id mailbox_id, os_ubase_t *value, os_tick_t timeout)
{
os_task_t *current_task;
os_task_t *block_task;
os_bool_t need_sched;
os_bool_t modify_read_index;
os_err_t ret;
os_mailbox_t *mb;
mb = OS_TYPE_CONVERT(os_mailbox_t *, mailbox_id);
OS_KERNEL_INIT();
OS_ASSERT(OS_NULL != mb);
OS_ASSERT(OS_NULL != value);
OS_ASSERT(OS_KOBJ_INITED == mb->object_inited);
OS_ASSERT((OS_NO_WAIT == timeout) || (OS_NULL == k_task_self()) || (OS_FALSE == os_is_irq_active()));
OS_ASSERT((OS_NO_WAIT == timeout) || (OS_NULL == k_task_self()) || (OS_FALSE == os_is_irq_disabled()));
OS_ASSERT((OS_NO_WAIT == timeout) || (OS_NULL == k_task_self()) || (OS_FALSE == os_is_schedule_locked()));
OS_ASSERT((timeout < (OS_TICK_MAX / 2)) || (OS_WAIT_FOREVER == timeout));
ret = OS_SUCCESS;
need_sched = OS_FALSE;
modify_read_index = OS_TRUE;
#if defined(OS_USING_SMP) && !defined(OS_IPC_MONOLOCK_MODE)
os_spin_lock_irq(&mb->lock, &irq_save);
#else
OS_KERNEL_ENTER();
#endif
if (0U != mb->used_mails)
{
*value = *((os_ubase_t *)mb->mail_pool_align + mb->read_index);
}
else
{
if (OS_NO_WAIT == timeout)
{
ret = OS_EMPTY;
}
else
{
current_task = _k_task_self();
OS_ASSERT((OS_NULL != current_task));
if (OS_MB_WAKE_TYPE_PRIO == mb->wake_type)
{
ret = k_block_task(&mb->lock, irq_save, &mb->recv_task_list_head, timeout, OS_TRUE);
}
else
{
ret = k_block_task(&mb->lock, irq_save, &mb->recv_task_list_head, timeout, OS_FALSE);
}
if (OS_SUCCESS == ret)
{
/*
* When the sender wakes up this task, it puts the data on the swap_data,
* so there's no need to get data from the mail pool of mailbox.
*/
*value = current_task->swap_data;
modify_read_index = OS_FALSE;
}
return ret;
}
}
if ((OS_SUCCESS == ret) && (OS_TRUE == modify_read_index))
{
_k_mb_modify_read_index(mb);
#if defined(OS_USING_SMP) && !defined(OS_IPC_MONOLOCK_MODE)
k_kernel_spin_lock();
#endif
if (!os_list_empty(&mb->send_task_list_head))
{
block_task = os_list_first_entry(&mb->send_task_list_head, os_task_t, task_node);
need_sched = k_unblock_task(block_task);
#if defined(OS_USING_SMP) && !defined(OS_IPC_MONOLOCK_MODE)
k_kernel_spin_unlock();
#endif
/*
* when the sender task was blocked, it put the data on the swap_data, so receiver get mail
* directly from swap data.
*
* In addition, the receiver has obtained a mail at this time, so it is necessary to put the
* mail info the mail pool of mailbox.
*/
*((os_ubase_t *)mb->mail_pool_align + mb->write_index) = block_task->swap_data;
_k_mb_modify_write_index(mb);
}
#if defined(OS_USING_SMP) && !defined(OS_IPC_MONOLOCK_MODE)
else
{
k_kernel_spin_unlock();
}
#endif
}
#if defined(OS_USING_SMP) && !defined(OS_IPC_MONOLOCK_MODE)
os_spin_unlock_irq(&mb->lock, irq_save);
if (OS_TRUE == need_sched)
{
OS_KERNEL_ENTER();
OS_KERNEL_EXIT_SCHED();
}
#else
if (OS_TRUE == need_sched)
{
OS_KERNEL_EXIT_SCHED();
}
else
{
OS_KERNEL_EXIT();
}
#endif
return ret;
}辅助函数:_k_mb_modify_read_index() —— 读索引推进 + 邮件数减一
接收后把邮件取出,调用它推进读索引:
static void _k_mb_modify_read_index(os_mailbox_t *mb)
{
mb->read_index++;
if (mb->read_index >= mb->capacity)
{
mb->read_index = 0U;
}
mb->used_mails--;
return;
}- 条件
read_index >= capacity:读索引到达容量上限 → 回绕到 0(环形缓冲区)。 used_mails--:已用邮件数减一。
分步详解:
第一步:准备与参数校验(条件:邮箱/value 非空、已初始化、等待条件合法)—— 不满足则断言崩溃
OS_ASSERT(OS_NULL != mb);
OS_ASSERT(OS_NULL != value);
OS_ASSERT(OS_KOBJ_INITED == mb->object_inited);
OS_ASSERT((OS_NO_WAIT == timeout) || (OS_NULL == k_task_self()) || (OS_FALSE == os_is_irq_active()));
OS_ASSERT((OS_NO_WAIT == timeout) || (OS_NULL == k_task_self()) || (OS_FALSE == os_is_irq_disabled()));
OS_ASSERT((OS_NO_WAIT == timeout) || (OS_NULL == k_task_self()) || (OS_FALSE == os_is_schedule_locked()));
OS_ASSERT((timeout < (OS_TICK_MAX / 2)) || (OS_WAIT_FOREVER == timeout));进入本步的条件:调用 os_mailbox_recv() 时无条件执行(本步是入口硬性校验,不满足条件直接断言崩溃)。
详细解释:
mb != OS_NULL:邮箱非空。value != OS_NULL:value是输出参数,必须非空(函数要写它,为NULL会解引用崩溃)。object_inited == OS_KOBJ_INITED:邮箱已初始化。- 三条"等待条件"断言:需要阻塞等待(
timeout != OS_NO_WAIT)时,必须有当前任务、不在中断、未关中断、未锁调度。 timeout < OS_TICK_MAX/2 || timeout == OS_WAIT_FOREVER:超时值合法。
第二步:邮箱非空(条件 used_mails != 0)—— 从读索引槽位取出邮件
if (0U != mb->used_mails)
{
*value = *((os_ubase_t *)mb->mail_pool_align + mb->read_index);
}进入本步的条件:used_mails != 0(邮箱里有邮件)。
*((os_ubase_t *)mb->mail_pool_align + mb->read_index):从读索引指向的槽位读出邮件值,写入*value(输出参数)。- 注意:此时还没推进读索引(留在下一步
_k_mb_modify_read_index做),且modify_read_index仍为OS_TRUE。
这句核心代码 *((os_ubase_t *)mb->mail_pool_align + mb->read_index) 的详细拆解:
这一行虽短,但涉及类型转换、指针算术、解引用三个关键点。从左到右可拆为 4 部分:
| 部分 | 含义 |
|---|---|
mb->mail_pool_align | 对齐后的邮件池基地址(指向第 0 个槽位) |
(os_ubase_t *) | 把 void * 强转为 os_ubase_t *(指针类型转换) |
+ mb->read_index | 指针算术:从基址偏移 N 个 os_ubase_t 大小的槽位 |
*(...) | 解引用:取出该地址处存的值 |
为什么要 (os_ubase_t *) 强转?mail_pool_align 是 void * 类型(无类型指针)。在 C 中,void * 不能做指针算术——编译器不知道每个元素多大,无法计算 +1 应该跳多少字节。所以必须先强转成 os_ubase_t *,告诉编译器"每个元素是 os_ubase_t(4/8 字节)"。
指针算术 + mb->read_index 实际跳了多少字节?
指针 + 1 不是加 1 字节,而是加一个元素的大小。因为类型是 os_ubase_t *,所以:
实际偏移字节数 = read_index × sizeof(os_ubase_t)以 sizeof(os_ubase_t) = 4、基址 0x20000000 为例:
read_index | 指针表达式 (os_ubase_t *)base + read_index | 实际地址 = base + read_index×4 | 读出的槽位 |
|---|---|---|---|
| 0 | base + 0 | 0x2000_0000 | 第 0 槽 |
| 1 | base + 1 | 0x2000_0004 | 第 1 槽 |
| 2 | base + 2 | 0x2000_0008 | 第 2 槽 |
| 15 | base + 15 | 0x2000_003C | 第 15 槽 |
为什么按 read_index 跳? 邮箱是环形缓冲区,read_index 记录"下一个该从哪个槽位读"。每次接收从该槽位取邮件,取完由下一步的 _k_mb_modify_read_index 推进它(到容量上限回绕到 0)。
*(...) 解引用做什么? *(p) 取出指针 p 指向的地址处存的值。这里是读操作——把该槽位里存的邮件值(os_ubase_t)取出来。
与发送侧的对称性:发送时写入用 write_index:
*((os_ubase_t *)mb->mail_pool_align + mb->write_index) = value;读写完全对称——发送写 write_index 槽、接收读 read_index 槽,分别由 _k_mb_modify_write_index / _k_mb_modify_read_index 推进。
一句话总结:*((os_ubase_t *)mb->mail_pool_align + mb->read_index) = 把邮件池基址当作 os_ubase_t 数组,取出第 read_index 个槽位里存的邮件值。void*→os_ubase_t* 强转让指针算术按元素大小(而非字节)跳,read_index 决定读哪个环形槽位。
第三步:邮箱空(条件 used_mails == 0)—— 等待或返回空
else
{
if (OS_NO_WAIT == timeout)
{
ret = OS_EMPTY;
}
else
{
current_task = _k_task_self();
OS_ASSERT((OS_NULL != current_task));
if (OS_MB_WAKE_TYPE_PRIO == mb->wake_type)
{
ret = k_block_task(&mb->lock, irq_save, &mb->recv_task_list_head, timeout, OS_TRUE);
}
else
{
ret = k_block_task(&mb->lock, irq_save, &mb->recv_task_list_head, timeout, OS_FALSE);
}
if (OS_SUCCESS == ret)
{
*value = current_task->swap_data;
modify_read_index = OS_FALSE;
}
return ret;
}
}进入本步的条件:used_mails == 0(邮箱空,没有邮件可取)。
- 子分支 A:条件
timeout == OS_NO_WAIT(不等待)→ret = OS_EMPTY(返回"邮箱空"错误码),继续走到第四步的if (ret == OS_SUCCESS ...)判断(此时ret != OS_SUCCESS,跳过第四步),最终返回OS_EMPTY。 - 子分支 B:条件
timeout != OS_NO_WAIT(等待):- 按
wake_type把当前任务k_block_task挂到接收者等待队列recv_task_list_head并让出 CPU,直到发送者唤醒; - 被唤醒且
ret == OS_SUCCESS时,从current_task->swap_data读出发送者直接转交的邮件值(发送者把value写入了接收者任务的swap_data,见 2.5 第二步),写入*value; - 置
modify_read_index = OS_FALSE(因为邮件不是从邮箱池取的,不推进读索引); - 直接
return ret。
- 按
第四步:推进读索引 + 接力给等待的发送者(条件:ret == OS_SUCCESS 且 modify_read_index == OS_TRUE)—— 更新索引并可能唤醒发送者
if ((OS_SUCCESS == ret) && (OS_TRUE == modify_read_index))
{
_k_mb_modify_read_index(mb);
if (!os_list_empty(&mb->send_task_list_head))
{
block_task = os_list_first_entry(&mb->send_task_list_head, os_task_t, task_node);
need_sched = k_unblock_task(block_task);
*((os_ubase_t *)mb->mail_pool_align + mb->write_index) = block_task->swap_data;
_k_mb_modify_write_index(mb);
}
}进入本步的条件:ret == OS_SUCCESS 且 modify_read_index == OS_TRUE(从第二步正常取出邮件;若从第三步子分支 B 的 swap_data 取,modify_read_index 已是 OS_FALSE,跳过本步)。
_k_mb_modify_read_index(mb):推进读索引、used_mails--(腾出一个槽位)。- 子分支:条件
send_task_list_head非空(有发送者因邮箱满而阻塞):- 取队首发送者
block_task,k_unblock_task唤醒它(返回need_sched); - 把这个腾出的空闲槽位写入发送者暂存的邮件值(
block_task->swap_data,见 2.5 第三步子分支 B),并_k_mb_modify_write_index——发送者的邮件由接收者接力放入邮箱池,无需发送者重新竞争。
- 取队首发送者
关键点 —— 发送与接收的"槽位接力"闭环(对应 2.5 的 current_task->swap_data = value):
这个接力机制需要把 2.5 发送和 2.6 接收两端串起来看,才构成一个完整闭环:
【发送方】(2.5 第三步子分支 B:邮箱满时)
① current_task->swap_data = value
└─ 把要发的邮件值暂存到"发送者自己"的 swap_data
② 阻塞挂到 send_task_list_head(等待空闲槽位)
【接收方】(2.6 第四步:腾出槽位时)
③ _k_mb_modify_read_index(mb) → used_mails--,腾出一个空闲槽位
④ 发现 send_task_list_head 非空(有发送者在等)
└─ 取出发送者 block_task,把它暂存的 block_task->swap_data(= value)写入腾出的槽位
└─ _k_mb_modify_write_index(mb) → used_mails++(邮件已入池)
└─ k_unblock_task(block_task) 唤醒发送者
【发送方】被唤醒后
⑤ 发现邮件已由接收者写入邮箱池,无需重新发送,直接返回为什么这样接力(为什么"能省"):如果没有这个机制,发送者邮箱满时必须阻塞,等有空槽位后再重新执行一次发送逻辑(重新竞争、重新写槽位)。而有了 swap_data 接力,接收者取走邮件腾出槽位的同一时刻,直接把发送者暂存的邮件值写入槽位并唤醒——发送者被唤醒时邮件已入池,省去"重试发送"的往返。这与消息队列的 swap_data 接力完全对称。
两个 swap_data 写法的区别(易混淆点):
| 位置 | 代码 | 写入谁 | 何时执行 |
|---|---|---|---|
| 2.5 发送·有接收者在等 | block_task->swap_data = value | 接收者的 swap_data | 直接把邮件转交给等待的接收者 |
| 2.5 发送·邮箱满 | current_task->swap_data = value | 发送者自己的 swap_data | 暂存邮件值,等接收者接力入池 |
| 2.6 接收·被发送者唤醒 | *value = current_task->swap_data | 读自己(接收者)的 swap_data | 取出发送者直接转交的邮件 |
| 2.6 接收·腾槽位后 | *(...write_index) = block_task->swap_data | 读发送者的 swap_data 写入槽位 | 把发送者暂存的邮件接力入池 |
第五步:触发调度(条件:need_sched)—— 收尾
if (OS_TRUE == need_sched)
{
OS_KERNEL_EXIT_SCHED();
}
else
{
OS_KERNEL_EXIT();
}
return ret;进入本步的条件:从第二步或第三步子分支 A 正常走到本步(阻塞等待的第三步子分支 B 已提前 return)。
- 子条件
need_sched == OS_TRUE(第四步唤醒过发送者)→OS_KERNEL_EXIT_SCHED()触发调度;否则仅退出临界区。
os_mailbox_recv 的 if-else 条件走向总结:
os_mailbox_recv() 条件走向总结:
├── OS_ASSERT 参数校验(mb 非空 / value 非空 / 已初始化 / 等待条件)任一失败 → 断言崩溃
├── if (used_mails != 0,邮箱有邮件)
│ └── 从 read_index 槽位读出 → *value(modify_read_index 保持 OS_TRUE)
└── else(邮箱空)
├── if (timeout == OS_NO_WAIT,不等待) → ret = OS_EMPTY(跳过第四步,返回)
└── else(等待)
├── 按 wake_type 阻塞挂到 recv_task_list_head
└── 被唤醒且 ret == OS_SUCCESS → *value = self->swap_data;modify_read_index = OS_FALSE;return
再判断:if (ret == OS_SUCCESS 且 modify_read_index == OS_TRUE)
├── _k_mb_modify_read_index(推进读索引,used_mails--)
└── if (send_task_list_head 非空,有发送者阻塞)
└── 唤醒发送者 + 把其 swap_data 写入腾出的槽位(接力入池)
最后按 need_sched 触发调度,返回 ret使用示例:
os_ubase_t value;
os_err_t ret = os_mailbox_recv(mb_id, &value, OS_WAIT_FOREVER);
if (ret == OS_EOK)
{
work_item_t *item = (work_item_t *)value; /* 取出的是指针 */
os_kprintf("got item: %s\r\n", item->message);
}2.7 查询与管理函数
以下函数定义在 kernel/include/os_mb.h 中(本项目邮箱的 set_wake_type、reset、is_empty、is_full、get_capacity、get_used_mails、get_unused_mails):
| 函数 | 说明 |
|---|---|
os_mailbox_set_wake_type | 设置唤醒策略:OS_MB_WAKE_TYPE_PRIO 或 OS_MB_WAKE_TYPE_FIFO |
os_mailbox_reset | 重置邮箱,清空所有邮件并唤醒阻塞任务 |
os_mailbox_is_empty | 检查邮箱是否为空 |
os_mailbox_is_full | 检查邮箱是否已满 |
os_mailbox_get_capacity | 获取邮箱总容量 |
os_mailbox_get_used_mails | 获取已使用的邮件数 |
os_mailbox_get_unused_mails | 获取剩余可用的邮件数 |
os_mailbox_set_wake_type() —— 设置唤醒策略
os_err_t os_mailbox_set_wake_type(os_mailbox_id mailbox_id, uint8_t wake_type);完整源码(kernel/source/os_mb.c,与项目一致):
os_err_t os_mailbox_set_wake_type(os_mailbox_id mailbox_id, uint8_t wake_type)
{
os_err_t ret;
os_mailbox_t *mb;
OS_KERNEL_INIT();
mb = OS_TYPE_CONVERT(os_mailbox_t *, mailbox_id);
OS_ASSERT(OS_NULL != mb);
OS_ASSERT(OS_KOBJ_INITED == mb->object_inited);
OS_ASSERT((OS_MB_WAKE_TYPE_PRIO == wake_type) || (OS_MB_WAKE_TYPE_FIFO == wake_type));
ret = OS_BUSY;
#if defined(OS_USING_SMP) && !defined(OS_IPC_MONOLOCK_MODE)
os_spin_lock_irq(&mb->lock, &irq_save);
k_kernel_spin_lock();
#else
OS_KERNEL_ENTER();
#endif
if (os_list_empty(&mb->recv_task_list_head) && os_list_empty(&mb->send_task_list_head))
{
mb->wake_type = wake_type;
ret = OS_SUCCESS;
}
#if defined(OS_USING_SMP) && !defined(OS_IPC_MONOLOCK_MODE)
k_kernel_spin_unlock();
os_spin_unlock_irq(&mb->lock, irq_save);
#else
OS_KERNEL_EXIT();
#endif
return ret;
}分步详解:
第一步:准备与参数校验(条件:邮箱非空、已初始化、wake_type 合法)—— 不满足则断言崩溃
OS_ASSERT(OS_NULL != mb);
OS_ASSERT(OS_KOBJ_INITED == mb->object_inited);
OS_ASSERT((OS_MB_WAKE_TYPE_PRIO == wake_type) || (OS_MB_WAKE_TYPE_FIFO == wake_type));进入本步的条件:调用 os_mailbox_set_wake_type() 时无条件执行(入口硬性校验)。
mb != OS_NULL:邮箱非空。object_inited == OS_KOBJ_INITED:邮箱已初始化。wake_type == OS_MB_WAKE_TYPE_PRIO 或 OS_MB_WAKE_TYPE_FIFO:唤醒策略参数必须合法。
第二步:修改唤醒策略(条件:两个等待队列都为空)—— 成功修改,否则返回 OS_BUSY
ret = OS_BUSY;
if (os_list_empty(&mb->recv_task_list_head) && os_list_empty(&mb->send_task_list_head))
{
mb->wake_type = wake_type;
ret = OS_SUCCESS;
}
return ret;进入本步的条件:第一步断言通过。
- 子分支 A:条件
recv_task_list_head和send_task_list_head两个等待队列都为空(无任务阻塞)→mb->wake_type = wake_type修改成功,ret = OS_SUCCESS。 - 子分支 B:条件两个等待队列任一非空(有任务阻塞)→ 保持
ret = OS_BUSY返回(队列中有任务已按旧策略排序,不能改)。
关键点:为什么必须在无阻塞任务时才能改? 等待队列的排序方式由 wake_type 决定(PRIO 按优先级、FIFO 按等待先后)。若已有任务按旧策略挂进队列,此时改变策略会导致队列排序混乱。所以只能在"队列完全空闲、无任务等待"时修改。
os_mailbox_reset() —— 重置邮箱
os_err_t os_mailbox_reset(os_mailbox_id mailbox_id);完整源码(kernel/source/os_mb.c,与项目一致):
os_err_t os_mailbox_reset(os_mailbox_id mailbox_id)
{
os_bool_t need_sched;
os_mailbox_t *mb;
OS_KERNEL_INIT();
mb = OS_TYPE_CONVERT(os_mailbox_t *, mailbox_id);
OS_ASSERT(OS_NULL != mb);
OS_ASSERT(OS_KOBJ_INITED == mb->object_inited);
need_sched = OS_FALSE;
#if defined(OS_USING_SMP) && !defined(OS_IPC_MONOLOCK_MODE)
os_spin_lock_irq(&mb->lock, &irq_save);
#else
OS_KERNEL_ENTER();
#endif
mb->read_index = 0U;
mb->write_index = 0U;
mb->used_mails = 0U;
if (OS_TRUE == k_cancel_all_blocked_task(&mb->send_task_list_head))
{
need_sched = OS_TRUE;
}
if (OS_TRUE == k_cancel_all_blocked_task(&mb->recv_task_list_head))
{
need_sched = OS_TRUE;
}
#if defined(OS_USING_SMP) && !defined(OS_IPC_MONOLOCK_MODE)
os_spin_unlock_irq(&mb->lock, irq_save);
if (OS_TRUE == need_sched)
{
OS_KERNEL_ENTER();
OS_KERNEL_EXIT_SCHED();
}
#else
if (OS_TRUE == need_sched)
{
OS_KERNEL_EXIT_SCHED();
}
else
{
OS_KERNEL_EXIT();
}
#endif
return OS_SUCCESS;
}分步详解:
第一步:准备与参数校验(条件:邮箱非空、已初始化)—— 不满足则断言崩溃
OS_ASSERT(OS_NULL != mb);
OS_ASSERT(OS_KOBJ_INITED == mb->object_inited);进入本步的条件:调用 os_mailbox_reset() 时无条件执行(入口硬性校验)。mb 非空、邮箱已初始化。
第二步:清空邮箱(无条件执行)—— 重置读写索引和邮件数
mb->read_index = 0U;
mb->write_index = 0U;
mb->used_mails = 0U;进入本步的条件:第一步断言通过。把读索引、写索引、已用邮件数全部清零——邮箱回到"空"的初始状态(邮件槽位内容不用清,因为 used_mails=0 表示没有有效邮件)。
第三步:唤醒所有阻塞的收发任务(条件:等待队列非空)—— 触发调度
if (OS_TRUE == k_cancel_all_blocked_task(&mb->send_task_list_head))
{
need_sched = OS_TRUE;
}
if (OS_TRUE == k_cancel_all_blocked_task(&mb->recv_task_list_head))
{
need_sched = OS_TRUE;
}
if (OS_TRUE == need_sched)
{
OS_KERNEL_EXIT_SCHED();
}
else
{
OS_KERNEL_EXIT();
}
return OS_SUCCESS;进入本步的条件:第二步已清空邮箱。
- 子分支 A:条件
send_task_list_head非空 → 唤醒所有阻塞的发送者,返回OS_TRUE则need_sched = OS_TRUE。 - 子分支 B:条件
recv_task_list_head非空 → 唤醒所有阻塞的接收者,返回OS_TRUE则need_sched = OS_TRUE。 - 子条件
need_sched == OS_TRUE(有唤醒)→ 触发调度;否则仅退出临界区。
关键点:重置会清空邮箱并唤醒所有阻塞的收发任务,让它们从等待中恢复。与 destroy 类似,都是先处理阻塞任务。
os_mailbox_is_empty() —— 检查邮箱是否为空
os_err_t os_mailbox_is_empty(os_mailbox_id mailbox_id, os_bool_t *empty_flag);完整源码(kernel/source/os_mb.c,与项目一致):
os_err_t os_mailbox_is_empty(os_mailbox_id mailbox_id, os_bool_t *empty_flag)
{
os_mailbox_t *mb;
OS_KERNEL_INIT();
mb = OS_TYPE_CONVERT(os_mailbox_t *, mailbox_id);
OS_ASSERT(OS_NULL != mb);
OS_ASSERT(OS_KOBJ_INITED == mb->object_inited);
OS_ASSERT(OS_NULL != empty_flag);
#if defined(OS_USING_SMP) && !defined(OS_IPC_MONOLOCK_MODE)
os_spin_lock_irq(&mb->lock, &irq_save);
#else
OS_KERNEL_ENTER();
#endif
if (0U == mb->used_mails)
{
*empty_flag = OS_TRUE;
}
else
{
*empty_flag = OS_FALSE;
}
#if defined(OS_USING_SMP) && !defined(OS_IPC_MONOLOCK_MODE)
os_spin_unlock_irq(&mb->lock, irq_save);
#else
OS_KERNEL_EXIT();
#endif
return OS_SUCCESS;
}分步详解:
第一步:准备与参数校验(条件:邮箱非空、已初始化、empty_flag 传出参数非空)—— 不满足则断言崩溃
OS_ASSERT(OS_NULL != mb);
OS_ASSERT(OS_KOBJ_INITED == mb->object_inited);
OS_ASSERT(OS_NULL != empty_flag);进入本步的条件:调用 os_mailbox_is_empty() 时无条件执行(入口硬性校验)。empty_flag 是传出参数,必须非空。
第二步:判断已用邮件数(条件:used_mails 是否为 0)—— 写入结果
if (0U == mb->used_mails)
{
*empty_flag = OS_TRUE;
}
else
{
*empty_flag = OS_FALSE;
}
return OS_SUCCESS;进入本步的条件:第一步断言通过。
- 子分支 A:条件
used_mails == 0(已用邮件数为 0 → 邮箱空)→*empty_flag = OS_TRUE。 - 子分支 B:条件
used_mails != 0(有邮件 → 邮箱非空)→*empty_flag = OS_FALSE。
关键点:判断"是否为空"的依据是已用邮件数 used_mails 是否为 0。返回恒为 OS_SUCCESS,结果通过传出参数 empty_flag 告知。
os_mailbox_is_full() —— 检查邮箱是否已满
os_err_t os_mailbox_is_full(os_mailbox_id mailbox_id, os_bool_t *full_flag);完整源码(kernel/source/os_mb.c,与项目一致):
os_err_t os_mailbox_is_full(os_mailbox_id mailbox_id, os_bool_t *full_flag)
{
os_mailbox_t *mb;
OS_KERNEL_INIT();
mb = OS_TYPE_CONVERT(os_mailbox_t *, mailbox_id);
OS_ASSERT(OS_NULL != mb);
OS_ASSERT(OS_KOBJ_INITED == mb->object_inited);
OS_ASSERT(OS_NULL != full_flag);
#if defined(OS_USING_SMP) && !defined(OS_IPC_MONOLOCK_MODE)
os_spin_lock_irq(&mb->lock, &irq_save);
#else
OS_KERNEL_ENTER();
#endif
if (mb->used_mails == mb->capacity)
{
*full_flag = OS_TRUE;
}
else
{
*full_flag = OS_FALSE;
}
#if defined(OS_USING_SMP) && !defined(OS_IPC_MONOLOCK_MODE)
os_spin_unlock_irq(&mb->lock, irq_save);
#else
OS_KERNEL_EXIT();
#endif
return OS_SUCCESS;
}分步详解:
第一步:准备与参数校验(条件:邮箱非空、已初始化、full_flag 传出参数非空)—— 不满足则断言崩溃
OS_ASSERT(OS_NULL != mb);
OS_ASSERT(OS_KOBJ_INITED == mb->object_inited);
OS_ASSERT(OS_NULL != full_flag);进入本步的条件:调用 os_mailbox_is_full() 时无条件执行(入口硬性校验)。full_flag 是传出参数,必须非空。
第二步:判断已用邮件数是否达容量(条件:used_mails == capacity)—— 写入结果
if (mb->used_mails == mb->capacity)
{
*full_flag = OS_TRUE;
}
else
{
*full_flag = OS_FALSE;
}
return OS_SUCCESS;进入本步的条件:第一步断言通过。
- 子分支 A:条件
used_mails == capacity(已用邮件数等于容量 → 邮箱满)→*full_flag = OS_TRUE。 - 子分支 B:条件
used_mails != capacity(未满)→*full_flag = OS_FALSE。
关键点:判断"是否已满"的依据是已用邮件数是否等于容量(与 send 里 used_mails == capacity 判断满一致)。
os_mailbox_get_capacity() —— 获取邮箱容量
os_err_t os_mailbox_get_capacity(os_mailbox_id mailbox_id, uint16_t *capacity);完整源码(kernel/source/os_mb.c,与项目一致):
os_err_t os_mailbox_get_capacity(os_mailbox_id mailbox_id, uint16_t *capacity)
{
os_mailbox_t *mb;
mb = OS_TYPE_CONVERT(os_mailbox_t *, mailbox_id);
OS_ASSERT(OS_NULL != mb);
OS_ASSERT(OS_KOBJ_INITED == mb->object_inited);
OS_ASSERT(OS_NULL != capacity);
*capacity = mb->capacity;
return OS_SUCCESS;
}分步详解:
第一步:准备与参数校验(条件:邮箱非空、已初始化、capacity 传出参数非空)—— 不满足则断言崩溃
OS_ASSERT(OS_NULL != mb);
OS_ASSERT(OS_KOBJ_INITED == mb->object_inited);
OS_ASSERT(OS_NULL != capacity);进入本步的条件:调用 os_mailbox_get_capacity() 时无条件执行(入口硬性校验)。capacity 是传出参数,必须非空。
第二步:写入容量(无条件执行)—— 返回 capacity
*capacity = mb->capacity;
return OS_SUCCESS;进入本步的条件:第一步断言通过。把 mb->capacity(邮箱总容量,即最多能容纳的邮件数)写入传出参数,返回 OS_SUCCESS。
os_mailbox_get_used_mails() —— 获取已用邮件数
os_err_t os_mailbox_get_used_mails(os_mailbox_id mailbox_id, uint16_t *used_mails);完整源码(kernel/source/os_mb.c,与项目一致):
os_err_t os_mailbox_get_used_mails(os_mailbox_id mailbox_id, uint16_t *used_mails)
{
os_mailbox_t *mb;
mb = OS_TYPE_CONVERT(os_mailbox_t *, mailbox_id);
OS_ASSERT(OS_NULL != mb);
OS_ASSERT(OS_KOBJ_INITED == mb->object_inited);
OS_ASSERT(OS_NULL != used_mails);
*used_mails = mb->used_mails;
return OS_SUCCESS;
}分步详解:
第一步:准备与参数校验(条件:邮箱非空、已初始化、used_mails 传出参数非空)—— 不满足则断言崩溃
OS_ASSERT(OS_NULL != mb);
OS_ASSERT(OS_KOBJ_INITED == mb->object_inited);
OS_ASSERT(OS_NULL != used_mails);进入本步的条件:调用 os_mailbox_get_used_mails() 时无条件执行(入口硬性校验)。used_mails 是传出参数,必须非空。
第二步:写入已用邮件数(无条件执行)—— 返回 used_mails
*used_mails = mb->used_mails;
return OS_SUCCESS;进入本步的条件:第一步断言通过。把 mb->used_mails(当前已存入邮箱的邮件数)写入传出参数,返回 OS_SUCCESS。
os_mailbox_get_unused_mails() —— 获取剩余可用邮件数
os_err_t os_mailbox_get_unused_mails(os_mailbox_id mailbox_id, uint16_t *unused_mails);完整源码(kernel/source/os_mb.c,与项目一致):
os_err_t os_mailbox_get_unused_mails(os_mailbox_id mailbox_id, uint16_t *unused_mails)
{
uint16_t unused_entry_cnt;
os_mailbox_t *mb;
OS_KERNEL_INIT();
mb = OS_TYPE_CONVERT(os_mailbox_t *, mailbox_id);
OS_ASSERT(OS_NULL != mb);
OS_ASSERT(OS_KOBJ_INITED == mb->object_inited);
OS_ASSERT(OS_NULL != unused_mails);
#if defined(OS_USING_SMP) && !defined(OS_IPC_MONOLOCK_MODE)
os_spin_lock_irq(&mb->lock, &irq_save);
#else
OS_KERNEL_ENTER();
#endif
unused_entry_cnt = mb->capacity - mb->used_mails;
#if defined(OS_USING_SMP) && !defined(OS_IPC_MONOLOCK_MODE)
os_spin_unlock_irq(&mb->lock, irq_save);
#else
OS_KERNEL_EXIT();
#endif
*unused_mails = unused_entry_cnt;
return OS_SUCCESS;
}分步详解:
第一步:准备与参数校验(条件:邮箱非空、已初始化、unused_mails 传出参数非空)—— 不满足则断言崩溃
OS_ASSERT(OS_NULL != mb);
OS_ASSERT(OS_KOBJ_INITED == mb->object_inited);
OS_ASSERT(OS_NULL != unused_mails);进入本步的条件:调用 os_mailbox_get_unused_mails() 时无条件执行(入口硬性校验)。unused_mails 是传出参数,必须非空。
第二步:计算并写入剩余可用邮件数(无条件执行)—— 返回 capacity - used_mails
unused_entry_cnt = mb->capacity - mb->used_mails;
*unused_mails = unused_entry_cnt;
return OS_SUCCESS;进入本步的条件:第一步断言通过。计算 容量 - 已用 = 剩余可用 写入传出参数,返回 OS_SUCCESS。
关键点:与 get_used_mails 互补——used + unused = capacity(总容量)。
📝 本节小结
核心要点回顾
本节围绕邮箱机制展开,核心要点如下:
1. 邮箱的本质
- 邮箱是轻量级消息传递机制,传递
os_ubase_t类型的值(指针/整数值),不拷贝数据,只传值本身。 - 内部用环形缓冲区(
mail_pool_align)+ 读写索引(read_index/write_index)管理邮件,used_mails记录当前邮件数。
2. 与消息队列的区别
| 维度 | 邮箱 | 消息队列 |
|---|---|---|
| 传递内容 | os_ubase_t 值 | 任意大小数据块 |
| 数据拷贝 | 否 | 是 |
| 性能 | 更高 | 有拷贝开销 |
| 典型用途 | 指针、句柄、小整数值 | 数据块、结构体 |
3. 两种创建方式
| 方式 | 函数 | 特点 |
|---|---|---|
| 动态创建 | os_mailbox_create_dynamic | 从堆分配控制块和邮件池(需 OS_USING_HEAP) |
| 静态创建 | os_mailbox_create_static | 用户提供控制块和邮件池,需防重复创建 |
4. 发送/接收核心行为
| 操作 | 邮箱有邮件/空间 | 邮箱满/空 |
|---|---|---|
send | 有接收者在等→直接转交 swap_data;否则写入邮件池 | 等待空闲槽位 或 返回 OS_FULL |
recv | 从读索引槽位取出 | 等待邮件 或 返回 OS_EMPTY |
5. swap_data 接力机制
- 发送时有接收者在等:邮件直接写入接收者的
swap_data,不入池。 - 接收时有发送者在等:把腾出的槽位写入发送者
swap_data的邮件值并唤醒,避免"重试发送"。
6. 中断限制
send/recv需要阻塞等待时不能在中断/关中断/锁调度中使用。
关键易错点速查
| 易错点 | 正确做法 |
|---|---|
在中断中使用需阻塞的 send/recv | 禁止,timeout != OS_NO_WAIT 会断言崩溃;中断中应用不阻塞接口 |
| 传指针后提前释放内存 | 发送方分配的内存不能提前释放,否则接收方取到野指针;谁分配谁负责 |
接收时 value 传出参数为 NULL | 断言失败,应保证 value 非空 |
| 忽略返回值 | OS_FULL/OS_EMPTY/OS_ETIMEOUT 需处理,避免误以为成功 |
| 用邮箱传大块数据 | 邮箱只能传指针/整数值;大块数据应传指针后共享内存,或用消息队列 |
邮箱满时 OS_NO_WAIT 丢邮件 | 设计时要估算峰值邮件数,池太小会 OS_FULL |
下一步
邮箱用于传递指针/整数值。接下来请学习下一小节:
- 4.3.6 自旋锁:了解多核下如何用自旋锁保护共享资源