内核入门(五)——线程间通信

  在裸机编程中,经常会使用全局变量进行功能间的通信,如某些功能可能由于一些操作而改变全局变量的值,另一个功能对此全局变量进行读取,根据读取到的全局变量值执行相应的动作,达到通信协作的目的。RT-Thread 中则提供了更多的工具帮助在不同的线程中间传递信息,本章会详细介绍这些工具。学习完本章,大家将学会如何将邮箱、消息队列、信号用于线程间的通信。

一 邮箱

1.1 邮箱的工作机制

  邮箱也称作交换消息,线程或中断服务例程把一封4 字节长度的邮件发送到邮箱中,而一个或多个线程可以从邮箱中接收这些邮件并进行处理。邮箱中的每一封邮件只能容纳固定的4 字节内容(针对32 位处理系统,指针的大小即为4 个字节,所以一封邮件恰好能够容纳一个指针)。

  非阻塞方式的邮件发送过程能够安全的应用于中断服务中,是线程、中断服务、定时器向线程发送消息的有效手段。通常来说,邮件收取过程可能是阻塞的,这取决于邮箱中是否有邮件,以及收取邮件时设置的超时时间。当邮箱中不存在邮件且超时时间不为0 时,邮件收取过程将变成阻塞方式。在这类情况下,只能由线程进行邮件的收取。
  当一个线程向邮箱发送邮件时,如果邮箱没满,将把邮件复制到邮箱中。如果邮箱已经满了,发送线程可以设置超时时间,选择等待挂起或直接返回- RT_EFULL。如果发送线程选择挂起等待,那么当邮箱中的邮件被收取而空出空间来时,等待挂起的发送线程将被唤醒继续发送。
  当一个线程从邮箱中接收邮件时,如果邮箱是空的,接收线程可以选择是否等待挂起直到收到新的邮件而唤醒,或可以设置超时时间。当达到设置的超时时间,邮箱依然未收到邮件时,这个选择超时等待的线程将被唤醒并返回- RT_ETIMEOUT。如果邮箱中存在邮件,那么接收线程将复制邮箱中的4 个字节邮件到接收缓存中。

1.2 邮箱的管理

1.2.1 邮箱控制块

  在RT-Thread 中,邮箱控制块是操作系统用于管理邮箱的一个数据结构,由结构体struct rt_mailbox表示。另外一种C 表达方rt_mailbox_t,表示的是邮箱的句柄,在C 语言中的实现是邮箱控制块的指针。邮箱控制块结构的详细定义请见以下代码:

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;

1.2.2 邮箱的管理

  1. 创建与删除

  创建邮箱对象时会先从对象管理器中分配一个邮箱对象,然后给邮箱动态分配一块内存空间用来存放邮件,这块内存的大小等于邮件大小(4 字节)与邮箱容量的乘积,接着初始化接收邮件数目和发送邮件在邮箱中的偏移量。动态创建一个邮箱对象可以调用如下的函数接口:

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

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

rt_err_t rt_mb_delete (rt_mailbox_t mb);
  1. 初始化与剥离

  初始化邮箱用于静态邮箱对象,内存是在系统编译时由编译器分配的,一般放于读写数据段或未初始化数据段中,其余的初始化工作与创建邮箱时相同。函数接口如下:

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

  初始化邮箱时,该函数接口需要获得用户已经申请获得的邮箱对象控制块,缓冲区的指针,以及邮箱名称和邮箱容量(能够存储的邮件数)。这里的size 参数指定的是邮箱的容量,即如果msgpool 指向的缓冲区的字节数是N,那么邮箱容量应该是N/4。
  相对应的,剥离邮箱使用下面的接口:

rt_err_t rt_mb_detach(rt_mailbox_t mb);
  1. 发送邮件

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

rt_err_t rt_mb_send (rt_mailbox_t mb, rt_uint32_t value);

  发送的邮件可以是32 位任意格式的数据,一个整型值或者一个指向缓冲区的指针。当邮箱中的邮件已经满时,发送邮件的线程或者中断程序会收到RT_EFULL的返回值。
  用户也可以通过如下的函数接口向指定邮箱发送邮件,rt_mb_send_wait()rt_mb_send() 的区别在于有等待时间,如果邮箱已经满了,那么发送线程将根据设定的timeout 参数等待邮箱中因为收取邮件而空出空间。如果设置的超时时间到达依然没有空出空间,这时发送线程将被唤醒并返回错误码。

rt_err_t rt_mb_send_wait (rt_mailbox_t mb,
rt_uint32_t value,
rt_int32_t timeout);
  1. 接收邮件

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

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

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

