邮箱

邮箱服务是实时操作系统中一种典型的任务间通信方法,特点是开销比较低,效率较高。邮箱中的每一封邮件只能容纳固定的4字节内容(针对32位处理系统,指针的大小即为4个字节,所以一封邮件恰好能够容纳一个指针)。典型的邮箱也称作交换消息,如图6-8所示,线程或中断服务例程把一封4字节长度的邮件发送到邮箱中。而一个或多个线程可以从邮箱中接收这些邮件进行处理。

邮箱工作示意图

RT-Thread操作系统采用的邮箱通信机制有点类似于传统意义上的管道,用于线程间通讯。非阻塞方式的邮件发送过程能够安全的应用于中断服务中,是线程,中断服务,定时器向线程发送消息的有效手段。通常来说,邮件收取过程可能是阻塞的,这取决于邮箱中是否有邮件,以及收取邮件时设置的超时时间。当邮箱中不存在邮件且超时时间不为0时,邮件收取过程将变成阻塞方式。所以在这类情况下,只能由线程进行邮件的收取。

RT-Thread操作系统的邮箱中可存放固定条数的邮件,邮箱容量在创建/初始化邮箱时设定,每个邮件大小为4字节。当需要在线程间传递比较大的消息时,可以把指向一个缓冲区的指针作为邮件发送到邮箱中。

在一个线程向邮箱发送邮件时,如果邮箱没满,将把邮件复制到邮箱中。如果邮箱已经满了,发送线程可以设置超时时间,选择是否等待挂起或直接返回-RT_EFULL。如果发送线程选择挂起等待,那么当邮箱中的邮件被收取而空出空间来时,等待挂起的发送线程将被唤醒继续发送的过程。

在一个线程从邮箱中接收邮件时,如果邮箱是空的,接收线程可以选择是否等待挂起直到收到新的邮件而唤醒,或设置超时时间。当达到设置的超时时间,邮箱依然未收到邮件时,这个选择超时等待的线程将被唤醒并返回-RT_ETIMEOUT。如果邮箱中存在邮件,那么接收线程将复制邮箱中的4个字节邮件到接收线程中。

邮箱控制块

  1. struct rt_mailbox
  2. {
  3. struct rt_ipc_object parent;
  4.  
  5. rt_uint32_t* msg_pool; /* 邮箱缓冲区的开始地址 */
  6. rt_uint16_t size; /* 邮箱缓冲区的大小 */
  7.  
  8. rt_uint16_t entry; /* 邮箱中邮件的数目 */
  9. rt_uint16_t in_offset, out_offset; /* 邮箱缓冲的进出指针 */
  10. rt_list_t suspend_sender_thread; /* 发送线程的挂起等待队列 */
  11. };
  12. typedef struct rt_mailbox* rt_mailbox_t;

rt_mailbox对象从rt_ipc_object中派生,由IPC容器管理。

邮箱相关接口

创建邮箱

创建邮箱对象可以调用如下的函数接口:

  1. rt_mailbox_t rt_mb_create (const char* name, rt_size_t size, rt_uint8_t flag);

创建邮箱对象时会先创建一个邮箱对象控制块,然后给邮箱分配一块内存空间用来存放邮件,这块内存的大小等于邮件大小(4字节)与邮箱容量的乘积,接着初始化接收邮件和发送邮件在邮箱中的偏移量。

函数参数


  1. 参数 描述

  1. name 邮箱名称;
  2.  
  3. size 邮箱容量;
  4.  
  5. flag 邮箱标志,它可以取如下数值:

  1. #define RT_IPC_FLAG_FIFO 0x00 /* IPC参数采用FIFO方式*/
  2. #define RT_IPC_FLAG_PRIO 0x01 /* IPC参数采用优先级方式*/

函数返回

创建成功返回邮箱对象的句柄;否则返回-RT_NULL。

删除邮箱

