RT-Thread
学习笔记

邮箱工作机制

邮箱控制块

邮箱管理函数接口

创建与删除

初始化与脱离

发送邮件

接收邮件

应用示例

总结


RT-Thread版本:4.0.5
MCU型号:STM32F103RCT6(ARM Cortex-M3 内核)

邮箱用于线程间通信(异步通信方式),如多个线程可以将它们检测到的按键状态、ADC采样值等信号发送到邮箱,其他线程向该邮箱取走需要的信息。

1 邮箱工作机制

apache james解析邮件内容 邮件源码如何解析_rtos

  • 邮箱中的每一封邮件只能容纳固定的4字节内容,当传递更多字节数据时,可以把指向一个数据缓冲区的指针发送到邮箱。
  • 非阻塞方式的邮件收发可以在线程和中断上下文,阻塞方式的邮件收发只能在线程上下文中。
  • 发送邮件:如果邮箱没满,将邮件复制到邮箱中;若满了,发送线程可以选择挂起等待或直接返回-RT_EFULL。如果发送线程选择挂起等待,在超时时间内,当邮箱中的邮件被收取空出空间时,该线程将被唤醒继续发送,否则超时返回-RT_EFULL
  • 接收邮件:邮箱为空时,接收线程可以选择挂起等待。当超时时间到了依然未收到邮件,则选择挂起的线程被唤醒并返回- RT_ETIMEOUT;若邮箱中存在邮件,接收线程将复制邮箱中的4个字节邮件到接收缓存中。
  • 邮箱缓冲区为循环队列(尾插头删),接收邮件指针mb->out_offset为队头,发送邮件指针mb->in_offset为队尾。

2 邮箱控制块

struct rt_mailbox
{
    struct rt_ipc_object parent;

    rt_uint32_t* msg_pool;                /* 邮箱缓冲区的开始地址 */
    rt_uint16_t size;                     /* 邮箱缓冲区的大小 */
    rt_uint16_t entry;                    /* 邮箱中邮件的数目 */
    rt_uint16_t in_offse;				  /* 邮箱缓冲区的入口指针(偏移量) */
    rt_uint16_t out_offset;    			  /* 邮箱缓冲区的出口指针(偏移量) */
    rt_list_t suspend_sender_thread;      /* 发送线程的挂起等待队列 */
};
typedef struct rt_mailbox* rt_mailbox_t;
  • mb->entry == 0:邮箱空
  • mb->entry == mb->size:邮箱满

3 邮箱管理函数接口

apache james解析邮件内容 邮件源码如何解析_多线程_02

3.1 创建与删除

动态创建一个邮箱对象:

/**
 * @param name 邮箱对象名称
 * @param size 邮箱容量
 * @param flag 邮箱标志: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
 *
 * @return 成功-rt_mailbox_t对象, 失败-RT_NULL
 */
rt_mailbox_t rt_mb_create(const char *name, rt_size_t size, rt_uint8_t flag)
{
    rt_mailbox_t mb;

    RT_DEBUG_NOT_IN_INTERRUPT;

    /* 分配邮箱对象 */
    mb = (rt_mailbox_t)rt_object_allocate(RT_Object_Class_MailBox, name);
    if (mb == RT_NULL)
        return mb;

    /* 设置接收线程等待模式 */
    mb->parent.parent.flag = flag;

    /* 初始化邮箱对象 */
    rt_ipc_object_init(&(mb->parent));	// 调用rt_list_init(&(ipc->suspend_thread));

    /* 初始化邮箱 */
    mb->size     = size;
    mb->msg_pool = RT_KERNEL_MALLOC(mb->size * sizeof(rt_uint32_t));	// x4 每逢邮箱4 bytes
    if (mb->msg_pool == RT_NULL)
    {
        /* 删除邮箱对象 */
        rt_object_delete(&(mb->parent.parent));   // 已从内存堆分配空间, 故需要删除

        return RT_NULL;
    }
    mb->entry      = 0;
    mb->in_offset  = 0;
    mb->out_offset = 0;

    /* 额外初始化发送邮件挂起线程的链表, 接收线程挂起链表由ipc父类管理 */
    rt_list_init(&(mb->suspend_sender_thread)); 

    return mb;
}
  • 将当前邮箱数目entry、邮箱出入口指针out_offsein_offse(数组索引)均初始化为0
  • 将接收、发送线程挂起链表分别初始化(前驱指针与后继指针均指向自身)
  • 实际邮箱大小为邮件数量size * 每封邮件大小4

