RT-Thread内核学习(认真系列) ---- (4)线程间通信

 

一、概述

二、邮箱

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

RT-Thread内核学习(认真系列) ---- (4)线程间通信_第1张图片

非阻塞方式(等待时间为0)的邮件发送过程能够安全的应用于中断服务中,是线程、中断服务、定时器向线程发送消息的有效手段。通常来说,邮件收取过程可能是阻塞的,这取决于邮箱中是否有邮件,以及收取邮件时设置的超时时间。当邮箱中不存在邮件且超时时间不为 0 时,邮件收取过程将变成阻塞方式。在这类情况下,只能由线程进行邮件的收取

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

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

2.1、API

2.1.1、创建和删除邮箱

rt_mailbox_t rt_mb_create (const char* name, 
                           rt_size_t size,   //邮箱容量(每个邮件为4个字节)
                           rt_uint8_t flag); //邮箱标志,它可以取如下数值:RT_IPC_FLAG_FIFO 或
                                             //                          RT_IPC_FLAG_PRIO

rt_err_t rt_mb_delete (rt_mailbox_t mb);

 

2.1.2、初始化和脱离邮箱

rt_err_t rt_mb_init(rt_mailbox_t mb,  //邮箱对象句柄(指针),记得加&
                    const char* name,
                    void* msgpool,    //缓冲区指针
                    rt_size_t size,   //邮箱容量(其中每个邮件4个字节)
                    rt_uint8_t flag)  

rt_err_t rt_mb_detach(rt_mailbox_t mb);

2.1.3、发送邮件

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

非等待发送:

rt_err_t rt_mb_send (rt_mailbox_t mb, rt_uint32_t value);

等待方式发送:

rt_err_t rt_mb_send_wait (rt_mailbox_t mb,
                          rt_uint32_t value,
                          rt_int32_t timeout); //等待时间

邮箱发送后,会占据邮箱一个位置。 

2.1.4、接收邮件

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

rt_err_t rt_mb_recv (rt_mailbox_t mb, 
                     rt_uint32_t* value, //接受邮件内容,所以用到指针
                     rt_int32_t timeout);

邮箱接收后,就会腾出一个邮箱的位置。 

2.2、使用场合

邮箱是一种简单的线程间消息传递方式,特点是开销比较低,效率较高。在 RT-Thread 操作系统的实现中能够一次传递一个 4 字节大小的邮件,并且邮箱具备一定的存储功能,能够缓存一定数量的邮件数 (邮件数由创建、初始化邮箱时指定的容量决定)。邮箱中一封邮件的最大长度是 4 字节,所以邮箱能够用于不超过 4 字节的消息传递。由于在 32 系统上 4 字节的内容恰好可以放置一个指针,因此当需要在线程间传递比较大的消息时,可以把指向一个缓冲区的指针作为邮件发送到邮箱中,即邮箱也可以传递指针,例如:

struct msg
{
    rt_uint8_t *data_ptr;
    rt_uint32_t data_size;
};

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

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

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

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

三、消息队列

消息队列能够接收来自线程或中断服务例程中不固定长度的消息,并把消息缓存在自己的内存空间中。其他线程也能够从消息队列中读取相应的消息,而当消息队列是空的时候,可以挂起读取线程。当有新的消息到达时,挂起的线程将被唤醒以接收并处理消息。消息队列是一种异步的通信方式。

如下图所示,线程或中断服务例程可以将一条或多条消息放入消息队列中。同样,一个或多个线程也可以从消息队列中获得消息。当有多个消息发送到消息队列时,通常将先进入消息队列的消息先传给线程,也就是说,线程先得到的是最先进入消息队列的消息,即先进先出原则 (FIFO)

RT-Thread内核学习(认真系列) ---- (4)线程间通信_第2张图片

RT-Thread 操作系统的消息队列对象由多个元素组成,当消息队列被创建时,它就被分配了消息队列控制块:消息队列名称、内存缓冲区、消息大小以及队列长度等。同时每个消息队列对象中包含着多个消息框,每个消息框可以存放一条消息;消息队列中的第一个和最后一个消息框被分别称为消息链表头和消息链表尾,对应于消息队列控制块中的 msg_queue_head 和 msg_queue_tail;有些消息框可能是空的,它们通过 msg_queue_free 形成一个空闲消息框链表。所有消息队列中的消息框总数即是消息队列的长度,这个长度可在消息队列创建时指定。

3.1、API

3.1.1、创建和删除消息队列

创建消息队列时先从对象管理器中分配一个消息队列对象,然后给消息队列对象分配一块内存空间,组织成空闲消息链表,这块内存的大小 =[消息大小 + 消息头(用于链表连接)的大小] X 消息队列最大个数,接着再初始化消息队列,此时消息队列为空。

rt_mq_t rt_mq_create(const char* name, 
                     rt_size_t msg_size, //消息队列中一条消息的最大长度,单位字节
                     rt_size_t max_msgs, //消息队列的最大个数
                     rt_uint8_t flag);   //消息队列采用的等待方式,它可以取如下数值:                                        
                                         //RT_IPC_FLAG_FIFO 或RT_IPC_FLAG_PRIO

rt_err_t rt_mq_delete(rt_mq_t mq);

3.1.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);    //消息队列采用的等待方式

rt_err_t rt_mq_detach(rt_mq_t mq);

3.1.3、发送消息

