二进制信号量( B i n a r y S e m a p h o r e s Binary\quad Semaphores BinarySemaphores),计数信号量( C o u n t i n g S e m a p h o r e s Counting\quad Semaphores CountingSemaphores),互斥量( M u t e x e s Mutexes Mutexes)以及递归互斥量( R e c u r s i v e M u t e x e s Recursive\quad Mutexes RecursiveMutexes),它们并不是完全独立,毫无关联的。至少在实现的数据结构来说,它们使用的都是队列的数据结构只是在细节上有一定的差异。
- 我们从图1可以看出二进制信号量的创建接口 x S e m a p h o r e C r e a t e B i n a r y xSemaphoreCreateBinary xSemaphoreCreateBinary其实最后调用的也是队列的创建接口 x Q u e u e G e n e r i c C r e a t e xQueueGenericCreate xQueueGenericCreate,从图2中可以看出这里的特别的地方是队列的长度是1,队列里面数据项的长度为0,也就是说此时二进制信号量所占用的存储空间就只有前一小节中图1里面的 Q u e u e D e f i n i t i o n QueueDefinition QueueDefinition结构体所占用的空间。
- 我们从图3和图4中可以看出计数信号量的创建接口 x S e m a p h o r e C r e a t e C o u n t i n g xSemaphoreCreateCounting xSemaphoreCreateCounting其实最后调用的也是队列的创建接口 x Q u e u e G e n e r i c C r e a t e xQueueGenericCreate xQueueGenericCreate,相对于二进制信号量这里队列的长度不再固定为1,但是相对队列来说特别的地方是队列里面数据项的长度为0,也就是说计数信号量所占用的存储空间也只有前一小节中图1里面的 Q u e u e D e f i n i t i o n QueueDefinition QueueDefinition结构体所占用的空间。
- 我们从图5和图6中可以看出互斥量的创建接口 x S e m a p h o r e C r e a t e M u t e x xSemaphoreCreateMutex xSemaphoreCreateMutex其实最后调用的也是队列的创建接口 x Q u e u e G e n e r i c C r e a t e xQueueGenericCreate xQueueGenericCreate,和二进制信号量一样队列的长度固定为1,队列里面数据项的长度为0,也就是说互斥量所占用的存储空间也只有前一小节中图1里面的 Q u e u e D e f i n i t i o n QueueDefinition QueueDefinition结构体所占用的空间。这里需要注意的是虽然这里互斥量和二进制信号量的所占用的存储空间一样,但是它们还是有一定的区别的,最明显的是互斥量具有 优先级继承机制但是二进制信号量没有,至于互斥量的 优先级继承机制后面会重点讲到。
- 结合图6和图7中可以看出递归互斥量的创建接口 x S e m a p h o r e C r e a t e R e c u r s i v e M u t e x xSemaphoreCreateRecursiveMutex xSemaphoreCreateRecursiveMutex其实最后调用的也是队列的创建接口 x Q u e u e G e n e r i c C r e a t e xQueueGenericCreate xQueueGenericCreate,和二进制信号量一样队列的长度固定为1,队列里面数据项的长度为0,也就是说递归互斥量所占用的存储空间也只有前一小节中图1里面的 Q u e u e D e f i n i t i o n QueueDefinition QueueDefinition结构体所占用的空间。和互斥量一样递归互斥量也具有 优先级继承机制,它和互斥量的区别是它可以无限的 G i v e Give Give和 T a k e Take Take,这个后面会讲到。
二进制信号量常用于互斥和同步,从前面的介绍可以看出二进制信号量,互斥量和递归互斥量所占用的存储空间是一样的,其实互斥量和递归互斥量是一种特殊的二进制信号量。正是因为优先级继承机制,二进制信号量更适用于任务与任务间或任务与中断之间的同步而互斥量更适用于简单的互斥操作。很多的信号量(这里包括二进制信号量,计数信号量,互斥量和递归互斥量)接口函数都有一个阻塞时间参数,比如在
T
a
k
e
Take
Take操作的时候这个参数表示当试图
T
a
k
e
Take
Take信号量的时候,信号量此时
I
s
n
o
t
a
v
a
i
l
a
b
l
e
Is\quad not\quad \quad available
Isnotavailable,此时进行
T
a
k
e
Take
Take操作的任务就要进入阻塞状态,这个阻塞时间参数就表示处于阻塞状态的最长时间。这里就存在两种情况,一种情况是还没有到达这个阻塞参数规定的最大阻塞时间的时候,信号量就已经
I
s
a
v
a
i
l
a
b
l
e
Is\quad available
Isavailable,此时阻塞的任务会自动退出阻塞状态(假如此时没有优先级更高的任务在等待同一个信号量
I
s
a
v
a
i
l
a
b
l
e
Is\quad available
Isavailable,因为如果有多个任务在等待同一信号量的话,信号量
I
s
a
v
a
i
l
a
b
l
e
Is\quad available
Isavailable的时候优先级最高的任务会得到信号量,其它任务会继续等待),另一种情况是到了阻塞参数规定的最大阻塞时间的时候,等待的信号量还是没有
I
s
a
v
a
i
l
a
b
l
e
Is\quad available
Isavailable,此时等待的任务会自动退出阻塞状态。
G
i
v
e
Give
Give操作的接口一般没有这个阻塞时间参数。
前面也介绍过二进制信号量(也包括计数信号量,互斥量和递归互斥量)的队列里面的数据项是不占用存储空间的,创建队列的接口的
u
x
I
t
e
m
S
i
z
e
uxItemSize
uxItemSize参数的值为0,因此二进制信号量(也包括计数信号量,互斥量和递归互斥量)是不关心具体的消息是什么,只关心消息的有(一个或多个)与无(0个),消息的有与无和队列一样,用表示队列状态的结构体里面的元素
u
x
M
e
s
s
a
g
e
s
W
a
i
t
i
n
g
uxMessagesWaiting
uxMessagesWaiting来表示,如图8所示。二进制信号量(也包括计数信号量,互斥量和递归互斥量)的
G
i
v
e
Give
Give相当于队列的写操作,二进制信号量(也包括计数信号量,互斥量和递归互斥量)的
T
a
k
e
Take
Take相当于队列的读操作,只不过没有实际数据的写入以及读出,只是对元素
u
x
M
e
s
s
a
g
e
s
W
a
i
t
i
n
g
uxMessagesWaiting
uxMessagesWaiting进行加一或减一的操作。对于二进制信号量,互斥量和递归互斥量元素
u
x
M
e
s
s
a
g
e
s
W
a
i
t
i
n
g
uxMessagesWaiting
uxMessagesWaiting的值只能为1或0,对于计数信号量元素
u
x
M
e
s
s
a
g
e
s
W
a
i
t
i
n
g
uxMessagesWaiting
uxMessagesWaiting的值就没有这个限制,可以大于1。
下面我们来看一个二进制信号量应用的例子。假设一个任务用来服务一个外设,当外设有需求的时候会告诉这个任务来处理。如果这个任务对外设进行轮序操作来检查外设是否有服务需求的话,这种操作是非常浪费
C
P
U
CPU
CPU资源的并且让其它比这个任务的优先级低的任务无法执行。最好的方案是在外设没有服务请求的时候都让这个任务处于阻塞的状态(这样其它比这个任务的优先级低的任务就可以执行),等到外设有服务请求的时候再唤醒这个任务来处理。这种方案可以通过二进制信号量来实现,当任务试图通过
T
a
k
e
Take
Take操作获取信号量来对外设进行服务的时候,如果此时信号量
I
s
n
o
t
a
v
a
i
l
a
b
l
e
Is\quad not\quad \quad available
Isnotavailable且
T
a
k
e
Take
Take操作指定了阻塞时间(如果阻塞时间设置为
p
o
r
t
M
A
X
_
D
E
L
A
Y
portMAX\_DELAY
portMAX_DELAY且
I
N
C
L
U
D
E
_
v
T
a
s
k
S
u
s
p
e
n
d
INCLUDE\_vTaskSuspend
INCLUDE_vTaskSuspend宏在
F
r
e
e
R
T
O
S
C
o
n
f
i
g
.
h
FreeRTOSConfig.h
FreeRTOSConfig.h中定义为1,那么任务会一直阻塞直到等待的信号量
I
s
a
v
a
i
l
a
b
l
e
Is\quad available
Isavailable),此时该任务就会进入阻塞状态。每当外设有服务请求的时候可以触发相应的中断来对信号量进行
G
i
v
e
Give
Give操作,此时信号量就变成
I
s
a
v
a
i
l
a
b
l
e
Is\quad available
Isavailable从而唤醒阻塞的任务来对外设进行服务。外设中断对于二进制信号量永远只有
G
i
v
e
Give
Give操作而没有
T
a
k
e
Take
Take操作,服务于外设的任务永远只有
T
a
k
e
Take
Take操作而没有
G
i
v
e
Give
Give操作。这种行为对于互斥量和递归互斥量是不行的,
T
a
k
e
Take
Take操作之后必须要有对应的
G
i
v
e
Give
Give操作,这个后面在介绍互斥量和递归互斥量的时候会介绍。还有就是任务通知(
T
a
s
k
N
o
t
i
f
i
c
a
t
i
o
n
s
Task\quad Notifications
TaskNotifications)在某些应用场景下也可以用来替代二进制信号量的应用,它也比二进制信号量更快且更轻量化,这个后面介绍任务通知(
T
a
s
k
N
o
t
i
f
i
c
a
t
i
o
n
s
Task\quad Notifications
TaskNotifications)的时候再说。
基于上面描述的二进制信号量应用的例子,我们来看一个实际的例子。在这个例子中,一个二进制信号量用来通知任务
U
S
A
R
T
1
USART1
USART1是否已经收到了上位机发送的字符,如果任务对这个二进制信号量的
T
a
k
e
Take
Take操作成功了则表明
U
S
A
R
T
1
USART1
USART1已经收到了上位机发送的字符,这时任务会打印出这个收到的字符(这里为了简单起见,上位机每次只发送一个字符),如果
T
a
k
e
Take
Take操作没有成功,则任务会进入无限阻塞状态,下次
U
S
A
R
T
1
USART1
USART1收到上位机发送的字符的时候会触发接收中断并在中断中对二进制信号量进行
G
i
v
e
Give
Give操作,这样会唤醒处于阻塞状态的任务来打印出接收到的字符,当这个任务再次尝试对这个二进制信号量进行
T
a
k
e
Take
Take操作的时候,如果此时上位机没有接收到字符,那么任务会再次进入无限阻塞状态,直到下次上位机再次接收到字符的时候才会把它唤醒。主要的代码如下。
uint8_t received_character=0;
SemaphoreHandle_t xSemaphore = NULL;
/* Define a task that performs an action each time the USART1 interrupt occurs(USART1 has received characters). The Interrupt processing is deferred to this task. The task is synchronized with the interrupt using a semaphore. */
void Usart1ReceivingProcessingTask( void * pvParameters )
{
/* It is assumed the semaphore has already been created outside of this task. */
while(1)
{
printf("This the USART1 data processing task.\r\n");
/* Wait for the next event. */
if( xSemaphoreTake( xSemaphore, portMAX_DELAY ) == pdTRUE )
{
/* The event has occurred, process it here. */
printf("USART1 received character is %c.\r\n",received_character);
/* Processing is complete, return to wait for the next event. */
}
}
}
/* The USART1 ISR that defers its received characters processing to a task by using a semaphore to indicate when events that require processing have occurred. */
void USART1_IRQHandler( void * pvParameters )
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
/* The event has occurred, use the semaphore to unblock the task so the task can process the event. */
xSemaphoreGiveFromISR( xSemaphore, &xHigherPriorityTaskWoken );
/* Clear the interrupt here. */
received_character=USART_ReceiveData(USART1);
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
/* Now the task has been unblocked a context switch should be performed if
xHigherPriorityTaskWoken is equal to pdTRUE. NOTE: The syntax required to perform
a context switch from an ISR varies from port to port, and from compiler to
compiler. Check the web documentation and examples for the port being used to
find the syntax required for your application. */
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
int main(void)
{
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4);
NVIC_InitStruct.NVIC_IRQChannel=USART1_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=7;
NVIC_InitStruct.NVIC_IRQChannelSubPriority=0;
NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE;
NVIC_Init(&NVIC_InitStruct);
uart_init(115200);
printf("Binary semaphore demo start.\r\n");
/* The semaphore is created to hold a maximum of 3 structures of type Data_t. */
xSemaphore = xSemaphoreCreateBinary();
if( xSemaphore != NULL )
{
/* Create the task that will process the received character of USART1. */
xTaskCreate( Usart1ReceivingProcessingTask, "Receiver", 1000, NULL, 1, NULL );
/* Start the scheduler so the created tasks start executing. */
vTaskStartScheduler();
}
else
{
/* The semaphore could not be created. */
}
/* If all is well then main() will never reach here as the scheduler will
now be running the tasks. If main() does reach here then it is likely that
there was insufficient heap memory available for the idle task to be created.
Chapter 2 provides more information on heap memory management. */
while(1);
}
这种把部分处理放在任务中进行的操作( U S A R T 1 USART1 USART1的数据接收放在中断中进行,但是数据的处理放在任务中进行)在 F r e e R T O S FreeRTOS FreeRTOS的官方文档里面有一个专门的称呼 D e f e r r e d I n t e r r u p t P r o c e s s i n g Deferred\quad Interrupt\quad Processing DeferredInterruptProcessing,如图9所示。前面的实际的例子中还有一点需要注意的是 N V I C _ P r i o r i t y G r o u p C o n f i g ( N V I C _ P r i o r i t y G r o u p _ 4 ) ; NVIC\_PriorityGroupConfig( NVIC\_PriorityGroup\_4); NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);的调用要放在 N V I C _ I n i t ( & N V I C _ I n i t S t r u c t ) ; NVIC\_Init(\& NVIC\_InitStruct); NVIC_Init(&NVIC_InitStruct);的前面,因为 N V I C _ I n i t ( & N V I C _ I n i t S t r u c t ) ; NVIC\_Init(\& NVIC\_InitStruct); NVIC_Init(&NVIC_InitStruct);里面的初始化配置需要用到 N V I C _ P r i o r i t y G r o u p C o n f i g ( N V I C _ P r i o r i t y G r o u p _ 4 ) ; NVIC\_PriorityGroupConfig( NVIC\_PriorityGroup\_4); NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);的配置,否则可能会有坑。
/*******************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************/
计数信号量和二进制信号量区别不大,最大的区别是创建的队列的长度不再限制为1,而是可以大于1,其它基本没有太大区别。计数信号量主要用于两种场景:
- 对事件计数:在这种场景下事件触发的中断会对计数信号量进行 G i v e Give Give操作,对应的任务对计数信号量进行 T a k e Take Take操作,成功之后就来处理事件。在这种情况下,计数信号量的值(就是队列状态的结构体里面的元素 u x M e s s a g e s W a i t i n g uxMessagesWaiting uxMessagesWaiting的值,如图8所示)表示已经发生但是还没有被处理的事件的总数。一般计数信号量在创建的时候 u x M e s s a g e s W a i t i n g uxMessagesWaiting uxMessagesWaiting的值为0,因为在这种场景下此时没有任何事件。
- 资源管理:在这种场景下,计数信号量的值表示可用资源的总数,所以在这种场景下一般计数信号量在创建的时候
u
x
M
e
s
s
a
g
e
s
W
a
i
t
i
n
g
uxMessagesWaiting
uxMessagesWaiting的值不为0,而是可用资源的总数,因为此时资源还没有被使用。当某个任务试图获取多个资源中的一个的时候,它必须先尝试对计数信号量进行
T
a
k
e
Take
Take操作,只有
T
a
k
e
Take
Take操作操作成功了(会将计数信号量的值减一,也就是
u
x
M
e
s
s
a
g
e
s
W
a
i
t
i
n
g
uxMessagesWaiting
uxMessagesWaiting的值减一,当计数信号量的值为0的时候就表明此时没有可用的资源可以用了),任务才能实际获得多个资源中的一个,因为这表明此时还有多个资源中还有可用的资源。当任务使用完该资源的时候,需要对计数信号量进行
G
i
v
e
Give
Give操作(会将计数信号量的值加一),这样该资源才能被其它需要等的任务使用。所以这种场景下比较特殊的一点的是,如果任务通过
T
a
k
e
Take
Take操作成功的拿到了资源,那么它在使用完资源之后还必须通过
G
i
v
e
Give
Give操作还回去给其它需要的任务使用,这也是符合现实的。既然不用了那就还回去给别人用。
下面我们用计数信号量来简单的模拟一下一个有5个停车位的停车场,这里新建一个计数信号量来表示停车场的停车位,因此该计数信号量刚刚建立的时候的参数 u x M a x C o u n t uxMaxCount uxMaxCount和 u x I n i t i a l C o u n t uxInitialCount uxInitialCount的值都是5,这是因为刚开始的时候停车场没有停任何车,5个停车为都是空的。然后新建两个任务,一个任务用来申请停车场的停车位(也就相当于此时有车主向到停车场里面停车),对应于对计数信号量的 T a k e Take Take操作,每进行一次 T a k e Take Take操作计数信号量的值减一,当计数信号量的值为0的时候表明停车场已满,没有可用的停车位了。另一个任务用来释放停车场的停车位(对应车主已经不想再停车了,想开走了),对应于对计数信号量的 G i v e Give Give操作,每进行一次 G i v e Give Give操作计数信号量的值加一,当计数信号量的值为5的时候表明停车场已空,就无法再进行 G i v e Give Give操作了,这是因为停车场总共才5个停车位,当有5个空的停车位的时候,你总不能凭空搞出其它停车位。具体的实现的主要代码如下所示。
SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount,UBaseType_t uxInitialCount );
SemaphoreHandle_t xCountingSemaphore = NULL;
/* Define a task that try getting an available parking space . */
static void Take_Task(void* parameter)
{
BaseType_t xReturn = pdTRUE;
while (1)
{
if( Key_Scan(1) == KEY1_PRES )
{
xReturn = xSemaphoreTake(xCountingSemaphore,0);
if ( pdTRUE == xReturn )
printf( "KEY1 has been pressed,get an available parking space.\r\n" );
else
printf( "KEY1 has been pressed,but all parking spaces are full.\r\n" );
}
vTaskDelay(pdMS_TO_TICKS(20));
}
}
/* Define a task that try releasing a parking space. */
static void Give_Task(void* parameter)
{
BaseType_t xReturn = pdTRUE;
while (1)
{
if( Key_Scan(1) == KEY0_PRES )
{
xReturn = xSemaphoreGive(xCountingSemaphore);
if ( pdTRUE == xReturn )
printf( "KEY0 has been pressed,release a parking space successfully.\r\n" );
else
printf( "KEY0 has been pressed,but the maximum number of parking spaces is 5 and now the number of available parking spaces is 5.Parking spaces release failed.\r\n" );
}
vTaskDelay(pdMS_TO_TICKS(200));
}
}
int main(void)
{
NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4);
KEY_Init();
uart_init(115200);
printf("Counting semaphore demo start.\r\n");
/* The counting semaphore is created to have a maximum count value of 5, and an initial count value of 5 for 5 available resources. */
xCountingSemaphore = xSemaphoreCreateCounting(5,5);
if( xCountingSemaphore != NULL )
{
xTaskCreate( Take_Task, "Take", 1000, NULL, 2, NULL );
xTaskCreate( Give_Task, "Give", 1000, NULL, 3, NULL );
/* Start the scheduler so the created tasks start executing. */
vTaskStartScheduler();
}
else
{
/* The semaphore could not be created. */
}
/* If all is well then main() will never reach here as the scheduler will
now be running the tasks. If main() does reach here then it is likely that
there was insufficient heap memory available for the idle task to be created.
Chapter 2 provides more information on heap memory management. */
while(1);
}
/*******************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************/
前面也提到过二进制信号量更适用于任务与任务间或任务与中断之间的同步而互斥量更适用于简单的互斥操作。当互斥量用于互斥操作的时候,互斥量可以看做是和一个被两个或多个任务共享的资源(这里和前面的计数信号量里面介绍的资源管理不同的是这里的资源就一个,如果这个资源被某个任务获取了权限使用的话,其它任务就得等待该任务使用完之后才能使用,而计数信号量里面介绍的资源管理里面的资源是可以有多个的)相关的令牌(
T
o
k
e
n
Token
Token),当某个任务想要获取该资源的时候必须首先获取该令牌(对应于
T
a
k
e
Take
Take操作),成为该令牌的持有者,然后才有权限访问该资源,当使用完该资源之后,任务必须(这一点也是互斥量和二进制信号量的一个区别点,对于互斥量同一个任务
T
a
k
e
Take
Take操作成功之后必须要有对应的
G
i
v
e
Give
Give操作来释放该资源以便其它任务可以使用,但是对于二进制信号量来说的话就没有强制同一个任务
T
a
k
e
Take
Take操作成功之后必须要有对应的
G
i
v
e
Give
Give操作)返回该令牌(对应于
G
i
v
e
Give
Give操作),只有这样其它想要获取该资源的任务才能获取到该令牌,成为该令牌的持有者,从而获得该资源的访问权限。
F
r
e
e
R
T
O
S
FreeRTOS
FreeRTOS的官方文档里面的图10到图15比较详细的描述了以上流程。
互斥量是一种特殊的二进制信号量,它们之间的另一个较大的区别是互斥量支持优先级继承机制而二进制信号量不支持。这里和优先级继承机制相关的还有优先级翻转的概念,因此我们首先来看一下优先级翻转。这里我们直接上 F r e e R T O S FreeRTOS FreeRTOS官方文档的图,如图16所示。这里一种有三个任务,任务 H P HP HP,任务 M P MP MP和任务 L P LP LP,任务 H P HP HP的优先级最高,任务 M P MP MP的优先级中等,任务 L P LP LP的优先级最低,这三个任务共享一个互斥资源且此时用二进制信号量来对互斥资源进行管理。现在假设有这样一种场景:
- 此时任务 L P LP LP获得了执行机会且获得了二进制信号量从而获得了共享资源的使用权限。
- 任务 L P LP LP还没有使用完共享资源释放二进制信号量的时候,任务 H P HP HP抢占任务 L P LP LP开始执行,此时任务 H P HP HP也试图获取二进制信号量来访问共享资源,但是此时二进制信号量已经被任务 L P LP LP占有了,因此任务 H P HP HP获取二进制信号量失败从而进入阻塞等待状态。
- 此时任务 L P LP LP继续执行。
- 此时任务 M P MP MP获得执行的机会,因此它会抢占任务 L P LP LP开始执行(此时任务 L P LP LP还还没有使用完共享资源,因此也就还没有释放管理共享资源的二进制信号量)。此时就出现了一个奇特的现象任务 H P HP HP此时在等待任务 L P LP LP释放二进制信号量以便获取共享资源的访问权限,但是此时任务 L P LP LP根本就没有执行。
那究竟什么是优先级翻转,优先级翻转就是高优先级的任务反而需要等待低优先级的任务,这个是和 F r e e R T O S FreeRTOS FreeRTOS的设计理念相违背的,因为设计上高优先级的任务要优先执行且可以抢占低优先级的任务。我们在设计嵌入式系统应用的时候应该尽量避免优先级翻转的出现。互斥量的优先级继承机制就是为了减少优先级翻转造成的高优先级的任务等待低优先级的任务的时间,注意这里是减少等待的时间并不是完全消除。还有互斥量不能在中断中使用,因为这里的优先级继承机制的优先级指的是任务的优先级并不是中断的优先级,还有就是中断里面是不允许阻塞时间的。那么优先级继承机制是如何实现这种等待时间的减少的,假设现在有一个低优先级的任务获得了互斥量管理的共享资源的权限且还没有使用完该资源,与此同时有多个比它优先级高的任务试图获取管理的共享资源的互斥量来访问这个共享资源,因为这个互斥量已经被低优先级的任务占有了,因此这些高优先级的任务会进入阻塞等待状态,那么优先级继承机制核心点是此时该获得了互斥量的低优先级的任务的优先级暂时会被设置为所有因为等待被低优先级的任务占有的互斥量的高优先级任务中优先级最高的任务的优先级。图16的例子是使用二进制信号量来进行互斥资源管理的,二进制信号量是不支持优先级继承机制的,下面我们来看一下使用具有优先级继承机制的互斥量之后,图16中的例子的运行情况又是什么样子的,这里假设任务 H P HP HP的优先级为5,任务 M P MP MP的优先级为4,任务 L P LP LP的优先级为3。这里我们直接上 F r e e R T O S FreeRTOS FreeRTOS官方文档的图,如图17所示。
- 此时任务 L P LP LP获得了执行机会且获得了互斥量的令牌从而获得了共享资源的使用权限。
- 任务 L P LP LP还没有使用完共享资源释放互斥量的时候,任务 H P HP HP抢占任务 L P LP LP开始执行,此时任务 H P HP HP也试图获取互斥量来访问共享资源,但是此时互斥量已经被任务 L P LP LP占有了,因此任务 H P HP HP获取互斥量失败从而进入阻塞等待状态。
- 此时任务 L P LP LP继续执行,因为优先级继承机制,任务 L P LP LP的优先级现在变为5,正因为如此这里任务 L P LP LP会继续执行直到使用完共享资源并释放,然后归还互斥量令牌(此时任务 L P LP LP的优先级也会被设置为初始值),而不会像图16中的例子那样被任务 M P MP MP抢占执行。
- 因为任务 L P LP LP已经使用完共享资源并且归还了管理共享资源的互斥量的令牌,进入阻塞,此时任务 H P HP HP从阻塞等待状态中唤醒并获取了管理共享资源的互斥量的令牌,因此有了共享资源的使用权限。任务 H P HP HP使用完共享资源并且归还了管理共享资源的互斥量的令牌之后进入阻塞,此时任务 M P MP MP获得机会开始执行。
对比图16和图17中的两个例子,图17中在具有优先级继承机制下,任务 H P HP HP等待任务 L P LP LP释放管理共享资源的互斥量的时间明显减少。
/*******************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************/
但是互斥量也不是完美的,在使用互斥量用于管理对互斥资源访问的时候有可能会造成死锁(
D
e
a
d
l
o
c
k
Deadlock
Deadlock)的情况。那什么是死锁,我们来看一下下面的场景:
- 任务 A A A成功执行且 T a k e Take Take到了互斥量 X X X
- 任务 B B B的优先级高于任务 A A A,此时任务 A A A的运行被任务 B B B抢占
- 任务 B B B成功执行且 T a k e Take Take到了互斥量 Y Y Y,任务 B B B继续执行并且尝试 T a k e Take Take互斥量 X X X,但是此时互斥量 X X X已经被任务 A A A占有了,此时任务 B B B选择进入阻塞状态来等待任务 A A A释放互斥量 X X X
- 此时任务 A A A继续执行并且尝试 T a k e Take Take互斥量 Y Y Y,但是此时互斥量 Y Y Y已经被任务 B B B占有了,此时任务 A A A选择进入阻塞状态来等待任务 B B B释放互斥量 Y Y Y
以上场景最后的结果是任务
A
A
A等待任务
B
B
B释放互斥量
Y
Y
Y而任务
B
B
B又在等待任务
A
A
A释放互斥量
X
X
X,这样搞得最后两个任务都动不了。和优先级翻转一样,死锁最好的解决办法就是在设计之初从系统设计上保保证不出现死锁。还有就是
T
a
k
e
Take
Take操作或类似的操作的参数
x
T
i
c
k
s
T
o
W
a
i
t
xTicksToWait
xTicksToWait最好不要设置为
p
o
r
t
M
A
X
_
D
E
L
A
Y
portMAX\_DELAY
portMAX_DELAY,而是设置为比理论上需要等待的时间的最大值大一点,这样超时的时候可以根据返回的错误状态来检测到设计的错误。
以上死锁的场景发生在两个任务之间,但是死锁也有可能发生在同一个任务,这种情况发生在当一个任务已经通过
T
a
k
e
Take
Take操作成功获得了互斥量的使用权限,但是此时又一次或多次的进行
T
a
k
e
Take
Take操作,此时这个任务就会处于等待它自己释放它自己占用的互斥量的死锁状态。递归互斥量可以避免一个任务陷入等待它自己释放它自己占用的互斥量的死锁状态。占有递归互斥量的任务可以对递归互斥量多次进行
T
a
k
e
Take
Take操作,但是如果要释放递归互斥量,那么
G
i
v
e
Give
Give操作要和
T
a
k
e
Take
Take操作的次数一样,比如占有递归互斥量的任务已经对递归互斥量进行了10次
T
a
k
e
Take
Take操作,那么只有这个占有递归互斥量的任务对这个递归互斥量同样进行10次
G
i
v
e
Give
Give操作之后这个任务占有的递归互斥量才算是释放了。递归互斥量和互斥量的属性基本一样,除了可以多次进行
T
a
k
e
Take
Take操作和
G
i
v
e
Give
Give操作(
T
a
k
e
Take
Take操作会将图18中的递归互斥量的队列信息结构体的元素
u
x
R
e
c
u
r
s
i
v
e
C
a
l
l
C
o
u
n
t
uxRecursiveCallCount
uxRecursiveCallCount的值加一,
G
i
v
e
Give
Give操作会将图18中的递归互斥量的队列信息结构体的元素
u
x
R
e
c
u
r
s
i
v
e
C
a
l
l
C
o
u
n
t
uxRecursiveCallCount
uxRecursiveCallCount的值减一,当元素
u
x
R
e
c
u
r
s
i
v
e
C
a
l
l
C
o
u
n
t
uxRecursiveCallCount
uxRecursiveCallCount的值为0的时候就说明这个递归互斥量被释放了,元素
x
M
u
t
e
x
H
o
l
d
e
r
xMutexHolder
xMutexHolder就是占用互斥量或递归互斥量的任务的任务句柄),递归互斥量也具有优先级继承机制且不能在中断中使用。
本小节的讲解就到这里,更多细节可以参考野火的讲解,图19中的文档的第6章和第7章以及图20中和信号量相关的 F r e e R T O S FreeRTOS FreeRTOS的接口以及使用介绍。本小节上面涉及到的实际的例子的工程代码在这里。