RT-Thread实现了三种线程间通信方式,邮箱(mailbox)、消息队列(messagequeue)、和信号(signal)。
邮箱
邮箱中的每一封邮件只能容纳固定的 4 字节内容。任何线程都可以发送或接受邮件消息。
邮箱控制块
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_offset, out_offset; /* 邮箱缓冲的进出指针 */
rt_list_t suspend_sender_thread; /* 发送线程的挂起等待队列 */
};
typedef struct rt_mailbox* rt_mailbox_t;
创建邮箱
rt_mailbox_t rt_mb_create(const char *name, rt_size_t size, rt_uint8_t flag)
.msg_pool
就是一个循环队列,FIFO,通过.in_offset
,.out_offset
记录进出位置下标。.suspend_sender_thread
当邮箱满时,发送线程会挂在该链上。
发送邮件
rt_err_t rt_mb_send_wait (rt_mailbox_t mb, rt_uint32_t value)
当发送邮件时, 当且仅当邮箱还没满邮件的时候才能进行发送,如果邮箱已满,可以根据用户设定的等待时间进行等待,当邮箱中的邮件被收取而空出空间来时,等待挂起的发送线程将被唤醒继续发送的过程,当等待时间到了还未完成发送邮件,或者未设置等待时间,此时发送邮件失败,发送邮件的线程或者中断程序会收到一个错误码( -RT_EFULL)。如果邮件发送成功,会唤起等待接收邮件的线程。线程发送邮件可以带阻塞,但在中断中不能采用任何带阻塞的方式发送邮件。
接受邮件
rt_err_t rt_mb_recv(rt_mailbox_t mb, rt_ubase_t *value, rt_int32_t timeout)
接收邮件时, 根据邮箱控制块中的 entry 判断队列是否有邮件,如果邮箱的邮件非空,那么可以根据 out_offset 找到最先发送到邮箱中的邮件进行接收。在接收时如果邮箱为空,如果用户设置了等待超时时间,系统会将当前线程挂起,当达到设置的超时时间,邮箱依然未收到邮件时,那么线程将被唤醒并返回-RT_ETIMEOUT。如果邮箱中存在邮件,那么接收线程将复制邮箱中的 4 个字节邮件到接收线程中。如果接收成功,会唤起等待发送的线程。
工程文件
- rtt2a9_mailbox
线程2发送邮件,线程1等待按键事件后接收邮件消息。
消息队列
消息队列是另一种常用的线程间通讯方式,是邮箱的扩展。可以应用在多种场合:线程间的消息交换、使用串口接收不定长数据等。
- 相比于邮箱,消息队列能发送不固定长度的消息,但没有发送线程的挂起列表。
- 支持发送紧急消息,紧急消息会直接加到对头。
消息队列控制块
struct rt_messagequeue
{
struct rt_ipc_object parent;
void* msg_pool; /* 指向存放消息的缓冲区的指针 */
rt_uint16_t msg_size; /* 每个消息的长度 */
rt_uint16_t max_msgs; /* 最大能够容纳的消息数 */
rt_uint16_t entry; /* 队列中已有的消息数 */
void* msg_queue_head; /* 消息链表头 */
void* msg_queue_tail; /* 消息链表尾 */
void* msg_queue_free; /* 空闲消息链表 */
};
typedef struct rt_messagequeue* rt_mq_t;
创建消息队列
rt_mq_t rt_mq_create(const char *name,
rt_size_t msg_size,
rt_size_t max_msgs,
rt_uint8_t flag);
.msg_size
要注意对齐。创建后,整个链表都是空闲块,.msg_queue_free
指向表头,.msg_queue_head
和.msg_queue_tail
指向RT_NULL
。
发送消息
rt_err_t rt_mq_send(rt_mq_t mq, void *buffer, rt_size_t size);
从空闲链表中摘下.msg_queue_free
指向的空闲块,加到消息队列的尾部。如果消息队列为空,则更新.msg_queue_head
和.msg_queue_tail
。发送紧急消息时,把消息直接加到对头。
接收消息
rt_err_t rt_mq_recv(rt_mq_t mq,
void *buffer,
rt_size_t size,
rt_int32_t timeout);
通过.entry
判断队列中是否有消息,没有,则挂起或直接返回。有,则从对头取出消息,拷贝数据后将内存块挂到空闲队列。
工程文件
线程2发送消息,消息中带一个信号量,用于线程1接收到消息后通知线程2继续发送消息。
信号
信号(又称为软中断信号),在软件层次上是对中断机制的一种模拟,在原理上,一个线程收到一个信号与处理器收到一个中断请求可以说是类似的。
信号相关结构体:
struct siginfo
{
rt_uint16_t si_signo;
rt_uint16_t si_code;
union sigval si_value;
};
typedef struct siginfo siginfo_t;
struct rt_slist_node
{
struct rt_slist_node *next; /**< point to next node. */
};
typedef struct rt_slist_node rt_slist_t; /**< Type for single list. */
struct siginfo_node
{
siginfo_t si; /* 信号值; 信号类型 */
struct rt_slist_node list; /* list.next 指向下一个siginfo_node */
};
static struct rt_mempool *_rt_siginfo_pool;
线程控制块中信号相关属性:
#if defined(RT_USING_SIGNALS)
rt_sigset_t sig_pending; /**< the pending signals */
rt_sigset_t sig_mask; /**< the mask bits of signal */
void *sig_ret; /**< the return stack pointer from signal */
rt_sighandler_t *sig_vectors; /**< vectors of signal handler */
void *si_list; /**< the signal infor list */
#endif
.sig_pending
,32bit表示32中信号,置1表示相关信号发生,处于pending状态。.sig_mask
,屏蔽位,置1,表示对相关信号有兴趣。.sig_ret
,记录原堆栈的栈顶值。.sig_vectors
,类似异常向量表,记录着每个信号的处理函数地址。.si_list
,挂着未处理的siginfo_node
节点
信号初始化
申请一块内存池,每块内存大小为sizeof(struct siginfo_node)
。
int rt_system_signal_init(void){
_rt_siginfo_pool = rt_mp_create("signal", RT_SIG_INFO_MAX, sizeof(struct siginfo_node));
if (_rt_siginfo_pool == RT_NULL)
{
// LOG_E("create memory pool for signal info failed.");
RT_ASSERT(0);
}
return 0;
}
为线程安装信号
rt_sighandler_t rt_signal_install(int signo, rt_sighandler_t handler);
如果线程要处理某一信号,那么就要在线程中安装该信号。该接口根据handler
修改线程的.sig_vectors
表,SIG_IGN
表示忽略该信号,SIG_DFL
表示以系统默认的方式处理信号,或者传入用户指定的处理函数地址。成功返回安装信号前的 handler
值。
阻塞信号
void rt_signal_mask(int signo);
将线程的sig_mask
相应位置0,表示对该信号没兴趣。信号发生后不会处理该信号。
解除信号阻塞
void rt_signal_unmask(int signo);
将线程的sig_mask
相应位置1,表示对该信号有兴趣。在RT-Thread中信号安装时,默认是阻塞的。如果解除信号阻塞后发现该信号是挂起状态,则立刻调用void _signal_deliver(rt_thread_t tid)
传递信号。
_signal_deliver
的功能是:
-
如果目标线程是挂起状态,会唤醒该线程,并设置线程的信号状态位。
-
如果目标线程是就绪态(等着被调度),且目标线程就是当前线程,则调用
void rt_thread_handle_sig(rt_bool_t clean_state);
去处理信号;如果目标线程不是当前线程,则先初始化处理信号函数的上下文,然后rt_schedule()
。
发送信号
int rt_thread_kill(rt_thread_t tid, int sig);
通过此接口向目标线程发送信号。发送信号后线程控制块的信号节点指向。
- 如果该信号未挂起,则挂起,然后申请
siginfo_node
节点,并将该节点挂在tid->si_list
上。 - 如果该信号以挂起,则更新相关信息。
- signal.c: 346
信号处理
void rt_thread_handle_sig(rt_bool_t clean_state);
- 首先判断信号是否挂起,且
tid->sig_mask
位置1。 - 然后从
tid->si_list
链上摘下信号控制节点,从tid->sig_vectors
表中获取处理该信号的函数地址。 - 处理完后清除挂起标志位。
- signal.c: 141
处理信号的时机:
- 线程调度时。会调用
rt_thread_handle_sig(RT_TRUE);
检查线程状态。 - 发送信号时。通过
_signal_deliver(rt_thread_t tid);
传送信号。
工程文件
- 111_rtt2a9_signal
在上一个工程(消息队列)的基础上修改,按键2给线程1发送SIGUSR1
信号。线程1在接收到安装的信号后打印信号值。