在FreeRTOS中,同步是指任务之间按照某种规则进行协调和按序执行的过程。其目的是保证任务或线程之间的有序交互,使它们能够按照预期的顺序完成各自的操作或实现特定的约束条件。常见的同步场景包括等待其他任务完成、等待某个条件满足、协调任务之间的依赖关系等。
FreeRTOS提供了多种同步机制,例如信号量、互斥量、消息队列等,用于实现任务之间的同步。这些机制可以帮助任务之间进行协作,以确保它们按照一定的顺序、时机和约束进行执行。
同步机制在FreeRTOS中非常重要,因为它们可以确保系统的正确性和稳定性。如果没有同步机制,任务之间可能会出现竞争条件,导致系统行为不可预测。通过使用同步机制,FreeRTOS可以确保任务之间的正确交互,从而提高系统的可靠性和性能。
FreeRTOS中,互斥是一种同步机制,用于保护共享资源,确保任务访问这些资源时的原子性,避免数据错误。具体来说,互斥是指在多任务环境中,运行特定代码段时确保数据的一致性和完整性,避免多个任务同时访问和修改共享资源导致错误的发生。它通过互斥量(又称互斥信号量)来实现,互斥量是一种特殊的二值信号量,用于实现对临界资源的独占式处理。任意时刻互斥量的状态只有两种,开锁或闭锁。当互斥量被任务持有时,该互斥量处于闭锁状态,这个任务获得互斥量的所有权。当该任务释放这个互斥量时,该互斥量处于开锁状态,任务失去该互斥量的所有权。当一个任务持有互斥量时,其他任务将不能再对该互斥量进行开锁或持有。持有该互斥量的任务也能够再次获得这个锁而不被挂起,这就是递归访问,也就是递归互斥量的特性。
一句话理解同步与互斥:我等你用完厕所,我再用厕所。
什么叫同步?就是:哎哎哎,我正在用厕所,你等会。
什么叫互斥?就是:哎哎哎,我正在用厕所,你不能进来。
同步与互斥经常放在一起讲,是因为它们之的关系很大, “互斥”操作可以使用“同步”来实现。我“等”你用完厕所,我再用厕所。这不就是用“同步”来实现“互斥”吗?
实验目的:计算ul变量累加到1000000需要多长时间
具体实现:
创建2个Task,定义一个全局变量taskFlag,当taskFlag等于1时表示Task1正在运行,当taskFlag等于0时表示Task2正在运行
Task1:
Task2:
实验代码:
#define mainDELAY_LOOP_COUNT 1000000
volatile TickType_t vtotleTime;
volatile TickType_t vstartTime = 0, vendTime = 0;
volatile bool endFlag = FALSE;
volatile bool endLock = FALSE;
volatile uint8_t taskFlag = 0;
void vTask1( void *pvParameters )
{
volatile uint32_t ul; /* volatile用来避免被优化掉 */
vstartTime = xTaskGetTickCount();
/* 打印任务1的信息 */
printf( "Count start: %d\r\n",vstartTime );
/* 任务函数的主体一般都是无限循环 */
for( ;; )
{
/* 表示Task1在运行 */
taskFlag = 1;
/* 延迟一会(比较简单粗暴) */
if(endLock == FALSE)
{
ul++;
if((ul > mainDELAY_LOOP_COUNT) && (endFlag != TRUE))
{
endFlag = TRUE;
vendTime = xTaskGetTickCount();
vtotleTime = vendTime - vstartTime;
vTaskDelay(10);
}
}
}
}
void vTask2( void *pvParameters )
{
volatile uint32_t ul; /* volatile用来避免被优化掉 */
/* 任务函数的主体一般都是无限循环 */
for( ;; )
{
/* 表示Task2在运行 */
taskFlag = 0;
if(endFlag == TRUE)
{
endFlag = FALSE;
printf( "Count end: %d\r\nTotle time: %d\r\n",vendTime, vtotleTime);
endLock = TRUE;
}
}
}
int main( void )
{
prvSetupHardware();
xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL);
xTaskCreate(vTask2, "Task 2", 1000, NULL, 1, NULL);
/* 启动调度器 */
vTaskStartScheduler();
/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
return 0;
}
运行结果:
实验分析:从taskFlag分析,Task1在累加ul变量时,Task2仍然在运行,仍然耗费CPU资源,理论上分析如果在ul累加期间,使Task2任务挂起,ul从0累加到1000000耗时会减少一半。
基于“二”中的示例进行优化
优化思路:使用队列通信代替Task2对全局变量endFlag的判断,队列传输数据结构体:
typedef struct
{
TickType_t startTime;
TickType_t endTime;
TickType_t stopFlag;
}TIME;
实验代码:
#define mainDELAY_LOOP_COUNT 1000000
typedef struct
{
TickType_t startTime;
TickType_t endTime;
TickType_t stopFlag;
}TIME;
void vTask1( void *pvParameters )
{
TIME time1;
volatile uint32_t ul; /* volatile用来避免被优化掉 */
volatile TickType_t buf[10];
time1.stopFlag = FALSE;
T1 = xTaskGetTickCount();
time1.startTime = xTaskGetTickCount();
/* 打印任务1的信息 */
printf( "Count start: %d\r\n",time1.startTime);
/* 任务函数的主体一般都是无限循环 */
for( ;; )
{
/* 表示Task1在运行 */
taskFlag = 1;
/* 延迟一会(比较简单粗暴) */
ul++;
if((ul > mainDELAY_LOOP_COUNT) && time1.stopFlag != TRUE)
{
T2 = xTaskGetTickCount();
time1.endTime = xTaskGetTickCount();
time1.stopFlag = TRUE;
xQueueSend(task1Handle, &time1, NULL);
}
}
}
void vTask2( void *pvParameters )
{
TIME time2;
volatile uint32_t ul; /* volatile用来避免被优化掉 */
/* 任务函数的主体一般都是无限循环 */
for( ;; )
{
/* 表示Task2在运行 */
taskFlag = 0;
xQueueReceive(task1Handle, &time2, portMAX_DELAY);
if(time2.stopFlag == TRUE)
{
printf( "Count end: %d\r\nTotle time: %d\r\n",time2.endTime, time2.endTime - time2.startTime);
time2.stopFlag = FALSE;
}
}
}
int main( void )
{
prvSetupHardware();
xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL);
xTaskCreate(vTask2, "Task 2", 1000, NULL, 1, NULL);
task1Handle = xQueueCreate(1,sizeof(TIME));
/* 启动调度器 */
vTaskStartScheduler();
/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
return 0;
}
运行结果:
实验分析:使用队列时,Task2未接收到队列的数据时会进入挂起状态,不会再占用CPU资源,Task1往队列发送数据时,会同时将Task2从挂起状态改变为就绪或者运行状态。
实验目的:不同任务访问相同临界资源(比如全局变量)时有缺陷的互斥
仍然使用“二”中的示例,进行简单的修改,在“二”的代码中Task1累加完ul变量得到累加耗时后使用了vTaskDelay函数使Task1挂起,如果不使用vTaskDelay函数就会出现有缺陷的互斥现象
实验代码:
#define mainDELAY_LOOP_COUNT 1000000
volatile TickType_t vtotleTime;
volatile TickType_t vstartTime = 0, vendTime = 0;
volatile bool endFlag = FALSE;
volatile bool endLock = FALSE;
volatile uint8_t taskFlag = 0;
void vTask1( void *pvParameters )
{
volatile uint32_t ul; /* volatile用来避免被优化掉 */
vstartTime = xTaskGetTickCount();
/* 打印任务1的信息 */
printf( "Count start: %d\r\n",vstartTime );
/* 任务函数的主体一般都是无限循环 */
for( ;; )
{
/* 表示Task1在运行 */
taskFlag = 1;
/* 延迟一会(比较简单粗暴) */
if(endLock == FALSE)
{
ul++;
if((ul > mainDELAY_LOOP_COUNT) && (endFlag != TRUE))
{
endFlag = TRUE;
vendTime = xTaskGetTickCount();
vtotleTime = vendTime - vstartTime;
//vTaskDelay(10);
}
}
}
}
void vTask2( void *pvParameters )
{
volatile uint32_t ul; /* volatile用来避免被优化掉 */
/* 任务函数的主体一般都是无限循环 */
for( ;; )
{
/* 表示Task2在运行 */
taskFlag = 0;
if(endFlag == TRUE)
{
endFlag = FALSE;
printf( "Count end: %d\r\nTotle time: %d\r\n",vendTime, vtotleTime);
endLock = TRUE;
}
}
}
int main( void )
{
prvSetupHardware();
xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL);
xTaskCreate(vTask2, "Task 2", 1000, NULL, 1, NULL);
/* 启动调度器 */
vTaskStartScheduler();
/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
return 0;
}
运行结果:
实验分析:从运行结果来看,Task2打印了2次,理论上从代码分析,程序运行到Task2的打印时,应该是先将endFlag设置为了FALSE,但是打印了2次说明endFlag的值没有写入成功,单步调试分析一下:
将endFlag值在Watch1中显示,在Task2打印处设置一个断点,全速运行
从代码来看Task1也会修改endFlag的值,在Task1中计算累计时间处再打一个断点,全速运行
再次全速运行到Task2中打印处
再次全速运行再也不会听到断点处,这就是Task2中打印了2次的详细步骤。
缺陷原理:
1、当运行到Task2打印处是,endFlag被更改为FALSE
2、但是Task2还未来得及更改完endLock就切换到了Task1
3、Task1中又将endFlag更改为TRUE
4、切换到Task2时再次打印了一遍
5、这次在切换到Task1之前修改完了endLock的值
6、再切换到Task1时不会再更改endFlag的值了
修改代码逻辑可以避免Task2打印2次:
修改前:
void vTask2( void *pvParameters )
{
volatile uint32_t ul; /* volatile用来避免被优化掉 */
/* 任务函数的主体一般都是无限循环 */
for( ;; )
{
/* 表示Task2在运行 */
taskFlag = 0;
if(endFlag == TRUE)
{
endFlag = FALSE;
printf( "Count end: %d\r\nTotle time: %d\r\n",vendTime, vtotleTime);
endLock = TRUE;
}
}
}
修改后:
void vTask2( void *pvParameters )
{
volatile uint32_t ul; /* volatile用来避免被优化掉 */
/* 任务函数的主体一般都是无限循环 */
for( ;; )
{
/* 表示Task2在运行 */
taskFlag = 0;
if(endFlag == TRUE)
{
endLock = TRUE;
endFlag = FALSE;
printf( "Count end: %d\r\nTotle time: %d\r\n",vendTime, vtotleTime);
}
}
}
这样修改可以避免Task2打印2次,因为在打印时Task2已经完成了对endLock和endFlag的修改,但是同样存在缺陷,原因是:
C语言中1条给全局变量的赋值语句并不是程序的最小运行单位,C语言的本质是汇编,从Task2的汇编码可以看到,将endFlag的值赋值为0分为3个步骤:
假设在赋值过程中运行完汇编第二步后就切换了任务,其他任务对该临界资源也进行了修改,再切换到当前任务时该被修改的临界资源又被修改了。因此这种修改也并不是万无一失的。
补充一个办法:当前任务需要修改临界资源时,现将系统所有中断关闭,暂停任务调度和中断,修改完临界资源后再将中断恢复,恢复任务调度和中断。
但是这种方法关闭中断也对系统有一定的风险!
正确使用互斥与同步,FreeRTOS提供的方法是安全可靠的,比如队列、信号量、互斥量、任务通知等等,就像“三、优化有缺陷的同步”一样,使用FreeRTOS提供的方法同样可以优化有缺陷的互斥。