删除由rt_mb_create() 创建的邮箱:

rt_err_t rt_mb_delete(rt_mailbox_t mb)
{   
    RT_DEBUG_NOT_IN_INTERRUPT;

    /* 唤醒挂起在该邮箱上的所有接收线程, 被唤醒的线程返回-RT_ERROR */
    rt_ipc_list_resume_all(&(mb->parent.suspend_thread));

    /* 唤醒挂起在该邮箱上的所有私有发送线程, 被唤醒的线程返回-RT_ERROR */
    rt_ipc_list_resume_all(&(mb->suspend_sender_thread));

    /* 释放邮箱缓冲区内存 */
    RT_KERNEL_FREE(mb->msg_pool);

    /* 删除邮箱对象 */
    rt_object_delete(&(mb->parent.parent));

    return RT_EOK;
}
  • 先唤醒挂起在该邮箱上的所有接收和发送线程, 被唤醒的线程返回-RT_ERROR
  • 释放邮箱缓冲区内存(动态申请的),然后删除邮箱对象

3.2 初始化与脱离

静态邮箱对象的初始化(其缓冲区是提前申请好的):

/**
 * @param mb 静态邮箱对象
 * @param name 邮箱对象名称
 * @param msgpool 缓冲区指针(缓冲区需提前创建)
 * @param size 邮箱容量
 * @param flag 邮箱标志:RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
 *
 * @return successful-RT_EOK 
 */
rt_err_t rt_mb_init(rt_mailbox_t mb,
                    const char  *name,
                    void        *msgpool,
                    rt_size_t    size,
                    rt_uint8_t   flag)
{
    RT_ASSERT(mb != RT_NULL);

    /* 初始化object内核对象 */
    rt_object_init(&(mb->parent.parent), RT_Object_Class_MailBox, name);

    mb->parent.parent.flag = flag;

    /* 初始化邮箱对象 */
    rt_ipc_object_init(&(mb->parent));

    mb->msg_pool   = msgpool;
    mb->size       = size;
    mb->entry      = 0;
    mb->in_offset  = 0;
    mb->out_offset = 0;

   /* 额外初始化发送邮件挂起线程的链表, 接收线程挂起链表由ipc父类管理 */
    rt_list_init(&(mb->suspend_sender_thread));

    return RT_EOK;
}
  • 动态创建的邮箱缓冲区是由系统从堆区分配的,静态初始化的邮箱缓冲区需要提前创建好。然后传入该缓冲区指针
  • 邮箱容量size = sizeof(msg_pool) / 4(msg_pool为提前创建的邮箱缓冲区)

脱离rt_mb_init()初始化的邮箱对象:

rt_err_t rt_mb_detach(rt_mailbox_t mb)
{
    /* 唤醒挂起在该邮箱上的所有接收线程 */
    rt_ipc_list_resume_all(&(mb->parent.suspend_thread));
    /* 唤醒挂起在该邮箱上的所有私有发送线程, */
    rt_ipc_list_resume_all(&(mb->suspend_sender_thread));

    /* 脱离邮箱对象 */
    rt_object_detach(&(mb->parent.parent));

    return RT_EOK;
}

3.3 发送邮件

等待方式发送邮件:

/**
 * @param mb        邮箱对象的句柄
 * @param value     邮件内容
 * @param timeout   等待时间
 *
 * @return 返回操作状态
 */
