互斥信号量其实是特殊的二值信号量,由于其特有的优先级继承机制从而使它更适用于简单互锁,也就是保护临界资源(什么是优先级继承在后续相信讲解) 。
用作互斥时,信号量创建后可用信号量个数应该是满的, 任务在需要使用临界资源时,(临界资源是指任何时刻只能被一个任务访问的资源) ,先获取互斥信号量,使其变空,这样其他任务需要使用临界资源时就会因为无法获取信号量而进入阻塞,从而保证了临界资源的安全。
在操作系统中,我们使用信号量的很多时候是为了给临界资源建立一个标志,信号量表示了该临界资源被占用情况。这样,当一个任务在访问临界资源的时候,就会先对这个资源信息进行查询,从而在了解资源被占用的情况之后,再做处理,从而使得临界资源得到有效的保护。
互斥量基本概念
互斥量又称互斥信号量(本质是信号量),是一种特殊的二值信号量,它和信号量不同的是,它支持互斥量所有权、递归访问以及防止优先级翻转的特性,用于实现对临界资源的独占式处理。任意时刻互斥量的状态只有两种,开锁或闭锁。当互斥量被任务持有时,该互斥量处于闭锁状态,这个任务获得互斥量的所有权。当该任务释放这个互斥量时,该互斥量处于开锁状态, 任务失去该互斥量的所有权。当一个任务持有互斥量时,其他任务将不能再对该互斥量进行开锁或持有。 持有该互斥量的任务也能够再次获得这个锁而不被挂起,这就是递归访问,也就是递归互斥量的特性,这个特性与一般的信号量有很大的不同,在信号量中,由于已经不存在可用的信号量, 任务递归获取信号量时会发生主动挂起任务最终形成死锁。
如果想要用于实现同步(任务之间或者任务与中断之间),二值信号量或许是更好的选择,虽然互斥量也可以用于任务与任务、 任务与中断的同步,但是互斥量更多的是用于保护资源的互锁。
用于互锁的互斥量可以充当保护资源的令牌, 当一个任务希望访问某个资源时,它必须先获取令牌。当任务使用完资源后,必须还回令牌,以便其它任务可以访问该资源。是不是很熟悉,在我们的二值信号量里面也是一样的,用于保护临界资源,保证多任务的访问井然有序。当任务获取到信号量的时候才能开始使用被保护的资源,使用完就释放信号量,下一个任务才能获取到信号量从而可用使用被保护的资源。但是信号量会导致的另一个潜在问题,那就是任务优先级翻转(具体会在下文讲解) 。 而 FreeRTOS 提供的互斥量可以通过优先级继承算法, 可以降低优先级翻转问题产生的影响,所以,用于临界资源的保护一般建议使用互斥量。
互斥量的优先级继承机制
在 FreeRTOS 操作系统中为了降低优先级翻转问题利用了优先级继承算法。优先级继承算法是指,暂时提高某个占有某种资源的低优先级任务的优先级,使之与在所有等待该资源的任务中优先级最高那个任务的优先级相等,而当这个低优先级任务执行完毕释放该资源时,优先级重新回到初始设定值。因此,继承优先级的任务避免了系统资源被任何中间优先级的任务抢占。
互斥量与二值信号量最大的不同是:互斥量具有优先级继承机制,而信号量没有。也就是说,某个临界资源受到一个互斥量保护,如果这个资源正在被一个低优先级任务使用,那么此时的互斥量是闭锁状态,也代表了没有任务能申请到这个互斥量,如果此时一个高优先级任务想要对这个资源进行访问,去申请这个互斥量,那么高优先级任务会因为申请不到互斥量而进入阻塞态,那么系统会将现在持有该互斥量的任务的优先级临时提升到与高优先级任务的优先级相同,这个优先级提升的过程叫做优先级继承。这个优先级继承机制确保高优先级任务进入阻塞状态的时间尽可能短,以及将已经出现的“优先级翻转”危害降低到最小。
没有理解?没问题,结合过程示意图再说一遍。我们知道任务的优先级在创建的时候就已经是设置好的,高优先级的任务可以打断低优先级的任务,抢占 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 任务执行完,再把 CUP 使用权归还给 L 任务, L 任务继续执行,等到执行完毕之后释放该资源, H 任务此时才从阻塞态解除,使用该资源。这个过程,本来是最高优先级的 H 任务,在等待了更低优先级的 L 任务与 M 任务,其阻塞的时间是 M任务运行时间+L 任务运行时间,这只是只有 3 个任务的系统,假如很多个这样子的任务打断最低优先级的任务,那这个系统最高优先级任务岂不是崩溃了,这个现象是绝对不允许出现的,高优先级的任务必须能及时响应。所以,没有优先级继承的情况下,使用资源保护,其危害极大,具体见图
-
L 任务正在使用某临界资源, H 任务被唤醒,执行 H 任务。但 L 任务并未执行完毕,此时临界资源还未释放。
-
这个时刻 H 任务也要对该临界资源进行访问,但 L 任务还未释放资源,由于保护机制, H 任务进入阻塞态, L 任务得以继续运行,此时已经发生了优先级翻转现象。
-
某个时刻 M 任务被唤醒,由于 M 任务的优先级高于 L 任务, M 任务抢占了 CPU 的使用权, M 任务开始运行,此时 L 任务尚未执行完,临界资源还没被释放。
-
M 任务运行结束,归还 CPU 使用权, L 任务继续运行。
-
L任务运行结束,释放临界资源, H 任务得以对资源进行访问, H 任务开始运行。
在这过程中, H 任务的等待时间过长,这对系统来说这是很致命的,所以这种情况不允许出现,而互斥量就是用来降低优先级翻转的产生的危害。
假如有优先级继承呢?那么,在 H 任务申请该资源的时候,由于申请不到资源会进入阻塞态,那么系统就会把当前正在使用资源的 L 任务的优先级临时提高到与 H 任务优先级相同,此时 M 任务被唤醒了,因为它的优先级比 H 任务低,所以无法打断 L 任务,因为此时 L 任务的优先级被临时提升到 H,所以当 L 任务使用完该资源了,进行释放,那么此时 H 任务优先级最高,将接着抢占 CPU 的使用权, H 任务的阻塞时间仅仅是 L 任务的执行时间,此时的优先级的危害降到了最低,看!这就是优先级继承的优势,具体见图。
-
L 任务正在使用某临界资源, L 任务正在使用某临界资源, H 任务被唤醒,执行 H 任务。但 L 任务并未执行完毕,此时临界资源还未释放。
-
某一时刻 H 任务也要对该资源进行访问,由于保护机制, H 任务进入阻塞态。此时发生优先级继承,系统将 L 任务的优先级暂时提升到与 H 任务优先级相同, L任务继续执行。
-
在某一时刻 M 任务被唤醒,由于此时 M 任务的优先级暂时低于 L 任务,所以 M 任务仅在就绪态,而无法获得 CPU 使用权。
-
L任务运行完毕, H 任务获得对资源的访问权, H 任务从阻塞态变成运行态,此时 L 任务的优先级会变回原来的优先级。
-
当 H 任务运行完毕, M 任务得到 CPU 使用权,开始执行
-
系统正常运行,按照设定好的优先级运行。
但是使用互斥量的时候一定需要注意:在获得互斥量后,请尽快释放互斥量,同时需要注意的是在任务持有互斥量的这段时间,不得更改任务的优先级。 FreeRTOS 的优先级继承机制不能解决优先级反转,只能将这种情况的影响降低到最小, 硬实时系统在一开始设计时就要避免优先级反转发生。
互斥量应用场景
互斥量的使用比较单一,因为它是信号量的一种,并且它是以锁的形式存在。在初始化的时候,互斥量处于开锁的状态,而被任务持有的时候则立刻转为闭锁的状态。互斥量更适合于:
-
可能会引起优先级翻转的情况。
递归互斥量更适用于:
-
任务可能会多次获取互斥量的情况下。这样可以避免同一任务多次递归持有而造成死锁的问题。
多任务环境下往往存在多个任务竞争同一临界资源的应用场景,互斥量可被用于对临界资源的保护从而实现独占式访问。另外,互斥量可以降低信号量存在的优先级翻转问题带来的影响。
比如有两个任务需要对串口进行发送数据,其硬件资源只有一个,那么两个任务肯定不能同时发送啦,不然导致数据错误,那么,就可以用互斥量对串口资源进行保护,当一个任务正在使用串口的时候,另一个任务则无法使用串口,等到任务使用串口完毕之后,另外一个任务才能获得串口的使用权。
另外需要注意的是互斥量不能在中断服务函数中使用,因为其特有的优先级继承机制只在任务起作用,在中断的上下文环境毫无意义。
互斥量运作机制
多任务环境下会存在多个任务访问同一临界资源的场景,该资源会被任务独占处理。其他任务在资源被占用的情况下不允许对该临界资源进行访问,这个时候就需要用到FreeRTOS 的互斥量来进行资源保护,那么互斥量是怎样来避免这种冲突?
用互斥量处理不同任务对临界资源的同步访问时, 任务想要获得互斥量才能进行资源访问,如果一旦有任务成功获得了互斥量,则互斥量立即变为闭锁状态,此时其他任务会因为获取不到互斥量而不能访问这个资源, 任务会根据用户自定义的等待时间进行等待,直到互斥量被持有的任务释放后,其他任务才能获取互斥量从而得以访问该临界资源,此时互斥量再次上锁,如此一来就可以确保每个时刻只有一个任务正在访问这个临界资源,保证了临界资源操作的安全性。
-
因为互斥量具有优先级继承机制,一般选择使用互斥量对资源进行保护,如果资源被占用的时候,无论是什么优先级的任务想要使用该资源都会被阻塞。
-
假如正在使用该资源的任务 1 比阻塞中的任务 2 的优先级还低,那么任务1 将被系统临时提升到与高优先级任务 2 相等的优先级(任务 1 的优先级从 L变成 H)。
-
当任务 1 使用完资源之后,释放互斥量,此时任务 1 的优先级会从 H 变回原来的 L。
-
5. 任务 2 此时可以获得互斥量,然后进行资源的访问,当任务 2 访问了资源的时候,该互斥量的状态又为闭锁状态,其他任务无法获取互斥量。
互斥量控制块
互斥量的 API 函数实际上都是宏,它使用现有的队列机制, 这些宏定义在 semphr.h 文件中, 如果使用互斥量,需要包含 semphr.h 头文件。 所以 FreeRTOS 的互斥量控制块结构体与消息队列结构体是一模一样的,只不过结构体中某些成员变量代表的含义不一样而已,我们会具体讲解一下哪里与消息队列不一样。
typedef struct QueueDefinition /* 保留旧的命名约定以防止破坏内核感知调试器。 */
{
int8_t * pcHead; /*< 指向队列存储区域的起始位置。 */
int8_t * pcWriteTo; /*< 指向存储区域中的下一个空闲位置。 */
union
{
QueuePointers_t xQueue; /*< 当此结构用作队列时所需的数据。 */
SemaphoreData_t xSemaphore; /*< 当此结构用作信号量时所需的数据。 */
} u;
List_t xTasksWaitingToSend; /*< 等待向此队列发送数据的任务列表。按优先级顺序存储。 */
List_t xTasksWaitingToReceive; /*< 等待从队列读取数据的任务列表。按优先级顺序存储。 */
volatile UBaseType_t uxMessagesWaiting; /*< 有效信号量个数。 */
UBaseType_t uxLength; /*< 最大的信号量可用个数。 */
UBaseType_t uxItemSize; /*< 0 */
volatile int8_t cRxLock; /*< 存储在队列锁定期间从队列接收(移除)的项目数量。当队列未锁定时设置为queueUNLOCKED。 */
volatile int8_t cTxLock; /*< 存储在队列锁定期间向队列传输(添加)的项目数量。当队列未锁定时设置为queueUNLOCKED。 */
#if ( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
uint8_t ucStaticallyAllocated; /*< 如果队列使用的内存是静态分配的,则设置为pdTRUE,以确保不会尝试释放内存。 */
#endif
#if ( configUSE_QUEUE_SETS == 1 )
struct QueueDefinition * pxQueueSetContainer;
#endif
#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxQueueNumber;
uint8_t ucQueueType;
#endif
} xQUEUE;
/* 上面保留了旧的 xQUEUE 名称,然后在此处 typedef 为新的 Queue_t 名称,以便使用较旧的内核感知调试器。 */
typedef xQUEUE Queue_t;
typedef struct QueuePointers
{
int8_t * pcTail; /*< 指向队列存储区域末尾的字节。分配的字节数比存储队列项目所需的多一个,用作标记。 */
int8_t * pcReadFrom; /*< 当此结构用作队列时,指向最后一个从队列中读取项目的地址。 */
} QueuePointers_t;
typedef struct SemaphoreData
{
TaskHandle_t xMutexHolder; /*< 持有互斥锁的任务句柄。 */
UBaseType_t uxRecursiveCallCount; /*< 当此结构用作互斥锁时,维护递归互斥锁被递归“获取”的次数。 */
} SemaphoreData_t;
-
pcReadFrom 与 uxRecursiveCallCount 是一对互斥变量, 使用联合体用来确保两个互斥的结构体成员不会同时出现。 当结构体用于队列时, pcReadFrom 指向出队消息空间的最后一个,见文知义,就是读取消息时候是从 pcReadFrom 指向的空间读取消息内容。 当结构体用于互斥量时, uxRecursiveCallCount 用于计数,记录递归互斥量被“调用” 的次数。
-
如果控制块结构体是用于消息队列: uxMessagesWaiting 用来记录当前消息队列的消息个数; 如果控制块结构体被用于互斥量的时候, 这个值就表示有效互斥量个数,这个值是 1 则表示互斥量有效,如果是 0 则表示互斥量无效。
-
如果控制块结构体是用于消息队列: uxLength 表示队列的长度,也就是能存放多少消息; 如果控制块结构体被用于互斥量的时候, uxLength 表示最大的信号量可用个数, uxLength 最大为 1,因为信号量要么是有效的,要么是无效的。
-
如果控制块结构体是用于消息队列: uxItemSize 表示单个消息的大小; 如果控制块结构体被用于互斥量的时候, 则无需存储空间,为 0 即可。