当邮箱不再被使用时,应该删除它来释放相应的系统资源,一旦操作完成,邮箱将被永久性的删除。删除邮箱的函数接口如下:

  1. rt_err_t rt_mb_delete (rt_mailbox_t mb);

删除邮箱时,如果有线程被挂起在该邮箱对象上,内核先唤醒挂起在该邮箱上的所有线程(线程获得返回值是-RT_ERROR),然后再释放邮箱使用的内存,最后删除邮箱对象。

函数参数


  1. 参数 描述

  1. mb 邮箱对象的句柄。

函数返回

RT_EOK

初始化邮箱

初始化邮箱跟创建邮箱类似,只是初始化邮箱用于静态邮箱对象的初始化。其他与创建邮箱不同的是,此处静态邮箱对象所使用的内存空间是由用户线程指定的一个缓冲区空间,用户把缓冲区的指针传递给邮箱对象控制块,其余的初始化工作与创建邮箱时相同。函数接口如下:

  1. rt_err_t rt_mb_init(rt_mailbox_t mb, const char* name, void* msgpool,
  2. rt_size_t size, rt_uint8_t flag)

初始化邮箱时,该函数接口需要获得用户已经申请获得的邮箱对象控制块,缓冲区的指针,以及邮箱名称和邮箱容量。

函数参数


  1. 参数 描述

  1. mb 邮箱对象的句柄;
  2.  
  3. name 邮箱名称;
  4.  
  5. msgpool 缓冲区指针;
  6.  
  7. size 邮箱容量;
  8.  
  9. flag 邮箱标志,它可以取如下数值:

  1. #define RT_IPC_FLAG_FIFO 0x00 /* IPC参数采用FIFO方式*/
  2. #define RT_IPC_FLAG_PRIO 0x01 /* IPC参数采用优先级方式*/

函数返回

RT_EOK

  • 注:这里的size参数指定的是邮箱的容量,即如果msgpool的字节数是N,那么邮箱容量应该是N/4。

    脱离邮箱

脱离邮箱将把邮箱对象从内核对象管理器中删除。脱离邮箱使用下面的接口:

  1. rt_err_t rt_mb_detach(rt_mailbox_t mb);

使用该函数接口后,内核先唤醒所有挂在该邮箱上的线程(线程获得返回值是- RT_ERROR ),然后将该邮箱对象从内核对象管理器中删除。

函数参数


  1. 参数 描述

  1. mb 邮箱对象的句柄。

函数返回

RT_EOK

发送邮件

线程或者中断服务程序可以通过邮箱给其他线程发送邮件,发送邮件函数接口如下:

  1. rt_err_t rt_mb_send (rt_mailbox_t mb, rt_uint32_t value);

发送的邮件可以是32位任意格式的数据,一个整型值或者一个指向缓冲区的指针。当邮箱中的邮件已经满时,发送邮件的线程或者中断程序会收到-RT_EFULL 的返回值。

函数参数


  1. 参数 描述

  1. mb 邮箱对象的句柄;
  2.  
  3. value 邮件内容。

函数返回

发送成功返回RT_EOK;如果邮箱已经满了,返回-RT_EFULL。

等待方式发送邮件

用户也可以通过如下的函数接口向指定邮箱发送邮件:

  1. rt_err_t rt_mb_send_wait (rt_mailbox_t mb, rt_uint32_t value, rt_int32_t timeout);

rt_mb_send_wait与rt_mb_send的区别在于,如果邮箱已经满了,那么发送线程将根据设定的timeout参数等待邮箱中因为收取邮件而空出空间。如果设置的超时时间到达依然没有空出空间,这时发送线程将被唤醒返回错误码。

函数参数


  1. 参数 描述

  1. mb 邮箱对象的句柄;
  2.  
  3. value 邮件内容;
  4.  
  5. timeout 超时时间。

函数返回

发送成功返回RT_EOK;如果设置的时间超时依然未发送成功,返回-RT_ETIMEOUT,其他情况返回-RT_ERROR。

接收邮件