二 消息队列

  消息队列是另一种常用的线程间通讯方式,是邮箱的扩展。可以应用在多种场合:线程间的消息交换、使用串口接收不定长数据等。

2.1 消息队列的工作机制

  消息队列能够接收来自线程或中断服务例程中不固定长度的消息,并把消息缓存在自己的内存空间中。其他线程也能够从消息队列中读取相应的消息,而当消息队列是空的时候,可以挂起读取线程。当有新的消息到达时,挂起的线程将被唤醒以接收并处理消息。消息队列是一种异步的通信方式。
  如下图所示,线程或中断服务例程可以将一条或多条消息放入消息队列中。同样,一个或多个线程也可以从消息队列中获得消息。当有多个消息发送到消息队列时,通常将先进入消息队列的消息先传给线程,也就是说,线程先得到的是最先进入消息队列的消息,即先进先出原则(FIFO)。

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

2.2 消息队列的管理

2.2.1 消息队列控制块

  在RT-Thread 中,消息队列控制块是操作系统用于管理消息队列的一个数据结构,由结构体struct rt_messagequeue 示。另外一种C 表达方式rt_mq_t,表示的是消息队列的句柄,在C 语言中的实现是消息队列控制块的指针。消息队列控制块结构的详细定义请见以下代码:

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.2.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_ERROR),然后再释放消息队列使用的内存,最后删除消息队列对象。

rt_err_t rt_mq_delete(rt_mq_t mq);
  1. 初始化与脱离

  静态消息队列对象的内存是在系统编译时由编译器分配的,一般放于读数据段或未初始化数据段中。在使用这类静态消息队列对象前,需要用户申请获得的消息队列对象的句柄(即指向消息队列对象控制
块的指针)、消息队列名、消息缓冲区指针、消息大小以及消息队列缓冲区大小。初始化消息队列对象的函数接口如下:

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_ERROR),然后将该消息队列对象从内核对象管理器中脱离。

rt_err_t rt_mq_detach(rt_mq_t mq);
  1. 发送消息

  线程或者中断服务程序都可以给消息队列发送消息。当发送消息时,消息队列对象先从空闲消息链表上取下一个空闲消息块,把线程或者中断服务程序发送的消息内容复制到消息块上,然后把该消息块挂到消息队列的尾部。当且仅当空闲消息链表上有可用的空闲消息块时,发送者才能成功发送消息;当空闲消息链表上无可用消息块,说明消息队列已满,此时,发送消息的的线程或者中断程序会收到一个错误码RT_EFULL

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

  用户也可以通过如下的函数接口向指定的消息队列中发送消息:

rt_err_t rt_mq_send_wait(rt_mq_t mq,
						const void *buffer,
						rt_size_t size,
						rt_int32_t timeout);

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

rt_err_t rt_mq_urgent(rt_mq_t mq, void* buffer, rt_size_t size);
  1. 接收消息

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

rt_err_t rt_mq_recv (rt_mq_t mq, void* buffer,rt_size_t size, rt_int32_t timeout);

三 信号

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

3.1 信号的工作机制

  信号在RT-Thread 中用作异步通信,POSIX(Portable Operating System Interface of UNIX,可移植操作系统接口) 标准定义了sigset_t类型来定义一个信号集,然而sigset_t类型在不同的系统可能有不同的定义方式,在RT-Thread 中,将sigset_t定义成了unsigned long型,并命名为rt_sigset_t,应用程序能够使用的信号为SIGUSR1(10)SIGUSR2(12)
  信号本质是软中断,用来通知线程发生了异步事件,用做线程之间的异常通知应急处理。一个线程不必通过任何操作来等待信号的到达,事实上,线程也不知道信号到底什么时候到达,线程之间可以互相通过调用rt_thread_kill() 发送软中断信号。

3.2 信号的管理

  1. 安装信号

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

rt_sighandler_t rt_signal_install(int signo, rt_sighandler_t[] handler);
  1. 阻塞信号/接触阻塞

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

void rt_signal_mask(int signo);

  调用rt_signal_unmask() 可以用来解除信号阻塞:

void rt_signal_unmask(int signo);
  1. 发送信号

  当需要进行异常处理时,可以给设定了处理异常的线程发送信号,调用rt_thread_kill()可以用来向任何线程发送信号:

int rt_thread_kill(rt_thread_t tid, int sig);
  1. 等待信号

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

int rt_signal_wait(const rt_sigset_t *set, rt_siginfo_t[] *si, rt_int32_t timeout);

总结

使用场景总结
  邮箱、消息队列、信号分别适合不同的使用场景。

猜你喜欢

转载自blog.csdn.net/qq_33604695/article/details/105423647