前言
本篇文章带大家学习什么是互斥锁,并且学习一下互斥锁中一些函数的使用方法。
一、互斥锁的概念
FreeRTOS中的互斥锁(Mutex)是一种同步机制,用于控制多个任务对共享资源的访问,以确保只有一个任务可以访问该资源,从而避免竞争条件和数据损坏。互斥锁是一种二进制信号量,它只有两个状态:已锁定和未锁定。
二、互斥锁相关函数
1.xSemaphoreCreateMutex():
函数原型:SemaphoreHandle_t x SemaphoreCreateMutex( void );
功能:用于创建一个互斥锁。
返回值:返回一个指向新创建的互斥锁的句柄(handle)。
2.xSemaphoreTake():
函数原型:BaseType_t x SemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait);
功能:尝试获取互斥锁。如果互斥锁当前可用(未被锁定),则获取成功;否则,任务将被阻塞,直到互斥锁可用或达到超时时间。
参数:
xSemaphore:互斥锁的句柄。
xTicksToWait:等待获取互斥锁的最大时间,可以使用portMAX_DELAY来表示永久等待。
返回值:获取成功时返回pdTRUE,获取失败(超时或出错)时返回pdFALSE。
3.xSemaphoreGive():
函数原型:BaseType_t x SemaphoreGive(SemaphoreHandle_t xSemaphore);
功能:释放互斥锁,允许其他任务获取它。
参数:xSemaphore:互斥锁的句柄。
返回值:成功返回pdTRUE,失败返回pdFALSE。
4.xSemaphoreGiveFromISR():
函数原型:BaseType_t x SemaphoreGiveFromISR(SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken);
功能:从中断服务程序中释放互斥锁。
参数:
xSemaphore:互斥锁的句柄。
pxHigherPriorityTaskWoken:一个指向BaseType_t的指针,用于指示是否唤醒了更高优先级的任务。
返回值:成功返回pdTRUE,失败返回pdFALSE。
三、使用互斥锁完成同步
前面的文章使用信号量解决了访问共享资源的问题,这里使用互斥锁也是可以来解决这个问题的。
// 定义互斥信号量 SemaphoreHandle_t mutex; void Task1Function(void *pvParameters) { while (1) { // 等待获取互斥信号量 if (xSemaphoreTake(mutex, portMAX_DELAY) == pdTRUE) { printf("Task1\n"); // 释放互斥信号量 xSemaphoreGive(mutex); } vTaskDelay(1000 / portTICK_PERIOD_MS); // 任务延时一秒 } } void Task2Function(void *pvParameters) { while (1) { // 等待获取互斥信号量 if (xSemaphoreTake(mutex, portMAX_DELAY) == pdTRUE) { printf("Task2\n"); // 释放互斥信号量 xSemaphoreGive(mutex); } vTaskDelay(1000 / portTICK_PERIOD_MS); // 任务延时一秒 } } xTaskCreate(Task1Function, "Task1", 100, NULL, 1, NULL); xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL); // 创建互斥信号量 mutex = xSemaphoreCreateMutex();
四、FreeRTOS中互斥锁存在的问题
在FreeRTOS中没有实现谁上锁就应该由谁来解锁的机制,那么这样的话一个任务上锁后可以被其他的任务解锁并且使用,这个缺陷就可能会导致一系列的问题发生。
示例代码中创建了两个任务,任务1的优先级比任务2高,那么任务1会先获取到互斥量然后进入无限的循环中,任务2随后开始获取互斥量,第一次获取互斥量失败打印失败,获取失败后任务2自行开锁,随后又获取互斥量成功。
运行结果:
void Task1Function(void * param) { const TickType_t xTicksToWait = pdMS_TO_TICKS(10UL); BaseType_t xStatus; xStatus = xSemaphoreTake(xMutex, portMAX_DELAY);//获取互斥量 printf("Task1 take the Mutex %s\r\n", \ (xStatus == pdTRUE) ? "Success" : "Failed"); while(1) { vTaskDelay(xTicksToWait); } } void Task2Function(void * param) { const TickType_t xTicksToWait = pdMS_TO_TICKS(10UL); BaseType_t xStatus; xStatus = xSemaphoreTake(xMutex, 0);//获取互斥量 printf("Task2 take the Mutex %s\r\n", \ (xStatus == pdTRUE) ? "Success" : "Failed"); if(xStatus == pdFALSE)//获取互斥量失败,自己开锁 { xSemaphoreGive(xMutex); } xStatus = xSemaphoreTake(xMutex, portMAX_DELAY);//获取互斥量 printf("Task2 take the Mutex %s\r\n", \ (xStatus == pdTRUE) ? "Success" : "Failed"); while(1) { vTaskDelay(xTicksToWait); } } xTaskCreate(Task1Function, "Task1", 100, NULL, 2, &xHandleTask1); xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL);
五、优先级反转和优先级继承
在FreeRTOS中,优先级反转和优先级继承是解决多任务系统中的竞争条件和优先级反转问题的两种相关技术。
1.优先级反转(Priority Inversion):
场景:考虑一个具有三个任务的系统:任务A(高优先级)、任务B(中优先级)、和任务C(低优先级)。任务A正在等待一个共享资源,任务B持有该资源,而任务C需要执行并尝试获取资源。
问题:如果任务B持有资源,任务A会被阻塞,但由于任务C的优先级低于任务A,任务C会被调度并阻止任务B的执行,导致任务A被任务C所阻塞,产生了优先级反转问题。
解决方案:优先级反转的解决方案是在任务B持有资源时将其优先级提高到与任务A相同的优先级。这样,任务C不会阻止任务B的执行,任务A会优先执行。一旦任务A完成并释放资源,任务B的优先级将被还原。
2.优先级继承(Priority Inheritance):
场景:同样考虑一个三个任务的系统:任务A(高优先级)、任务B(中优先级)、和任务C(低优先级)。任务A正在等待一个共享资源,任务B持有该资源,而任务C需要执行并尝试获取资源。
问题:与优先级反转类似,如果任务C的优先级低于任务A,任务C会被调度并阻止任务B的执行,导致任务A被任务C所阻塞。
解决方案:优先级继承引入了一个概念,即任务优先级的临时提升。在这种情况下,任务C的优先级将被提升到与任务A相同的优先级,以确保任务A不会被任务C所阻塞。任务B的优先级仍然是中优先级,但任务C的优先级提升使其不会干扰任务A的执行。
在FreeRTOS中,优先级继承通常是默认行为,这意味着在等待共享资源时,任务的优先级会根据需要自动提升,以避免优先级反转问题。这是通过操作系统内核自动管理任务优先级的方式来实现的。任务的优先级将在等待资源期间提高,并在释放资源后还原。
优先级继承和优先级反转是关键的实时系统技术,确保高优先级任务不会被低优先级任务无限期地阻塞,从而提高系统的可靠性和可预测性。
优先级反转示例:
这里创建三个优先级不同的任务,任务1的优先级最高,任务2其次,任务3最低。
这里讲解一下任务的运行流程,首先任务1优先级最高最先运行,使用vTaskDelay阻塞释放CPU,任务2运行,任务2也主动释放CPU,任务3运行此时任务3先获取信号量,然后再去执行一些耗时的操作,导致无法释放信号量,此时任务2继续运行,一段后任务1想要获取到信号量,但是此时信号量被任务3占用,导致任务1无法获取信号量,此时就导致任务2一直运行。
运行效果:
static SemaphoreHandle_t xSem; int flag1 = 0; int flag2 = 0; int flag3 = 0; void Task1Function(void * param) { flag1 = 1; flag2 = 0; flag3 = 0; printf("Task1 is Start\r\n"); vTaskDelay(30); while(1) { printf("Task1 is Runing\r\n"); flag1 = 1; flag2 = 0; flag3 = 0; xSemaphoreTake(xSem, portMAX_DELAY);//获取信号量 flag1 = 1; flag2 = 0; flag3 = 0; xSemaphoreGive(xSem);//释放信号量 } } void Task2Function(void * param) { flag1 = 1; flag2 = 0; flag3 = 0; printf("Task2 is Start\r\n"); vTaskDelay(10); while(1) { flag1 = 0; flag2 = 1; flag3 = 0; } } void Task3Function(void * param) { int i; char c = 'A'; printf("Task3 is Start\r\n"); while(1) { flag1 = 0; flag2 = 0; flag3 = 1; xSemaphoreTake(xSem, portMAX_DELAY);//获取信号量 for(i = 0; i < 26; i++) { //执行一些耗时的操作 printf("%c", c + i); flag1 = 0; flag2 = 0; flag3 = 1; } xSemaphoreGive(xSem);//释放信号量 vTaskDelay(1); } } xSem = xSemaphoreCreateBinary(); xSemaphoreGive(xSem); xTaskCreate(Task1Function, "Task1", 100, NULL, 3, &xHandleTask1); xTaskCreate(Task2Function, "Task2", 100, NULL, 2, NULL); xTaskCreate(Task3Function, "Task3", 100, NULL, 1, NULL);
使用互斥锁解决优先级反转:
这里使用xMutex代替xSem即可完成这个实验。
static SemaphoreHandle_t xMutex;
当任务1无法运行时会将优先级和任务3进行一次交换,当任务3释放完信号量后,信号量交互回来,任务1继续运行。
运行结果:
六、死锁
死锁(Deadlock):
死锁是指在多任务系统中,多个任务相互等待对方释放资源,从而导致所有任务无法继续执行的状态。这是一种非常危险的情况,因为它会导致系统停滞,无法正常运行。死锁通常发生在以下情况下:
任务之间互斥地访问共享资源(例如,使用互斥锁)。
任务持有一个资源,同时等待另一个任务持有的资源。
死锁示例:
这里创建两个互斥锁A和B,然后创建两个任务:任务1和任务2,任务1先获取互斥锁A,然后释放CPU资源,然后任务2运行,获取互斥锁B,然后释放CPU资源,任务1运行获取互斥锁B,这时任务1获取不到互斥锁B,因为互斥锁B已经被任务2获取,此时任务2运行,获取互斥锁A,也是无法获取的状态,这时任务1和任务2就陷入了互相等待的状态,发生了死锁。
static SemaphoreHandle_t xMutexA; static SemaphoreHandle_t xMutexB; void Task1Function(void * param) { while(1) { xSemaphoreTake(xMutexA, portMAX_DELAY);//获取互斥锁A vTaskDelay(1); xSemaphoreTake(xMutexB, portMAX_DELAY);//获取互斥锁B printf("Take A and B is ok\r\n"); xSemaphoreGive(xMutexA);//释放互斥锁A xSemaphoreGive(xMutexB);//释放互斥锁B } } void Task2Function(void * param) { while(1) { xSemaphoreTake(xMutexB, portMAX_DELAY);//获取互斥锁B vTaskDelay(1); xSemaphoreTake(xMutexA, portMAX_DELAY);//获取互斥锁A printf("Take A and B is ok\r\n"); xSemaphoreGive(xMutexB);//释放互斥锁B xSemaphoreGive(xMutexA);//释放互斥锁A } } xMutexA = xSemaphoreCreateMutex(); xMutexB = xSemaphoreCreateMutex();
七、递归锁
递归锁是一种互斥锁的扩展,允许同一个任务多次获取同一个互斥锁而不会产生死锁。在标准的互斥锁中,如果一个任务已经获得锁,再次尝试获取锁会导致阻塞,因为互斥锁是互斥的,只允许一个任务持有。
递归锁允许任务在已经持有锁的情况下继续获取锁,每次获取都需要相应的释放。这对于某些算法和任务设计来说很有用,但需要谨慎使用,因为滥用递归锁可能会导致复杂的代码和潜在的问题,例如死锁。
在FreeRTOS中,可以使用递归互斥锁来实现递归锁的功能,通过xSemaphoreCreateRecursiveMutex()创建递归互斥锁,并使用xSemaphoreTakeRecursive()和xSemaphoreGiveRecursive()等函数来获取和释放递归锁。
// 创建一个递归互斥锁 SemaphoreHandle_t xRecursiveMutex; void Task1(void *pvParameters) { while (1) { // 获取递归锁,可以多次获取 xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY); // 临界区代码 // ... // 释放递归锁 xSemaphoreGiveRecursive(xRecursiveMutex); // 继续执行任务 vTaskDelay(pdMS_TO_TICKS(100)); } } void Task2(void *pvParameters) { while (1) { // 获取递归锁,可以多次获取 xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY); // 临界区代码 // ... // 释放递归锁 xSemaphoreGiveRecursive(xRecursiveMutex); // 继续执行任务 vTaskDelay(pdMS_TO_TICKS(100)); } } int main(void) { // 创建递归互斥锁 xRecursiveMutex = xSemaphoreCreateRecursiveMutex(); if (xRecursiveMutex != NULL) { // 创建任务 xTaskCreate(Task1, "Task1", configMINIMAL_STACK_SIZE, NULL, 1, NULL); xTaskCreate(Task2, "Task2", configMINIMAL_STACK_SIZE, NULL, 2, NULL); // 启动FreeRTOS调度器 vTaskStartScheduler(); } // 如果创建递归互斥锁失败,应采取适当的错误处理措施 while (1) { // 程常不会执行到这里 } return 0; }
总结
本篇文章就讲解到这里,下篇文章我们继续讲解FreeRTOS入门教程。