只有当接收者接收的邮箱中有邮件时,接收者才能立即取到邮件并返回RT_EOK的返回值,否则接收线程会根据超时时间设置,或挂起在邮箱的等待线程队列上,或直接返回。接收邮件函数接口如下:

  1. rt_err_t rt_mb_recv (rt_mailbox_t mb, rt_uint32_t* value, rt_int32_t timeout);

接收邮件时,接收者需指定接收邮件的邮箱句柄,并指定接收到的邮件存放位置以及最多能够等待的超时时间。如果接收时设定了超时,当指定的时间内依然未收到邮件时,将返回-RT_ETIMEOUT。

函数参数


  1. 参数 描述

  1. mb 邮箱对象的句柄;
  2.  
  3. value 邮件内容;
  4.  
  5. timeout 超时时间。

函数返回

成功收到返回RT_EOK,超时返回-RT_ETIMEOUT,其他返回-RT_ERROR。

使用邮箱的例程如下例所示:

  1. /*
  2. * 程序清单:邮箱例程
  3. *
  4. * 这个程序会创建2个动态线程,一个静态的邮箱对象,其中一个线程往邮箱中发送邮件,
  5. * 一个线程从邮箱中收取邮件。
  6. */
  7. #include <rtthread.h>
  8. #include "tc_comm.h"
  9.  
  10. /* 指向线程控制块的指针 */
  11. static rt_thread_t tid1 = RT_NULL;
  12. static rt_thread_t tid2 = RT_NULL;
  13.  
  14. /* 邮箱控制块 */
  15. static struct rt_mailbox mb;
  16. /* 用于放邮件的内存池 */
  17. static char mb_pool[128];
  18.  
  19. static char mb_str1[] = "I'm a mail!";
  20. static char mb_str2[] = "this is another mail!";
  21.  
  22. /* 线程1入口 */
  23. static void thread1_entry(void* parameter)
  24. {
  25. unsigned char* str;
  26.  
  27. while (1)
  28. {
  29. rt_kprintf("thread1: try to recv a mail\n");
  30.  
  31. /* 从邮箱中收取邮件 */
  32. if (rt_mb_recv(&mb, (rt_uint32_t*)&str, RT_WAITING_FOREVER)
  33. == RT_EOK)
  34. {
  35. /* 显示邮箱内容 */
  36. rt_kprintf("thread1: get a mail, the content:%s\n", str);
  37.  
  38. /* 延时10个OS Tick */
  39. rt_thread_delay(10);
  40. }
  41. }
  42. }
  43.  
  44. /* 线程2入口 */
  45. static void thread2_entry(void* parameter)
  46. {
  47. rt_uint8_t count;
  48.  
  49. count = 0;
  50. while (1)
  51. {
  52. count ++;
  53. if (count & 0x1)
  54. {
  55. /* 发送mb_str1地址到邮箱中 */
  56. rt_mb_send(&mb, (rt_uint32_t)&mb_str1[0]);
  57. }
  58. else
  59. {
  60. /* 发送mb_str2地址到邮箱中 */
  61. rt_mb_send(&mb, (rt_uint32_t)&mb_str2[0]);
  62. }
  63.  
  64. /* 延时20个OS Tick */
  65. rt_thread_delay(20);
  66. }
  67. }
  68.  
  69. int mbox_simple_init()
  70. {
  71. /* 初始化一个mailbox */
  72. rt_mb_init(&mb,
  73. "mbt", /* 名称是mbt */
  74. &mb_pool[0], /* 邮箱用到的内存池是mb_pool */
  75. sizeof(mb_pool)/4, /* 大小是mb_pool/4,因为每封邮件的大小是4字节 */
  76. RT_IPC_FLAG_FIFO); /* 采用FIFO方式进行线程等待 */
  77.  
  78. /* 创建线程1 */
  79. tid1 = rt_thread_create("t1",
  80. thread1_entry, /* 线程入口是thread1_entry */
  81. RT_NULL, /* 入口参数是RT_NULL */
  82. THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE);
  83. if (tid1 != RT_NULL)
  84. rt_thread_startup(tid1);
  85. else
  86. tc_stat(TC_STAT_END | TC_STAT_FAILED);
  87.  
  88. /* 创建线程2 */
  89. tid2 = rt_thread_create("t2",
  90. thread2_entry, /* 线程入口是thread2_entry */
  91. RT_NULL, /* 入口参数是RT_NULL */
  92. THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE);
  93. if (tid2 != RT_NULL)
  94. rt_thread_startup(tid2);
  95. else
  96. tc_stat(TC_STAT_END | TC_STAT_FAILED);
  97.  
  98. return 0;
  99. }
  100.  
  101. #ifdef RT_USING_TC
  102. static void _tc_cleanup()
  103. {
  104. /* 调度器上锁,上锁后,将不再切换到其他线程,仅响应中断 */
  105. rt_enter_critical();
  106.  
  107. /* 删除线程 */
  108. if (tid1 != RT_NULL && tid1->stat != RT_THREAD_CLOSE)
  109. rt_thread_delete(tid1);
  110. if (tid2 != RT_NULL && tid2->stat != RT_THREAD_CLOSE)
  111. rt_thread_delete(tid2);
  112.  
  113. /* 执行邮箱对象脱离 */
  114. rt_mb_detach(&mb);
  115.  
  116. /* 调度器解锁 */
  117. rt_exit_critical();
  118.  
  119. /* 设置TestCase状态 */
  120. tc_done(TC_STAT_PASSED);
  121. }
  122.  
  123. int _tc_mbox_simple()
  124. {
  125. /* 设置TestCase清理回调函数 */
  126. tc_cleanup(_tc_cleanup);
  127. mbox_simple_init();
  128.  
  129. /* 返回TestCase运行的最长时间 */
  130. return 100;
  131. }
  132. /* 输出函数命令到finsh shell中 */
  133. FINSH_FUNCTION_EXPORT(_tc_mbox_simple, a simple mailbox example);
  134. #else
  135. /* 用户应用入口 */
  136. int rt_application_init()
  137. {
  138. mbox_simple_init();
  139.  
  140. return 0;
  141. }
  142. #endif