rt_err_t rt_mq_send (rt_mq_t mq,   
                     void* buffer,     //消息内容
                     rt_size_t size);  //消息大小

若发送的消息长度大于消息队列中消息的最大长度,返回-RT_ERROR,消息队列已满,返回-RT_EFULL。

3.1.4、发送紧急消息

发送紧急消息的过程与发送消息几乎一样,唯一的不同是,当发送紧急消息时,从空闲消息链表上取下来的消息块不是挂到消息队列的队尾,而是挂到队首,这样,接收者就能够优先接收到紧急消息,从而及时进行消息处理。

rt_err_t rt_mq_urgent(rt_mq_t mq, void* buffer, rt_size_t size);

3.1.5、接收消息

当消息队列中有消息时,接收者才能接收消息,否则接收者会根据超时时间设置,或挂起在消息队列的等待线程队列上,或直接返回。

接收消息时,接收者需指定存储消息的消息队列对象句柄,并且指定一个内存缓冲区,接收到的消息内容将被复制到该缓冲区里。此外,还需指定未能及时取到消息时的超时时间。如下图所示,接收一个消息后消息队列上的队首消息被转移到了空闲消息链表的尾部。

rt_err_t rt_mq_recv (rt_mq_t mq, 
                     void* buffer,   //指定的缓冲区
                     rt_size_t size, 
                     rt_int32_t timeout); //超时时间

四、信号

信号(又称为软中断信号),在软件层次上是对中断机制的一种模拟,在原理上,一个线程收到一个信号与处理器收到一个中断请求可以说是类似的。

信号在 RT-Thread 中用作异步通信,POSIX 标准定义了 sigset_t 类型来定义一个信号集,然而sigset_t 类型在不同的系统可能有不同的定义方式,在 RT-Thread 中,将 sigset_t 定义成了 unsigned long型,并命名为 rt_sigset_t,应用程序能够使用的信号为 SIGUSR1(10)和 SIGUSR2(12)。

信号本质是软中断,用来通知线程发生了异步事件,用做线程之间的异常通知、应急处理。一个线程不必通过任何操作来等待信号的到达,事实上,线程也不知道信号到底什么时候到达,线程之间可以互相通过调用 rt_thread_kill() 发送软中断信号

收到信号的线程对各种信号有不同的处理方法,处理方法可以分为三类:
           1、是类似中断的处理程序,对于需要处理的信号,线程可以指定处理函数,由该函数来处理。
           2、忽略某个信号,对该信号不做任何处理,就像未发生过一样。
           3、对该信号的处理保留系统的默认值。

如下图所示,假设线程 1 需要对信号进行处理,首先线程 1 安装一个信号并解除阻塞,并在安装的同时设定了对信号的异常处理方式;然后其他线程可以给线程 1 发送信号,触发线程 1 对该信号的处理。

RT-Thread内核学习(认真系列) ---- (4)线程间通信_第3张图片

当信号被传递给线程 1 时,如果它正处于挂起状态,那会把状态改为就绪状态去处理对应的信号。如果它正处于运行状态,那么会在它当前的线程栈基础上建立新栈帧空间去处理对应的信号,需要注意的是使用的线程栈大小也会相应增加

4.1、API

4.1.1、安装信号

如果线程要处理某一信号,那么就要在线程中安装该信号。安装信号主要用来确定信号值及线程针对该信号值的动作之间的映射关系,即线程将要处理哪个信号,该信号被传递给线程时,将执行何种操作。详细定义请见以下代码:

rt_sighandler_t rt_signal_install(int signo, rt_sighandler_t[] handler);
//signo :信号值(只有 SIGUSR1 和 SIGUSR2 是开放给用户使用的,下同)
//handler : 设置对信号值的处理方式

在信号安装时设定 handler 参数,决定了该信号的不同的处理方法。处理方法可以分为三种:
          1)类似中断的处理方式,参数指向当信号发生时用户自定义的处理函数,由该函数来处理
          2)参数设为 SIG_IGN,忽略某个信号,对该信号不做任何处理,就像未发生过一样。
          3)参数设为 SIG_DFL,系统会调用默认的处理函数 _signal_default_handler()。

4.1.2、阻塞信号

信号阻塞,也可以理解为屏蔽信号。如果该信号被阻塞,则该信号将不会递达给安装此信号的线程,也不会引发软中断处理。调 rt_signal_mask() 可以使信号阻塞:

void rt_signal_mask(int signo);  //信号值

4.1.3、解除信号阻塞

线程中可以安装好几个信号,使用此函数可以对其中一些信号给予 “关注”,那么发送这些信号都会引发该线程的软中断。调用 rt_signal_unmask() 可以用来解除信号阻塞:

void rt_signal_unmask(int signo);

4.1.4、发送信号

int rt_thread_kill(rt_thread_t tid,  //接收信号的线程
                   int sig);         //信号值

4.1.5、等待信号

等待 set 信号的到来,如果没有等到这个信号,则将线程挂起,直到等到这个信号或者等待时间超过指定的超时时间 timeout。如果等到了该信号,则将指向该信号体的指针存入 si,如下是等待信号的函数:

int rt_signal_wait(const rt_sigset_t *set, //指定等待的信号
                   rt_siginfo_t[] *si,     //指向存储等到信号信息的指针
                   rt_int32_t timeout);    //指定的等待时间

 

 

 

 

 

你可能感兴趣的:(RT-Thread内核学习(认真系列) ---- (4)线程间通信)