rt_err_t rt_mb_send_wait(rt_mailbox_t mb,
                         rt_uint32_t  value,
                         rt_int32_t   timeout)
{
    struct rt_thread *thread;
    register rt_ubase_t temp;
    rt_uint32_t tick_delta;

    /* 初始化系统时间差 */
    tick_delta = 0;

    thread = rt_thread_self();

    RT_OBJECT_HOOK_CALL(rt_object_put_hook, (&(mb->parent.parent)));

    temp = rt_hw_interrupt_disable();

    /*1 无阻塞调用 */
    if (mb->entry == mb->size && timeout == 0)
    {
        rt_hw_interrupt_enable(temp);

        return -RT_EFULL;
    }

    /*2 邮箱已满 */
    while (mb->entry == mb->size)
    {
        /* 重置线程错误码 */
        thread->error = RT_EOK;
		// 这段代码是有必要的: 防止邮箱满时被误唤醒,timeout接近临界值
        // 此时tick_delta(代码取指耗费时间)可能会>timeout,使得timeout为负值然后被置0
        if (timeout == 0)   
        {                   
            rt_hw_interrupt_enable(temp);

            return -RT_EFULL;
        }

        RT_DEBUG_IN_THREAD_CONTEXT;
        /* 挂起当前线程*/
        rt_ipc_list_suspend(&(mb->suspend_sender_thread),
                            thread,
                            mb->parent.parent.flag);

        /* 有等待时间(非永久等待),启动线程内置定时器 */
        if (timeout > 0)
        {
            tick_delta = rt_tick_get(); // 获取当前系统时间

            RT_DEBUG_LOG(RT_DEBUG_IPC, ("mb_send_wait: start timer of thread:%s\n",
                                        thread->name));

            /* 重置线程超时时间并开始定时 */
            rt_timer_control(&(thread->thread_timer),
                             RT_TIMER_CTRL_SET_TIME,
                             &timeout);
            rt_timer_start(&(thread->thread_timer));
        }

        rt_hw_interrupt_enable(temp);

        rt_schedule();

        /* 超时时间到了邮箱还是满的 */
        if (thread->error != RT_EOK)
        {
            return thread->error;
        }

        temp = rt_hw_interrupt_disable();

        /* 如果不是永远等待(timeout == -1),则重新计算剩余超时时间 */
        if (timeout > 0)    // 防止邮箱满时被误唤醒
        {
            tick_delta = rt_tick_get() - tick_delta;
            timeout -= tick_delta;
            if (timeout < 0)
                timeout = 0;
        }
    }

    /* 将要发送的信息放入邮件中 */
    mb->msg_pool[mb->in_offset] = value;
    /* 邮件入口指针向后偏移,如果越界则置0 */
    ++ mb->in_offset;
    if (mb->in_offset >= mb->size)
        mb->in_offset = 0;
    /* 邮件总数量+1 */
    mb->entry ++;

    // 如果接收线程阻塞链表不为空,则唤醒该阻塞链表中第一个线程,并发起线程调度
    if (!rt_list_isempty(&mb->parent.suspend_thread))   
    {
        rt_ipc_list_resume(&(mb->parent.suspend_thread));

        rt_hw_interrupt_enable(temp);

        rt_schedule();  

        return RT_EOK;
    }

    rt_hw_interrupt_enable(temp);

    return RT_EOK;
}
  • 邮箱已满:
  • timeout == 0:即无阻塞调用,直接返回-RT_EFULL
  • timeout < 0(RT_WAITING_FOREVER == -1):永久阻塞挂起直到邮箱非满(有线程接收了邮件)
  • timeout > 0:将线程内置定时器超时时间设为timeout并启动,等待邮箱非满时被唤醒。可能存在被误唤醒的情况,因此用tick_delta来记录定时器启动开始往后流逝的tick,如果被误唤醒则将剩余时间作为定时器超时时间并再次启动。但考虑到CPU取指消耗的时间(个人猜测),所以timeout可能会被减为0,此时会返回-RT_EFULL
  • 邮箱未满:
  • 将要发送的信息放入邮件中,邮件入口指针(循环队列队尾指针)mb->in_offset向后偏移一位,如果越界则置零,从头开始;再让邮件总量mb->entry+1。
  • 如果接收线程阻塞链表不为空,则唤醒该阻塞链表中第一个线程,并发起线程调度。