使用场合

邮箱是一种简单的线程间消息传递方式,在RT-Thread操作系统的实现中能够一次传递4字节邮件,并且邮箱具备一定的存储功能,能够缓存一定数量的邮件数(邮件数由创建、初始化邮箱时指定的容量决定)。邮箱中一封邮件的最大长度是4字节,所以邮箱能够用于不超过4字节的消息传递,当传送的消息长度大于这个数目时就不能再采用邮箱的方式。最重要的是,在32位系统上4字节的内容恰好适合放置一个指针,所以邮箱也适合那种仅传递指针的情况,例如:

  1. struct msg
  2. {
  3. rt_uint8_t *data_ptr;
  4. rt_uint32_t data_size;
  5. };

对于这样一个消息结构体,其中包含了指向数据的指针data_ptr和数据块长度的变量data_size。当一个线程需要把这个消息发送给另外一个线程时,可以采用如下的操作:

  1. struct msg* msg_ptr;
  2.  
  3. msg_ptr = (struct msg*)rt_malloc(sizeof(struct msg));
  4. msg_ptr->data_ptr = ...; /* 指向相应的数据块地址*/
  5. msg_ptr->data_size = len; /* 数据块的长度*/
  6. /* 发送这个消息指针给mb邮箱*/
  7. rt_mb_send(mb, (rt_uint32_t)msg_ptr);

而在接收线程中,因为收取过来的是指针,而msg_ptr是一个新分配出来的内存块,所以在接收线程处理完毕后,需要释放相应的内存块:

  1. struct msg* msg_ptr;
  2. if (rt_mb_recv(mb, (rt_uint32_t*)&msg_ptr) == RT_EOK)
  3. {
  4. /* 在接收线程处理完毕后,需要释放相应的内存块*/
  5. rt_free(msg_ptr);
  6. }