学习目标:任务间同步及通信
学习内容:
在多任务的实时系统中,一项工作的完成往往需要多个任务协调的方式共同完成。
对于操作/访问同一块区域,称之为临界区。任务的同步方式有很多种,但其核心思想都是在访问临界区时只允许一个或一类任务运行。
关闭中断:
关闭和打开中断接口由两个函数完成:
rt_base_t rt_hw_interrupt_disable(void);
rt_base_t rt_hw_interrupt_enable(rt_base_t level); level 可以记录前一次的中断状态
调度器锁:
void rt_enter_critical(void);/*进入临界区
使用上述函数后,调度器将被上锁,不在进行其他的调度任务,直道退出。但在此期间仍然会响应中断,如果通过中断唤醒了更高优先级的线程,调度器因为被锁住,所以也不会立即执行,直至解锁。
void rt_exit_critical(void) 解锁与退出临界段
调度器锁能够方便地使用于一些线程与线程间同步的场合,由于轻型,它不会对系统中
断响应造成负担;但它的缺陷也很明显,就是它不能被用于中断与线程间的同步或通知,并
且如果执行调度器锁的时间过长,会对系统的实时性造成影响(因为使用了调度器锁后,系
统将不再具备优先级的关系,直到它脱离了调度器锁的状态)。
信号量:
信号量是一种轻型的用于解决线程间同步问题的内核对象,线程可以获取或者释放它,从而达到同步或者互斥的目的。信号量就像一把钥匙,将一段临界段锁住,只允许有钥匙的线程进行访问。
每个信号量对象都有一个信号量值和一个线程线程等待队列,信号量的值对应了信号量对象的实例数目,资源数目
当信号量实例数目为0时,再申请该信号量的线程会被挂起在该信号量的等待队列中,等待可用的信号量。
信号量控制块:
struct rt_semaphore
{
struct rt_ipc_object parent;/继承自ipc_object(容器对象)的类
rt_uint16_t value; //信号量的值
}
typedef struct rt_semaphore* rt_sem_t;
信号量相关的接口函数:
rt_sem_t rt_sem_create(const char * name,rt_uint32_t value,rt_uint8_t flag);
信号量的使用场合:
1.线程间的同步
2.锁
3.线程与中断之间的同步
互斥量
互斥量的概念为:二值性信号量,它支持互斥量间的所有权,递归访问以及防止优先级翻转
互斥量只有两种状态:开锁和闭锁。闭锁时线程获得他的所有权。开锁时失去所有权
持有该互斥量的线程可以再次获得这个锁而不被挂起,从而可以完成递归的调用。
优先级翻转问题的一个栗子:
优先级继承算法:
互斥量控制块:
struct rt_mutex
{
struct rt_ipc_object parent;
rt_uint16_t value;
rt_uint8_t original_priority;//原始持有的优先级
rt_uint8_t hold; 持有线程的持有次数
struct rt_thread *owner; 当前拥有互斥量的线程;
}
typedef struct rt_mutex* rt_mutex_t;
rt_mutex对象从rt_ipc_object中派生出来,由ipc容器管理。
使用场合:
互斥量的使用场景比较单一,因为他是信号量的一种,并且是以锁的形式存在。在初始化
的时候,互斥量永远都处于开锁的状态,而被线程持有时则处于闭锁的状态。互斥量更适合于
以下的场景:
1.线程多次持有互斥量的情况下。这样可以避免同一线程的多次递归造成的死锁问题。
2。隐藏的优先级翻转问题
3.互斥量不可以在中断服务例程中使用。
事件:
事件主要用于线程间的同步,与信号量不同,事件可以实现1对多,多对多的同步,即同一个线程可等待多个事件的触发,同样一个
事件也可以是多个线程同步多个事件。这种多个事件的集合可以用一个32位无符号整型变量来表示。每一位代表一个事件,线程可以
通过逻辑上的或与来和事件建立联系。形成一个事件集。
事件的特点:
• 事件只与线程相关,事件间相互独立:每个线程拥有32个事件标志,采用一个32 bit
无符号整型数进行记录,每一个bit代表一个事件。若干个事件构成一个事件集;
• 事件仅用于同步,不提供数据传输功能;
• 事件无排队性,即多次向线程发送同一事件(如果线程还未来得及读走),其效果等同
于只发送一次。
在rt_thread中,每个线程都有一个事件信息标记,它有三个属性分别是:
1.RT_EVENT_FLAG_AND(与)
2.RT_EVENT_FLAG_OR(或)
3.RT_EVENT_FLAG_CLEAR(清除标记)
在等待事件同步时,可以通过32个事件标志和事件信息来判断
当前接收的事件是否满足同步条件。
事件控制块:
struct rt_event
{
struct rt_ipc_object parent;
rt_uint32_t set;//事件集合
}
typedef struct rt_event* rt_event;
rt_event对象从rt_ipc_object中派生
使用场合:
事件可以使用在多种场合,在一定程度上可以替代信号量,用于线程间的同步。一个线程
或者中断服务历程发送一个事件给事件对象,从而进行线程的唤醒处理。
但是它与信号量不同之处在于:
事件的的发送操作在事件未清除前是不可累计的,而信号量的释放动作是可以累计的。
事件可以实现多对一或者一对多,并按照逻辑与或条件来完成对于线程的触发或者事件的操作,但信号量只能识别
单一的释放操作。
邮箱:
邮箱服务是一种实时操作系统中典型的任务通讯方法,其特点是开销较低,效率较高。
邮箱中每个邮件都只能容纳固定的4字节内容(针对32位的处理系统),指针的大小即为4个字节,
所以每一封邮件只能容纳固定的4字节内容。邮箱也可以称其为交换消息。
某一个线程或者中断服务历程把一封长度为4字节的邮件发送到邮箱之中,从而一个或多个线程可以从该邮箱中获取信息。
RT-Thread操作系统的邮箱中可存放固定条数的邮件,邮箱容量在创建/初始化邮箱时设
定,每个邮件大小为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_maibox* rt_mailbox_t;
rt_mailbox对象从rt_ipc_object中派生,由IPC容器管理。
使用场合:
邮箱是一种简单的线程间消息传递方式,在RT-Thread操作系统的实现中能够一次传递
4字节邮件,并且邮箱具备一定的存储功能,能够缓存一定数量的邮件数(邮件数由创建、初
始化邮箱时指定的容量决定)。邮箱中一封邮件的最大长度是4字节,所以邮箱能够用于不超
过4字节的消息传递,当传送的消息长度大于这个数目时就不能再采用邮箱的方式。最重要
的是,在32位系统上4字节的内容恰好适合放置一个指针,所以邮箱也适合那种仅传递指针
的情况,例如:
struct msg
{rt_uint8_t *data_ptr;
rt_uint32_r 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是一个新分配出来的内存块,所以
在接收线程处理完毕后,需要释放相应的内存块。
if (rt_mb_recv(mb, (rt_uint32_t*)&msg_ptr) == RT_EOK)
{
/* 在接收线程处理完毕后,需要释放相应的内存块*/
rt_free(msg_ptr);
}
消息队列:
消息队列是另一种常用的线程间通讯方式,它能够接收来自线程或中断服务例程中不固
定长度的消息,并把消息缓存在自己的内存空间中。
通过消息队列服务,线程或中断服务例程可以将一
条或多条消息放入消息队列中。同样,一个或多个线程可以从消息队列中获得消息。当有多
个消息发送到消息队列时,通常应将先进入消息队列的消息先传给线程,也就是说,线程先
得到的是最先进入消息队列的消息,即先进先出原则(FIFO)。
在rt_thread中,一个消息队列是由多个元素组成的,当消息队列被创建时,他就被分配了消息队列控制块,
由以下几部分组成:消息队列名称,内存缓冲区,消息大小以及队列长度等同时每
个消息队列对象中包含着多个消息框,每个消息框可以存放一条消息;消息队列中的第一
个和最后一个消息框被分别称为消息链表头和消息链表尾,对应于消息队列控制块中的
msg_queue_head和msg_queue_tail;有些消息框可能是空的,它们通过msg_queue_free形
成一个空闲消息框链表。所有消息队列中的消息框总数即是消息队列的长度,这个长度可在
消息队列创建时指定。
消息队列控制块
struct rt_messagequeue
{
struct rt_ipc_object parent;
void* msg_pool; //存放消息的消息池的开始地址
rt_uint16_t mag_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_message* rt_mq_t;
使用场合:
消息队列可以应用于发送不定长消息的场合,包括线程与线程间的消息交换,以及中断
服务例程中发送给线程的消息(中断服务例程不可能接收消息)。
消息队列和邮箱的明显不同是消息的长度并不限定在4个字节以内,另外消息队列也包
括了一个发送紧急消息的函数接口。但是当创建的是一个所有消息的最大长度是4字节的消
息队列时,消息队列对象将蜕化成邮箱。这个不限定长度的消息,也及时的反应到了代码
编写的场合上,同样是类似邮箱的代码:
struct msg
{
rt_uint8_t *data_ptr; /* 数据块首地址*/
rt_uint32_t data_size; /* 数据块大小*/
};
和邮箱例子相同的消息结构定义,假设依然需要发送这么一个消息给接收线程。在邮箱
例子中,这个结构只能够发送指向这个结构的指针(在函数指针被发送过去后,接收线程能
够正确的访问指向这个地址的内容,通常这块数据需要留给接收线程来释放)。而使用消息
队列的方式则大不相同:
void send_op(void *data, rt_size_t length)
{
struct msg msg_ptr;
msg_ptr.data_ptr = data; /* 指向相应的数据块地址*/
msg_ptr.data_size = length; /* 数据块的长度*/
/* 发送这个消息指针给mq消息队列*/
rt_mq_send(mq, (void*)&msg_ptr, sizeof(struct msg));
}
注意,上面的代码中,是把一个局部变量的数据内容发送到了消息队列中。在接收线程
中,同样也采用局部变量进行消息接收的结构体:
void message_handler()
{
struct msg msg_ptr; /* 用于放置消息的局部变量*/
/* 从消息队列中接收消息到msg_ptr中*/
if (rt_mq_recv(mq, (void*)&msg_ptr, sizeof(struct msg)) == RT_EOK)
{
/* 成功接收到消息,进行相应的数据处理*/
}
}
因为消息队列是直接的数据内容复制,所以在上面的例子中,都采用了局部变量的方式
保存消息结构体,这样也就免去动态内存分配的烦恼了(也就不用担心,接收线程在接收到
消息时,消息内存空间已经被释放
同步消息:
在一般的系统设计中会经常遇到要发送同步消息的问题,这个时候就可以根据当时的状
态选择相应的实现:两个线程间可以采用[消息队列+信号量或邮箱]的形式实现。发送线程
通过消息发送的形式发送相应的消息给消息队列,发送完毕后希望获得接收线程的收到确
认,工作示意图如图同步消息发送所示:
struct msg
{
/* 消息结构其他成员*/
struct rt_mailbox ack;
};
struct msg
{
/* 消息结构其他成员*/
struct rt_semaphore ack;
};
第一种类型的消息使用了邮箱来作为确认标志,而第二种类型的消息采用了信号量来作
为确认标志。邮箱做为确认标志,代表着接收线程能够通知一些状态值给发送线程;而信号
量作为确认标志只能够单一的通知发送线程,消息已经确认接收。