无等待发送邮件:

rt_err_t rt_mb_send(rt_mailbox_t mb, rt_ubase_t value)
{
    return rt_mb_send_wait(mb, value, 0);
}

发送紧急邮件:

rt_err_t rt_mb_urgent(rt_mailbox_t mb, rt_ubase_t value)
{
    register rt_ubase_t temp;

    temp = rt_hw_interrupt_disable();

    // 邮箱已满
    if (mb->entry == mb->size)
    {
        rt_hw_interrupt_enable(temp);
        return -RT_EFULL;
    }

    /* 倒回先前的位置 */
    if (mb->out_offset > 0)
    {
        mb->out_offset --;
    }
    else
    {
        mb->out_offset = mb->size - 1;
    }

    /* 将要发送的信息放入邮件中 */
    mb->msg_pool[mb->out_offset] = value;

    /* 邮件总数量+1 */
    mb->entry ++;

    // 如果接收线程阻塞链表不为空,则唤醒该阻塞链表中第一个线程,并发起线程调度
    if (!rt_list_isempty(&mb->parent.suspend_thread))
    {
        _ipc_list_resume(&(mb->parent.suspend_thread));

        rt_hw_interrupt_enable(temp);

        rt_schedule();

        return RT_EOK;
    }

    rt_hw_interrupt_enable(temp);

    return RT_EOK;
}

采用无阻塞方式发送,邮件被直接插入到队头,使得接收者能够优先收到紧急邮件。

  • 邮箱已满:直接返回-RT_EFULL
  • 邮箱未满:
  • 让邮件出口指针mb->out_offset(循环队列队头指针)倒回到前一个位置,然后在该位置插入邮件,再让邮件总量mb->entry+1。
  • 如果接收线程阻塞链表不为空,则唤醒该阻塞链表中第一个线程,并发起线程调度。

3.4 接收邮件

/**
 * @param mb        邮箱对象的句柄
 * @param value     存放邮件内容的指针
 * @param timeout   等待时间
 *
 * @return 成功-RT_EOK 失败-其他值
 */
