写在前面:
由于时间的不足与学习的碎片化,写博客变得有些奢侈。
但是对于记录学习(忘了以后能快速复习)的渴望一天天变得强烈。
既然如此
不如以天为单位,以时间为顺序,仅仅将博客当做一个知识学习的目录,记录笔者认为最通俗、最有帮助的资料,并尽量总结几句话指明本质,以便于日后搜索起来更加容易。
标题的结构如下:“类型”:“知识点”——“简短的解释”
部分内容由于保密协议无法上传。
点击此处进入学习日记的总目录
2024.04.05:UCOSIII第三十一节:互斥量
- 四十七、UCOSIII:互斥量
- 1、互斥量的基本概念
- 2、互斥量的优先级继承机制
- 1. 优先级翻转
- 2. 优先级继承
- 3、互斥量应用场景
- 4、互斥量运作机制
- 5、互斥量控制块
四十七、UCOSIII:互斥量
1、互斥量的基本概念
互斥量又称互斥信号量(本质也是一种信号量,不具备传递数据功能),是一种特殊的二值信号量,它和信号量不同的是, 它支持互斥量所有权、递归访问以及防止优先级翻转的特性,用于实现对临界资源的独占式处理。
任意时刻互斥量的状态只有两种, 开锁或闭锁。当互斥量被任务持有时,该互斥量处于闭锁状态,这个任务获得互斥量的所有权。
当该任务释放这个互斥量时, 该互斥量处于开锁状态,任务失去该互斥量的所有权。
当一个任务持有互斥量时,其他任务将不能再对该互斥量进行开锁或持有。
持有该互斥量的任务也能够再次获得这个锁而不被挂起,这就是递归访问,也就是递归互斥量的特性,这个特性与一般的信号量有很大的不同, 在信号量中,由于已经不存在可用的信号量,任务递归获取信号量时会发生主动挂起任务最终形成死锁。
如果想要用于实现同步(任务之间或者任务与中断之间),二值信号量或许是更好的选择,虽然互斥量也可以用于任务与任务间同步, 但是互斥量更多的是用于保护资源的互锁。
用于互锁的互斥量可以充当保护资源的令牌,当一个任务希望访问某个资源时,它必须先获取令牌。
当任务使用完资源后,必须还回令牌, 以便其他任务可以访问该资源。是不是很熟悉,在我们的二值信号量里面也是一样的,用于保护临界资源,保证多任务的访问井然有序。
当任务获取到信号量的时候才能开始使用被保护的资源,使用完就释放信号量,下一个任务才能获取到信号量从而可用使用被保护的资源。
但是信号量会导致的另一个潜在问题,那就是任务优先级翻转(具体会在下文讲解)。
而μC/OS提供的互斥量可以通过优先级继承算法, 可以降低优先级翻转问题产生的影响,所以,用于临界资源的保护一般建议使用互斥量。
2、互斥量的优先级继承机制
在μC/OS操作系统中为了降低优先级翻转问题利用了优先级继承算法。
优先级继承算法是指,暂时提高某个占有某种资源的低优先级任务的优先级, 使之与在所有等待该资源的任务中优先级最高那个任务的优先级相等。而当这个低优先级任务执行完毕释放该资源时,优先级重新回到初始设定值。
因此,继承优先级的任务避免了系统资源被任何中间优先级的任务抢占。
互斥量与二值信号量最大的不同是:
互斥量具有优先级继承机制,而信号量没有。
也就是说,某个临界资源受到一个互斥量保护, 如果这个资源正在被一个低优先级任务使用,那么此时的互斥量是闭锁状态,也代表了没有任务能申请到这个互斥量,如果此时一个高优先级任务想要对这个资源进行访问, 去申请这个互斥量,那么高优先级任务会因为申请不到互斥量而进入阻塞态,那么系统会将现在持有该互斥量的任务的优先级临时提升到与高优先级任务的优先级相同, 这个优先级提升的过程叫作优先级继承。
这个优先级继承机制确保高优先级任务进入阻塞状态的时间尽可能短,以及将已经出现的“优先级翻转”危害降低到最小。
没有理解?没问题,结合过程示意图再说一遍。
1. 优先级翻转
我们知道任务的优先级在创建的时候就已经是设置好的,高优先级的任务可以打断低优先级的任务, 抢占CPU的使用权。
但是在很多场合中,某些资源只有一个,当低优先级任务正在占用该资源的时候, 即便高优先级任务也只能乖乖的等待低优先级任务使用完该资源后释放资源。
这里高优先级任务无法运行而低优先级任务可以运行的现象称为“优先级翻转”。
为什么说优先级翻转在操作系统中是危害很大?
因为在我们一开始创造这个系统的时候,我们就已经设置好了任务的优先级了,越重要的任务优先级越高。
但是发生优先级翻转,对我们操作系统是致命的危害,会导致系统的高优先级任务阻塞时间过长。
举个例子,现在有3个任务分别为H任务(High)、M任务(Middle)、L任务(Low),3个任务的优先级顺序为H任务>M任务>L任务。
正常运行的时候H任务可以打断M任务与L任务,M任务可以打断L任务,假设系统中有一个资源被保护了,此时该资源被L任务正在使用中, 某一刻,H任务需要使用该资源,但是L任务还没使用完,H任务则因为申请不到资源而进入阻塞态,L任务继续使用该资源,此时已经出现了“优先级翻转”现象, 高优先级任务在等着低优先级的任务执行。
如果在L任务执行的时候刚好M任务被唤醒了,由于M任务优先级比L任务优先级高,那么会打断L任务, 抢占了CPU的使用权,直到M任务执行完,再把CPU使用权归还给L任务,L任务继续执行,等到执行完毕之后释放该资源,H任务此时才从阻塞态解除, 使用该资源。
这个过程,本来是最高优先级的H任务,在等待了更低优先级的L任务与M任务,其阻塞的时间是M任务运行时间+L任务运行时间, 这只是只有3个任务的系统,假如很多个这样子的任务打断最低优先级的任务,那这个系统最高优先级任务岂不是崩溃了,这个现象是绝对不允许出现的, 高优先级的任务必须能及时响应。
所以,没有优先级继承的情况下,使用资源保护,其危害极大,具体见图
- (1):L任务正在使用某临界资源, H任务被唤醒,执行H任务。但L任务并未执行完毕,此时临界资源还未释放。
- (2):这个时刻H任务也要对该临界资源进行访问, 但 L任务还未释放资源,由于保护机制,H任务进入阻塞态,L任务得以继续运行,此时已经发生了优先级翻转现象。
- (3):某个时刻M任务被唤醒,由于M任务的优先级高于L任务, M任务抢占了CPU的使用权,M任务开始运行,此时L任务尚未执行完,临界资源还没被释放。
- (4):M任务运行结束,归还CPU使用权,L任务继续运行。
- (5):L任务运行结束,释放临界资源,H任务得以对资源进行访问,H任务开始运行。
在这过程中,H任务的等待时间过长,这对系统来说这是很致命的。
所以这种情况不允许出现,而互斥量就是用来降低优先级翻转的产生的危害。
2. 优先级继承
假如有优先级继承呢?
那么,在H任务申请该资源的时候,由于申请不到资源会进入阻塞态,那么系统就会把当前正在使用资源的L任务的优先级临时提高到与H任务优先级相同, 此时M任务被唤醒了,因为它的优先级比H任务低,所以无法打断L任务,因为此时L任务的优先级被临时提升到H,所以当L任务使用完该资源了,进行释放, 那么此时H任务优先级最高,将接着抢占CPU的使用权, H任务的阻塞时间仅仅是L任务的执行时间,此时的优先级的危害降到了最低。具体见图
- (1):L任务正在使用某临界资源,L任务正在使用某临界资源, H任务被唤醒,执行H任务。但L任务并未执行完毕,此时临界资源还未释放。
- (2):某一时刻H任务也要对该资源进行访问,由于保护机制, H任务进入阻塞态。此时发生优先级继承,系统将L任务的优先级暂时提升到与H任务优先级相同,L任务继续执行。
- (3):在某一时刻M任务被唤醒,由于此时M任务的优先级暂时低于L任务,所以M任务仅在就绪态,而无法获得CPU使用权。
- (4):L任务运行完毕,H任务获得对资源的访问权,H任务从阻塞态变成运行态,此时L任务的优先级会变回原来的优先级。
- (5):当H任务运行完毕,M任务得到CPU使用权,开始执行。
- (6):系统正常运行,按照设定好的优先级运行。
但是使用互斥量的时候一定需要注意:
在获得互斥量后,请尽快释放互斥量,同时需要注意的是在任务持有互斥量的这段时间, 不得更改任务的优先级。
ΜC/OS的优先级继承机制不能解决优先级翻转,只能将这种情况的影响降低到最小,硬实时系统在一开始设计时就要避免优先级翻转发生。
3、互斥量应用场景
互斥量的使用比较单一,因为它是信号量的一种,并且它是以锁的形式存在。
在初始化的时候,互斥量处于开锁的状态,而被任务持有的时候则立刻转为闭锁的状态。
互斥量更适合于:
- 可能会引起优先级翻转的情况。
- 任务可能会多次获取互斥量的情况下,这样可以避免同一任务多次递归持有而造成死锁的问题。
多任务环境下往往存在多个任务竞争同一临界资源的应用场景,互斥量可被用于对临界资源的保护从而实现独占式访问。另外, 互斥量可以降低信号量存在的优先级翻转问题带来的影响。
比如有两个任务需要对串口进行发送数据,其硬件资源只有一个,那么两个任务肯定不能同时发送啦,不然导致数据错误,那么, 就可以用互斥量对串口资源进行保护。
当一个任务正在使用串口的时候,另一个任务则无法使用串口,等到任务使用串口完毕之后,另外一个任务才能获得串口的使用权。
另外需要注意的是互斥量不能在中断服务函数中使用,因为其特有的优先级继承机制只在任务起作用,而在中断的上下文环境中毫无意义。
4、互斥量运作机制
多任务环境下会存在多个任务访问同一临界资源的场景,该资源会被任务独占处理。
其他任务在资源被占用的情况下不允许对该临界资源进行访问, 这个时候就需要用到μC/OS的互斥量来进行资源保护,那么互斥量是怎样来避免这种冲突?
用互斥量处理不同任务对临界资源的同步访问时,任务想要获得互斥量才能进行资源访问。
如果一旦有任务成功获得了互斥量,则互斥量立即变为闭锁状态。
此时其他任务会因为获取不到互斥量而不能访问这个资源,任务会根据用户自定义的等待时间进行等待,直到互斥量被持有的任务释放后, 其他任务才能获取互斥量从而得以访问该临界资源。
此时互斥量再次上锁,如此一来就可以确保每个时刻只有一个任务正在访问这个临界资源,保证了临界资源操作的安全性。
- (1):因为互斥量具有优先级继承机制,一般选择使用互斥量对资源进行保护, 如果资源被占用的时候,无论是什么优先级的任务想要使用该资源都会被阻塞。
- (2):假如正在使用该资源的任务1比阻塞中的任务2的优先级还低, 那么任务1将被系统临时提升到与高优先级任务2相等的优先级(任务1的优先级从L 变成H)。
- (3):当任务1使用完资源之后,释放互斥量,此时任务1的优先级会从H变回原来的L。
- (4)-(5):任务2此时可以获得互斥量,然后进行资源的访问, 当任务2访问了资源的时候,该互斥量的状态又为闭锁状态,其他任务无法获取互斥量。
5、互斥量控制块
μC/OS的互斥量由多个元素组成,在互斥量被创建时,需要由我们自己定义互斥量(也可以称之为互斥量句柄)。
因为它是用于保存互斥量的一些信息的, 其数据结构OS_MUTEX除了互斥量必须的一些基本信息外,还有指向任务控制块的指针OwnerTCBPtr、任务优先级变量OwnerOriginalPrio、 PendList链表与OwnerNestingCtr变量等,为的是方便系统来管理互斥量。
示意图具体见图:
其数据结构具体如何:
struct os_mutex
{
/* ------------------ GENERIC MEMBERS ------------------ */
OS_OBJ_TYPE Type; (1)
CPU_CHAR *NamePtr; (2)
OS_PEND_LIST PendList; (3)
#if OS_CFG_DBG_EN > 0u
OS_MUTEX *DbgPrevPtr;
OS_MUTEX *DbgNextPtr;
CPU_CHAR *DbgNamePtr;
#endif
/* ------------------ SPECIFIC MEMBERS ------------------ */
OS_TCB *OwnerTCBPtr; (4)
OS_PRIO OwnerOriginalPrio; (5)
OS_NESTING_CTR OwnerNestingCtr; (6)
CPU_TS TS; (7)
};
- (1):互斥量的类型,用户无需理会,μC/OS能识别它是一个mutex。
- (2):互斥量的名字,每个内核对象都被分配一个名。
- (3):等待互斥量的任务列表。
- (4):指向持有互斥量任务控制块的指针, 如果任务占用这个mutex,那么该变量OwnerTCBPtr会指向占用这个mutex的任务的OS_TCB。
- (5):用于记录持有互斥量任务的优先级, 如果任务占用这个mutex,那么该变量OwnerOriginalPrio中存放着任务的原优先级,当占用mutex任务的优先级被提升时就会用到这个变量。
- (6):表示互斥量是否可用,当该值为0的时候表示互斥量处于开锁状态, 互斥量可用。μC/OS允许任务递归调用同一个mutex多达256次,每递归调用一次mutex该值就会加一,但也需要释放相同次数才能真正释放掉这个mutex。
- (7):mutex中的变量TS用于保存该mutex最后一次被释放的时间戳。 当mutex被释放,读取时基计数值并存放到该变量中。
注意:
用户代码不能直接访问这个结构体,必须通过μC/OS提供的API访问。