1. 信号量
信号量有两种:计数性信号量和二值信号量,计数性信号量可以被获取多次,二值信号量只有0和1两种状态,只能被获取一次。
信号量可以用来对资源进行保护,防止多个任务同时访问某个资源。为资源创建一个专属的二值信号量,任务在申请访问一个资源之前,先申请获取信号量,如果当前没有任务正在访问资源,则获取信号量可以成功,可以继续访问资源;如果当前资源正在被某个任务访问,则获取信号量会失败,任务进入挂起状态,等待其它任务访问资源完成并释放信号量后再访问资源。
如果某个资源允许多个任务同时访问,可以采用计数性信号量,每次有任务申请获取信号量则信号量计数减一,直到减为0后不再允许其他任务再申请获取,即可以通过信号量的初始值来控制同时访问资源的最大任务数量。
2. 优先级反转
使用信号量可能会导致优先级反转的问题,详细的说明见下图,截取自RT-Thread编程指南,简单来说就是当一个高优先级任务申请访问资源时,资源正在被低优先级任务占用,所以高优先级任务被挂起,此时一个中优先级任务就绪,剥夺低优先级任务的CPU使用权,开始执行,就造成了该任务先于高优先级任务开始执行。
3. 互斥量
互斥量可以理解为二值信号量,它可以避免上述优先级反转的问题,方法是当高优先级任务申请访问资源但资源被低优先级任务时,将低优先级任务的优先级提高到高优先级同级别,就避免了中优先级任务就绪时剥夺低优先级任务的CPU使用权。详细的说明见下图。任务在申请获取互斥量时,系统会判断当前占有该互斥量的任务的优先级和申请任务的优先级,如果申请任务优先级更高,系统会把这个更高的优先级赋给占有互斥量的任务,当任务释放互斥量时再恢复其原有的优先级。
此外,互斥量还支持重入,即多次获取,这时互斥量被获取了多少次就需要释放多少次,才能完全释放。且互斥量是归属于某个任务的,哪个任务获取互斥量就要由哪个任务释放互斥量,其它任务不能释放。
4. 死锁
死锁也称抱死,是指两个任务无限制地互相等待对方正在占用的资源。详细说明如下,截取自《嵌入式实时操作系统uC/OS-III》:
避免死锁有以下三个方法:
- (1)不同任务中用相同的顺序申请多个资源;
- (2)任务中需要用到多个资源时,先获取所有资源,再做下一步工作;
- (3)在申请获取互斥量或信号量时设置超时时间,避免永远等待。
最有效的是方法(1),按相同的顺序访问资源,就可以避免两个任务和两个资源间的交叉访问,如果任务中使用资源的顺序固定,那么可以先获取所有资源的访问权限,再访问资源,这样就能避免死锁的问题。方法(3)只是能暂时避免死锁,但会导致系统报错,不推荐。
5. 邮箱和消息队列
5.1 uCOS
uCOS-II有邮箱和消息队列两个概念,都是Event的一种,对于邮箱来说,Event控制块中有一个指针直接指向数据地址,所以每次只能传递一个数据指针。而对消息队列来说,Event控制块中的指针指向一个消息队列,消息队列中的每一个节点包含一个数据地址,所以能同时传递多个数据地址。队列大小为1的消息队列与邮箱的作用是一样的。
uCOS-II中有一个静态的全局结构体数组OSQTbl[OS_MAX_QS],包含了系统中所有可用的消息队列,每创建一个消息队列,就从这个数组中取一个节点出来。
在uCOS-III中,取消了邮箱的概念,只有消息队列。在代码中,定义了一个消息池:
OS_MSG OSCfg_MsgPool [OS_CFG_MSG_POOL_SIZE];
所有消息队列在发送消息的时候,都从消息池里取一个节点出来,挂到消息队列的消息链表上,消息队列控制块有指针指向消息链表的头和尾,还会记录消息节点的数量。
消息队列控制块的定义如下:
struct os_msg_q { /* OS_MSG_Q */
OS_MSG *InPtr; /* Pointer to next OS_MSG to be inserted in the queue */
OS_MSG *OutPtr; /* Pointer to next OS_MSG to be extracted from the queue */
OS_MSG_QTY NbrEntriesSize; /* Maximum allowable number of entries in the queue */
OS_MSG_QTY NbrEntries; /* Current number of entries in the queue */
OS_MSG_QTY NbrEntriesMax; /* Peak number of entries in the queue */
};
5.2 rt-thread
RTT中有邮箱和消息队列两个概念,它的邮箱和uCOS-II的邮箱概念不同。RTT的邮箱是基于一个数组来实现的,数组的成员是4字节变量,可以是数据地址或变量,这个数组就是该邮箱的消息池,在创建邮箱的时候固定进行分配。所以RTT的邮箱在传递数据时每次固定传输一个4字节的数据,每次发送邮件的时候从消息池中取一个4字节变量出来,接收邮件的时候释放一个4字节变量。邮箱控制块中存储着当前邮箱发送邮件(in_offset)和接收邮件(out_offset)的数据地址,以及当前邮箱中存在的邮件数量(entry)。
邮箱控制块的定义如下:
struct rt_mailbox
{
struct rt_ipc_object parent; /**< inherit from ipc_object */
rt_ubase_t *msg_pool; /**< start address of message buffer */
rt_uint16_t size; /**< size of message pool */
rt_uint16_t entry; /**< index of messages in msg_pool */
rt_uint16_t in_offset; /**< input offset of the message buffer */
rt_uint16_t out_offset; /**< output offset of the message buffer */
rt_list_t suspend_sender_thread; /**< sender thread suspended on this mailbox */
};
RTT消息队列的实现和uCOS相似。但RTT不再是所有消息队列共用一个消息池,而是每个消息队列单独有一个消息池,分别管理。
RTT的消息队列控制块定义如下:
struct rt_messagequeue
{
struct rt_ipc_object parent; /**< inherit from ipc_object */
void *msg_pool; /**< start address of message queue */
rt_uint16_t msg_size; /**< message size of each message */
rt_uint16_t max_msgs; /**< max number of messages */
rt_uint16_t entry; /**< index of messages in the queue */
void *msg_queue_head; /**< list head */
void *msg_queue_tail; /**< list tail */
void *msg_queue_free; /**< pointer indicated the free node of queue */
rt_list_t suspend_sender_thread; /**< sender thread suspended on this message queue */
};