rt_err_t rt_mb_recv(rt_mailbox_t mb, rt_ubase_t *value, rt_int32_t timeout)
{
    struct rt_thread *thread;
    register rt_ubase_t temp;
    rt_uint32_t tick_delta;

    tick_delta = 0;

    thread = rt_thread_self();

    temp = rt_hw_interrupt_disable();

    /*1 对于非阻塞调用,直接返回-RT_ETIMEOUT */
    if (mb->entry == 0 && timeout == 0)
    {
        rt_hw_interrupt_enable(temp);

        return -RT_ETIMEOUT;
    }

    /*2 邮箱为空 */
    while (mb->entry == 0)
    {
        /* reset error number in thread */
        thread->error = RT_EOK;

         /* 重置线程错误码 */
        if (timeout == 0)
        {
            rt_hw_interrupt_enable(temp);

            thread->error = -RT_ETIMEOUT;

            return -RT_ETIMEOUT;
        }

        RT_DEBUG_IN_THREAD_CONTEXT;
        /* 挂起当前线程 */
        _ipc_list_suspend(&(mb->parent.suspend_thread),
                            thread,
                            mb->parent.parent.flag);

        /* 有等待时间(非永久等待),启动线程内置定时器 */
        if (timeout > 0)
        {
            tick_delta = rt_tick_get(); // 获取当前系统时间

            /* 重置线程超时时间并开始定时 */
            rt_timer_control(&(thread->thread_timer),
                             RT_TIMER_CTRL_SET_TIME,
                             &timeout);
            rt_timer_start(&(thread->thread_timer));
        }

        rt_hw_interrupt_enable(temp);

        rt_schedule();

        /* 超时时间到了邮箱还是满的 */
        if (thread->error != RT_EOK)
        {
            return thread->error;
        }

        temp = rt_hw_interrupt_disable();

        /* 如果不是永远等待(timeout == -1),则重新计算剩余超时时间 */
        if (timeout > 0)
        {
            tick_delta = rt_tick_get() - tick_delta;
            timeout -= tick_delta;
            if (timeout < 0)
                timeout = 0;
        }
    }

    /* 向接收邮件指针中填入邮件 */
    *value = mb->msg_pool[mb->out_offset];

    // 邮件出口指针向后移动
    ++ mb->out_offset;
    if (mb->out_offset >= mb->size)
        mb->out_offset = 0;

    /* 邮箱总量-1 */
    if(mb->entry > 0)
    {
        mb->entry --;
    }

    // 如果发送线程阻塞链表不为空,则唤醒该阻塞链表中第一个线程,并发起线程调度
    if (!rt_list_isempty(&(mb->suspend_sender_thread)))
    {
        _ipc_list_resume(&(mb->suspend_sender_thread));

        rt_hw_interrupt_enable(temp);

        rt_schedule();

        return RT_EOK;
    }

    rt_hw_interrupt_enable(temp);

    return RT_EOK;
}
  • 邮箱为空:
  • timeout == 0:即无阻塞调用,直接返回-RT_ETIMEOUT
  • timeout < 0(RT_WAITING_FOREVER == -1):永久阻塞挂起直到邮箱非空(有线程发送了邮件)
  • timeout > 0:将线程内置定时器超时时间设为timeout并启动,等待邮箱非空时被唤醒。
  • 邮箱非空:
  • 将队头邮件填入到接收指针*value中,然后mb->out_offset向后移动,邮件总量mb->entry-1。
  • 如果发送线程阻塞链表不为空,则唤醒该阻塞链表中第一个线程,并发起线程调度。

4 应用示例

/*
 * Date          Author
 * 2022-02-12    issac wan
 */
#include <rtthread.h>
#define my_printf(fmt, ...)         rt_kprintf("[%u]"fmt"\n", rt_tick_get(), ##__VA_ARGS__)

#define THREAD_STACK_SIZE     512
#define THREAD_PRIORITY       20
#define THREAD_TIMESLICE      5

struct send_mb
{
    rt_uint32_t a;
    char* str;
};
struct send_mb urgent_mb_struct =
{
    .a = 520,
    .str = "logic"
};

static struct rt_mailbox mb;    // 静态邮箱对象
static char mb_pool[256];       // 静态邮箱缓冲区
static const char* common_mb_str1 = "common_mb_str1";
static const char* common_mb_str2 = "common_mb_str2";
static const char* urgent_mb_str = "urgent_mb_str";
static const char* over_mb_str = "over";

static rt_sem_t sem;

static rt_thread_t thread1;
static rt_thread_t thread2;

static void thread1_entry(void* param)
{
    void* ret_str;
    while(1)
    {
        if (RT_EOK == rt_mb_recv(&mb, (rt_uint32_t *)&ret_str, RT_WAITING_FOREVER))
        {
            if (ret_str == urgent_mb_str)
            {
                my_printf("%s", (char*)ret_str);
                rt_sem_take(sem, RT_WAITING_FOREVER);
                // 信号量资源同步后,说明收到了结构体紧急邮件,因此非阻塞获取
                rt_mb_recv(&mb, (rt_uint32_t *)&ret_str, 0);
                my_printf("%d %s", ((struct send_mb*)ret_str)->a, ((struct send_mb*)ret_str)->str);
            }
            else if (ret_str == "over")
                break;
            else
            my_printf("%s", (char*)ret_str);
        }
        rt_thread_mdelay(200);
    }
    my_printf("%s", (char*)ret_str);
    rt_mb_detach(&mb);  // 将邮箱对象脱离
    rt_sem_delete(sem); // 删除信号量对象
}

