在之前的博客中,学习了有关线程管理以及线程间同步的相关内容。了解到了线程的不同状态,线程的优先级,主线程以及空闲线程,钩子等概念,然后还了解到了信号量、互斥量以及事件集实现线程间同步的方法。在这一篇博客中,开始学习线程间通信的相关内容。
主要包括3个概念:
1. 邮箱
顾名思义,邮箱这种线程间通信的方式就是线程1发出msg到邮箱,线程2在邮箱中获得该msg。当然,也可以拓展到多个线程之间的通信。由于每封邮件为4个字节,所以邮箱能用于不超过4个字节的消息传递。由于在32位系统中,4字节的内容恰好可以放一个指针,所以当线程需要发送较大内容时,可以将该指针(地址)发送出去。
1.1 邮箱工作机制
特点:
非阻塞发送邮件可以安全地应用于中断服务,是线程、中断服务以及定时器向线程发送消息的有效手段。阻塞接收邮件,只有在邮箱里有邮件,并且满足接收时间timeout的要求时,才可以接收邮件。
发送邮件线程:
1.邮箱没满,把邮件复制到邮箱;
2.邮箱满了,挂起等待或者等待timeout.
接收邮件线程:
1.邮箱非空,复制邮箱中的4个字节邮件到接收缓存中;
2.邮箱为空,挂起等待直到收到邮件或者设置超时等待,直到timeout还没收到邮件,返回-RT_EFULL.
1.2 邮箱控制块
用于管理邮箱的数据结构struct rt_mailbox,其对象由rt_ipc_object派生,用rt_mailbox_t表示邮箱句柄。
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);
创建邮箱对象时会先从对象管理器中分配一个邮箱对象,然后给邮箱动态分配一块内存空间用来存放邮件,这块内存的大小等于邮件大小(4 字节)与邮箱容量的乘积,接着初始化接收邮件数目和发送邮件在邮箱中的偏移量。
当用 rt_mb_create() 创建的邮箱不再被使用时,应该删除它来释放相应的系统资源,一旦操作完成,
邮箱将被永久性的删除。
rt_err_t rt_mb_delete (rt_mailbox_t mb);
删除邮箱时,如果有线程被挂起在该邮箱对象上,内核先唤醒挂起在该邮箱上的所有线程(线程返回值是 - RT_ERROR),然后再释放邮箱使用的内存,最后删除邮箱对象。
1.3.2 初始化和脱离邮箱
邮箱的初始化和互斥量的初始化、信号量的初始化概念都很类似,都是用于静态的初始化。且内存都在系统编译时由系统分配,一般放在读写数据段或未初始化数据段。使用的接口是:
rt_err_t rt_mb_init(rt_mailbox_t mb,
const char* name,
void* msgpool,
rt_size_t size,//邮箱的容量,msgpool指向的缓冲区的字节数/4
rt_uint8_t flag)
脱离邮箱将把静态初始化的邮箱对象从内核对象管理器中脱离:
rt_err_t rt_mb_detach(rt_mailbox_t mb);
使用该函数接口后,内核先唤醒所有挂在该邮箱上的线程(线程获得返回值是 - RT_ERROR),然后将该邮箱对象从内核对象管理器中脱离。(类似删除,但是没有系统资源释放,只是从内核对象管理器中脱离)
1.3.3 发送邮件
线程或者中断服务程序可以通过邮箱给其他线程发送邮件,邮件可以是 32 位任意格式的数据,一个整型值或者一个指向缓冲区的指针。当邮箱中的邮件已经满时,发送邮件的线程或者中断程序会收到 -RT_EFULL 的返回值:
rt_err_t rt_mb_send (rt_mailbox_t mb, rt_uint32_t value);
1.3.4 等待方式发送邮件
向指定邮箱发送邮件:
rt_err_t rt_mb_send_wait (rt_mailbox_t mb,
rt_uint32_t value,
rt_int32_t timeout);
与rt_mb_send()不同的是:有等待时间。也即,在等待的时间内,有空间就发送,否则到达超时timeout,返回错误码。
1.3.5 接收邮件
接收邮件接口就不区分立即接受和超时接收了,就一个接口,里面有timeout的参数,有邮件则立即接收,否则等到超时返回-RT_ETIMEOUT。需要注意的是:接收邮件时,接收者需指定接收邮件的句柄,并指定接收到的邮件存放位置以及最多能够等待的超时时间。
rt_err_t rt_mb_recv (rt_mailbox_t mb, rt_uint32_t* value, rt_int32_t timeout);
邮箱的拓展,可以接收不定长度的消息。主要用于线程间消息交换以及接收串口不定长度的数据等。是一种异步通信机制。需要注意的是中断服务历程可以发送消息,但是不能接收消息。
2.1 消息队列的工作机制
线程或中断服务例程可以将一条或多条消息放入消息队列中。同样,一个或多个线程也可以从消息队列中获得消息。当有多个消息发送到消息队列时,通常将先进入消息队列的消息先传给线程(FIFO方式)。其工作示意图如图所示:
RT-Thread 操作系统的消息队列对象由多个元素组成,当消息队列被创建时,它就被分配了消息队列控制块:消息队列名称、内存缓冲区、消息大小以及队列长度等。同时每个消息队列对象中包含着多个消息框,每个消息框可以存放一条消息;消息队列中的第一个和最后一个消息框被分别称为消息链表头和消息链表尾,对应于消息队列控制块中的 msg_queue_head 和 msg_queue_tail;有些消息框可能是空的,它们通过 msg_queue_free 形成一个空闲消息框链表。所有消息队列中的消息框总数即是消息队列的长度,这个长度可在消息队列创建时指定。
2.2 消息队列控制块
管理消息队列的数据结构,由 struct rt_messagequeue 表示,其对象由rt_ipc_object派生,由IPC容器管理。用rt_mq_t表示消息队列的句柄。
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; /* 空 闲 消 息 链 表 */
rt_list_t suspend_sender_thread; /* 发 送 线 程 的 挂 起 等 待 队 列 */
};
typedef struct rt_messagequeue* rt_mq_t;
2.3 消息队列管理方式
对一个消息队列的操作包含:创建消息队列 - 发送消息 - 接收消息 - 删除消息队列。
2.3.1 创建和删除消息队列
动态创建使用的接口是:
rt_mq_t rt_mq_create(const char* name,//消息队列名称
rt_size_t msg_size,//一条消息的最大长度
rt_size_t max_msgs,//消息队列最大个数
rt_uint8_t flag//FIFO:RT_IPC_FLAG_FIFO或着优先级:RT_IPC_FLAG_PRIO
);
创建消息队列时先从对象管理器中分配一个消息队列对象,然后给消息队列对象分配一块内存空间,组织成空闲消息链表,这块内存的大小 =[消息大小 + 消息头(用于链表连接)的大小]X 消息队列最大个数,接着再初始化消息队列,此时消息队列为空。
删除使用的接口是:
rt_err_t rt_mq_delete(rt_mq_t mq);
删除时将释放系统资源,一旦操作,消息队列将被永久性地删除。
删除消息队列时,如果有线程被挂起在该消息队列等待队列上,则内核先唤醒挂起在该消息等待队列上的所有线程(线程返回值是 - RT_ERROR),然后再释放消息队列使用的内存,最后删除消息队列对象。(类似邮箱)
2.3.2 初始化和脱离消息队列
初始化静态消息队列对象跟创建消息队列对象类似,只是静态消息队列对象的内存是在系统编译时由编译器分配的,一般放于读数据段或未初始化数据段中。在使用这类静态消息队列对象前,需要进行初始化。(类似邮件、信号量等)
rt_err_t rt_mq_init(rt_mq_t mq, const char* name,
void *msgpool, rt_size_t msg_size,
rt_size_t pool_size, rt_uint8_t flag);
其中pool_size指的是存放消息的缓冲区大小,其余参数含义参考邮箱。
脱离消息队列将使消息队列对象被从内核对象管理器中脱离。使用的接是:
rt_err_t rt_mq_detach(rt_mq_t mq);
使用该函数接口后,内核先唤醒所有挂在该消息等待队列对象上的线程(线程返回值是 -RT_ERROR),然后将该消息队列对象从内核对象管理器中脱离。(参考邮箱等的脱离含义及步骤)
2.3.3 发送消息
线程或者中断服务程序都可以给消息队列发送消息。当发送消息时,消息队列对象先从空闲消息链表上取下一个空闲消息块,把线程或者中断服务程序发送的消息内容复制到消息块上,然后把该消息块挂到消息队列的尾部。当且仅当空闲消息链表上有可用的空闲消息块时,发送者才能成功发送消息;否则,当空闲消息链表上无可用消息块,说明消息队列已满,此时,发送消息的的线程或者中断程序会收到一个错误码(-RT_EFULL)。发送消息的函数接口如下:
rt_err_t rt_mq_send (rt_mq_t mq, void* buffer, rt_size_t size);
2.3.4 等待方式发送消息
与邮箱类似,消息的发送也有等待的方式,其含义同邮箱。使用的接口是:
rt_err_t rt_mq_send_wait(rt_mq_t mq,
const void *buffer,
rt_size_t size,
rt_int32_t timeout);
2.3.5 发送紧急消息
与邮箱发送不同的是:消息的发送,有紧急消息的方式。其方式和消息的发送类似,所不同的是:**从空闲消息链表上取下来的消息块不是挂到消息队列的队尾,而是挂到队首,这样接收者就能优先接收到紧急消息,从而及时进行消息处理。**使用的接口是:
rt_err_t rt_mq_urgent(rt_mq_t mq, void* buffer, rt_size_t size);
2.3.6 接收消息
当消息队列中有消息时,接收者才能接收消息,否则接收者会根据超时时间设置,或挂起在消息队列的等待线程队列上,或直接返回。接收消息函数接口如下:
rt_err_t rt_mq_recv (rt_mq_t mq, void* buffer,
rt_size_t size, rt_int32_t timeout);
参数含义一目了然,不赘述。
接收消息时,接收者需指定存储消息的消息队列对象句柄mq,并且指定一个内存缓冲区buffer,接收到的消息内容将被复制到该缓冲区里。此外,还需指定未能及时取到消息时的超时时间。同时接受一个消息后,消息队列上的队首消息被转移到了空闲消息链表的尾部。
消息队列是直接的数据内容复制,所以在上面的例子中,都采用了局部变量的方式保存消息结构体,这样也就免去动态内存分配的烦恼了(也就不用担心,接收线程在接收到消息时,消息内存空间已经被释放)。
对于需要发送同步消息的问题,就可以根据当时状态的不同选择相应的实现:两个线程间可以采用 [消息队列 + 信号量或邮箱] 的形式实现。发送线程通过消息发送的形式发送相应的消息给消息队列,发送完毕后希望获得接收线程的收到确认,工作示意图如下图所示:
信号(又称为软中断信号),在软件层次上是对中断机制的一种模拟,在原理上,一个线程收到一个信号与处理器收到一个中断请求可以说是类似的。
3.1 信号的工作机制
用作异步通信(发送方与接收方不需要事先同步时钟信号,而同步需要通信双方必须先建立同步,即双方的时钟要调整到同一个频率。)
POSIX 标准定义了 sigset_t 类型来定义一个信号集,然而 sigset_t类型在不同的系统可能有不同的定义方式,在 RT-Thread 中,将 sigset_t 定义成了 unsigned long 型,并命名为 rt_sigset_t,应用程序能够使用的信号为 SIGUSR1(10)和 IGUSR2(12)。
收到信号的线程对各种信号有不同的处理方法,处理方法可以分为三类:(参考3.2.1信号的安装参数handler)
如下图所示,假设线程 1 需要对信号进行处理,首先线程 1 安装一个信号并解除阻塞,并在安装的同时设定了对信号的异常处理方式;然后其他线程可以给线程 1 发送信号,触发线程 1 对该信号的处理。
当信号被传递给线程 1 时,如果它正处于挂起状态,那会把状态改为就绪状态去处理对应的信号。如果它正处于运行状态,那么会在它当前的线程栈基础上建立新栈帧空间去处理对应的信号,需要注意的是使用的线程栈大小也会相应增加。
3.2 信号的管理方式
对于信号的操作,有以下几种:安装信号、阻塞信号、阻塞解除、信号发送、信号等待。
安装信号主要用来确定信号值及线程针对该信号值的动作之间的映射关系,即线程将要处理哪个信号,该信号被传递给线程时,将执行何种操作。就是将信号与对应的处理方式进行配对注册。
rt_sighandler_t rt_signal_install(int signo, rt_sighandler_t[] handler);
其中signo为信号值,只有(SIGUSR1和SIGUSR2开放给了用户使用)
3.2.2 阻塞信号
信号阻塞,也可以理解为屏蔽信号。如果该信号被阻塞,则该信号将不会递达给安装此信号的线程,也不会引发软中断处理。调 rt_signal_mask() 可以使信号阻塞:
void rt_signal_mask(int signo);
3.2.3 解除信号阻塞
与阻塞信号相对,使用此函数的时候,信号的阻塞状态被解除,当线程收到对应的信号时,就会产生软中断。
void rt_signal_unmask(int signo);
3.2.4 发送信号
当需要进行异常处理时,可以给设定了处理异常的线程发送信号,调用 rt_thread_kill() 可以用来向任何线程发送信号:
int rt_thread_kill(rt_thread_t tid, int sig);
其中tid表示接收信号的线程。sig表示信号值。
3.2.5 等待信号
等待 set 信号的到来,如果没有等到这个信号,则将线程挂起,直到等到这个信号或者等待时间超过指定的超时时间 timeout。如果等到了该信号,则将指向该信号体的指针存入 si。
int rt_signal_wait(const rt_sigset_t *set,//指定等待的信号
rt_siginfo_t[] *si, //等待信号的类型,指向存储等到信号信息的指针
rt_int32_t timeout);
有关线程通信的部分就到这里了,至此线程相关的内容基本上就结束了,以后还有什么相关的再做更新。