static void thread2_entry(void* param)
{
    rt_uint8_t cnt = 0;
    while(cnt++ < 10)
    {
       if (cnt & 0x1)  //  奇数
           rt_mb_send(&mb, (rt_uint32_t)common_mb_str1);
       else            //  偶数
           rt_mb_send(&mb, (rt_uint32_t)common_mb_str2);

        rt_thread_mdelay(100);
    }
    rt_mb_urgent(&mb, (rt_uint32_t)urgent_mb_str);    // 发送紧急邮件
    rt_thread_mdelay(300);      // 等待线程1收到紧急邮件
    rt_mb_urgent(&mb, (rt_uint32_t)&urgent_mb_struct);
    rt_sem_release(sem);        // 释放资源唤醒线程1

    rt_mb_send(&mb, (rt_uint32_t)over_mb_str);
}

int mailbox_sample(void){
    rt_err_t result;
    /* 静态初始化邮箱 */
    result = rt_mb_init(&mb,
                        "test_mb",
                        mb_pool,
                        sizeof(mb_pool) / 4,   // 邮箱中的邮件数目(一封邮件占4字节 )
                        RT_IPC_FLAG_FIFO);
    if (result != RT_EOK)
        return -RT_ERROR;

    sem = rt_sem_create("sem", 0, RT_IPC_FLAG_PRIO);

    thread1 = rt_thread_create("thread1",
                                thread1_entry,
                                RT_NULL,
                                THREAD_STACK_SIZE,
                                THREAD_PRIORITY - 1,
                                THREAD_TIMESLICE);
    if (thread1 == RT_NULL)
        return -RT_ERROR;
    else
        rt_thread_startup(thread1);


    thread2 = rt_thread_create("thread2",
                                thread2_entry,
                                RT_NULL,
                                THREAD_STACK_SIZE,
                                THREAD_PRIORITY,
                                THREAD_TIMESLICE);
    if (thread2 == RT_NULL)
        return -RT_ERROR;
    else
        rt_thread_startup(thread2);

    return RT_EOK;
}
INIT_APP_EXPORT(mailbox_sample);
  • 线程1每200ms接收一封邮件,线程2每100ms发送一封普通邮件
  • 当线程2普通邮件发送完后,会发生一封紧急邮件,通知线程1接下来要发送结构体变量
  • 使用信号量进行同步,线程2成功发送结构体变量后,会释放信号量,唤醒线程1,此时线程1采用非阻塞的方式收取该邮件。

打印结果如下:

[3]common_mb_str1
[205]common_mb_str2
[407]common_mb_str1
[609]common_mb_str2
[811]common_mb_str1
[1013]common_mb_str2
[1215]urgent_mb_str
[1417]520 logic
[1618]common_mb_str1
[1820]common_mb_str2
[2022]common_mb_str1
[2224]common_mb_str2
[2426]over

5 总结

  • 邮箱用于线程间异步通信,每封邮件4字节,可以用指针变量来接收邮件,来访问其数据缓冲区,实现不定长、任意类型的数据接收。
  • 邮箱缓冲区为循环队列(尾插头删),元素大小4字节。mb->msg_pool为队列缓冲区首地址,mb->size为队列最大容量,mb->out_offset为队头,mb->in_offset为队尾,用mb->entry记录队列元素个数。
  • mb->entry == 0表示邮箱空,mb->entry == mb->size表示邮箱满
  • 发送邮件的线程阻塞挂起在mb->suspend_sender_thread链表上,接收邮件的线程阻塞挂起在mb->parent.suspend_thread链表上。
  • 静态初始化的邮箱需要先创建好缓冲区,将其地址传入
  • 发送邮箱可以选择阻塞与非阻塞方式,其中紧急发送属于非阻塞,使队头指针mb->out_offset移动到其前一个位置,在此处插入邮件,接收线程可以直接从队头取走邮件。
  • 发送局部变量时,需要注意其生命周期,如果线程或函数被系统回收了,那么邮件接收者使用的接收指针,会指向非法内存,成了野指针。

END