同步与互斥与通信
同步与互斥
同步:两任务要协调
互斥:两任务要争用
举一个例子。在团队活动里,同事A先写完报表,经理B才能拿去向领导汇报。经理B必须等同事A完成报表,AB之间有依赖,B必须放慢脚步,被称为同步。在团队活动中,同事A已经使用会议室了,经理B也想使用,即使经理B是领导,他也得等着,这就叫互斥。经理B跟同事A说:你用完会议室就提醒我。这就是使用"同步"来实现"互斥"。
仅仅使用简单的全局变量的方法实现同步与互斥是有缺陷的
可以实现同步与互斥的内核方法有:任务通知(task notification)、队列(queue)、事件组(event group)、信号量(semaphoe)、互斥量(mutex)。
-
队列(Queue)
- 功能:用于在任务之间传递数据。队列是一种先进先出(FIFO)数据结构,可以存储一定数量的数据项。
- 用途:适用于需要传递数据或消息的场景,例如生产者-消费者模式。任务可以将数据发送到队列中,另一个任务可以从队列中接收数据。队列可以存储任意类型的数据,并且支持阻塞操作,以便任务可以在队列为空时等待。
-
事件组(Event Group)
- 功能:用于在多个任务之间传递事件或信号。这些事件可以是单个或多个标志位(bit),每个任务可以对这些标志位进行设置、清除或等待。
- 用途:适合处理需要多个任务之间协调的场景。例如,一个任务可以等待多个事件的发生(例如,一个任务等待两个条件同时满足),而另一个任务可以设置这些事件,从而通知等待的任务。事件组也可以实现任务的优先级控制。
-
信号量(Semaphore)
- 功能:用于控制对共享资源的访问。信号量主要有两种类型:二进制信号量和计数信号量。
- 用途:二进制信号量通常用于实现互斥,确保同一时间只有一个任务可以访问特定资源。计数信号量允许多个任务同时访问资源,直到达到预定的最大值。信号量可以帮助避免竞态条件和数据不一致问题。
-
任务通知(Task Notification)
- 功能:用于实现任务间的简单通知机制。在 FreeRTOS 中,每个任务都有一个相关的通知值,可以通过直接从一个任务通知另一个任务。
- 用途:适合需要简单、低开销的通知机制的场景。任务可以使用通知值来发送较小的信息(如指示一个事件发生),并且可以选择使用阻塞或非阻塞方式等待通知。这种机制比队列更加轻量级。
队列
创建队列(动态、静态)
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
| 参数 | 说明 |
|---|---|
| uxQueueLength | 队列长度,最多能存放多少个数据(item) |
| uxItemSize | 每个数据(item)的大小:以字节为单位 |
| 返回值 | 非0:成功,返回句柄,以后使用句柄来操作队列 NULL:失败,因为内存不足 |
QueueHandle_t xQueueCreateStatic(* UBaseType_t uxQueueLength,* UBaseType_t uxItemSize,* uint8_t *pucQueueStorageBuffer,* StaticQueue_t *pxQueueBuffer* );
| 参数 | 说明 |
|---|---|
| uxQueueLength | 队列长度,最多能存放多少个数据(item) |
| uxItemSize | 每个数据(item)的大小:以字节为单位 |
| pucQueueStorageBuffer | 指向一个用于存储队列的内存区域的指针,如果uxItemSize非0,pucQueueStorageBuffer必须指向一个uint8_t数组, 此数组大小至少为"uxQueueLength * uxItemSize" |
| pxQueueBuffer | 必须执行一个StaticQueue_t结构体,用来保存队列的数据结构(控制信息) |
| 返回值 | 非0:成功,返回句柄,以后使用句柄来操作队列 NULL:失败,因为pxQueueBuffer为NULL |
读队列
使用 xQueueReceive() 函数读队列,读到一个数据后,队列中该数据会被移除。这个函数有两个版本:在任务中使用、在ISR中使用。函数原型如下:
BaseType_t xQueueReceive( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait ); BaseType_t xQueueReceiveFromISR( QueueHandle_t xQueue, void *pvBuffer, BaseType_t *pxTaskWoken );
| 参数 | 说明 |
|---|---|
| xQueue | 队列句柄,要读哪个队列 |
| pvBuffer | bufer指针,队列的数据会被复制到这个buffer 复制多大的数据?在创建队列时已经指定了数据大小 |
| xTicksToWait | 如果队列空则无法读出数据,可以让任务进入阻塞状态, xTicksToWait表示阻塞的最大时间(Tick Count)。 如果被设为0,无法读出数据时函数会立刻返回; 如果被设为portMAX_DELAY,则会一直阻塞直到有数据可写 |
| 返回值 | pdPASS:从队列读出数据入 errQUEUE_EMPTY:读取失败,因为队列空了 |
写队列
可以把数据写到队列头部,也可以写到尾部,这些函数有两个版本:在任务中使用、在ISR中使用。函数原型如下:
/* 等同于xQueueSendToBack * 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait */ BaseType_t xQueueSend( QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait ); /* * 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait */ BaseType_t xQueueSendToBack( QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait ); /* * 往队列尾部写入数据,此函数可以在中断函数中使用,不可阻塞 */ BaseType_t xQueueSendToBackFromISR( QueueHandle_t xQueue, const void *pvItemToQueue, BaseType_t *pxHigherPriorityTaskWoken ); /* * 往队列头部写入数据,如果没有空间,阻塞时间为xTicksToWait */ BaseType_t xQueueSendToFront( QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait ); /* * 往队列头部写入数据,此函数可以在中断函数中使用,不可阻塞 */ BaseType_t xQueueSendToFrontFromISR( QueueHandle_t xQueue, const void *pvItemToQueue, BaseType_t *pxHigherPriorityTaskWoken );
| 参数 | 说明 |
|---|---|
| xQueue | 队列句柄,要写哪个队列 |
| pvItemToQueue | 数据指针,这个数据的值会被复制进队列, 复制多大的数据?在创建队列时已经指定了数据大小 |
| xTicksToWait | 如果队列满则无法写入新数据,可以让任务进入阻塞状态, xTicksToWait表示阻塞的最大时间(Tick Count)。 如果被设为0,无法写入数据时函数会立刻返回; 如果被设为portMAX_DELAY,则会一直阻塞直到有空间可写 |
| 返回值 | pdPASS:数据成功写入了队列 errQUEUE_FULL:写入失败,因为队列满了。 |
编码实例1
改造红外任务
之前:game1任务->挡球板任务->读取缓冲区,写缓冲区由红外中断完成。这种方式没有数据时会循环读取,导致很耗费cpu资源
改造:game1任务->挡球板任务->读队列,写队列在红外中断中完成。
编码实例2
使用队列实现多任务输入
系统框图

红外中断接收到按键值之后,会把按键值转换为游戏控制的键值写入挡球板队列。编码器中断接收到编码器旋转后,旋转数据写入编码器队列,编码器任务读到数据,进行数据转换(编码器数据转换为游戏数据),并把数据写入挡球板队列。
使用队列集改造系统结构
红外遥控器、旋转编码器,它们的驱动程序应该专注于“产生硬件数据”,不应该跟“业务有任何联系”。比如:红外遥控器驱动程序里,它只应该把键值记录下来、写入某个队列,它不应该把键值转换为游戏的控制键。在红外遥控器的驱动程序里,不应该有游戏相关的代码,这样,切换使用场景时,这个驱动程序还可以继续使用。把红外遥控器的按键转换为游戏的控制键,应该在红外任务里实现。
但是,如果使用编码器的模式,也就是每个硬件中断都创建一个对应的任务,那么有多个硬件设备时,会很消耗CPU资源。
因此我们引入队列集这一概念,它可以读取多个队列。引入队列集之后,我们就可以只创建一个“inputTask”,来代替红外任务、编码器任务等多个硬件任务。在“inputTask”中,它读取各个设备的队列,得到数据后再分别转换为游戏的控制键。
队列集的本质也是队列,只不过里面存放的是“队列句柄”。使用过程如下:
-
- 创建队列A,它的长度是n1
- 创建队列B,它的长度是n2
- 创建队列集S,它的长度是“n1+n2”
- 把队列A、B加入队列集S
- 这样,写队列A的时候,会顺便把队列A的句柄写入队列集S
- 这样,写队列B的时候,会顺便把队列B的句柄写入队列集S
- InputTask先读取队列集S,它的返回值是一个队列句柄,这样就可以知道哪个队列有有数据了;然后InputTask再读取这个队列句柄得到数据。
创建队列集
QueueSetHandle_t xQueueCreateSet( const UBaseType_t uxEventQueueLength )
| 参数 | 说明 |
|---|---|
| uxQueueLength | 队列集长度,最多能存放多少个数据(队列句柄) |
| 返回值 | 非0:成功,返回句柄,以后使用句柄来操作队列NULL:失败,因为内存不足 |
把队列放入队列集
BaseType_t xQueueAddToSet( QueueSetMemberHandle_t xQueueOrSemaphore,
QueueSetHandle_t xQueueSet );
| 参数 | 说明 |
|---|---|
| xQueueOrSemaphore | 队列句柄,这个队列要加入队列集 |
| xQueueSet | 队列集句柄 |
| 返回值 | pdTRUE:成功pdFALSE:失败 |
读取队列集
QueueSetMemberHandle_t xQueueSelectFromSet( QueueSetHandle_t xQueueSet, TickType_t const xTicksToWait );
| 参数 | 说明 |
|---|---|
| xQueueSet | 队列集句柄 |
| xTicksToWait | 如果队列集空则无法读出数据,可以让任务进入阻塞状态,xTicksToWait表示阻塞的最大时间(Tick Count)。如果被设为0,无法读出数据时函数会立刻返回;如果被设为portMAX_DELAY,则会一直阻塞直到有数据可写 |
| 返回值 | NULL:失败,队列句柄:成功 |
改善后的系统框架

写队列的同时也会写队列集,Input任务会读取队列集得到队列句柄,然后根据句柄进行后续的数据处理,处理完毕后写入挡球板队列。
赛车游戏
红外遥控器,用不同的按键控制三辆汽车
现在有三辆汽车,创建三个汽车任务。在红外中断中将键值写队列,汽车任务要来读队列。因为是三个任务,所以要写三个队列,在这里为了代码的可移植性,我们使用一个队列注册函数以及一个队列分发函数。
void RegisterQueueHandle(QueueHandle_t queueHandle) { if (g_queue_cnt < 10) { g_xQueues[g_queue_cnt] = queueHandle; g_queue_cnt++; } }
static void DispatchKey(struct ir_data *pidata) { int i; for (i = 0; i < g_queue_cnt; i++) { xQueueSendFromISR(g_xQueues[i], pidata, NULL); } }
在开始时每个任务调用任务函数时,创建自己的队列,并注册队列,然后循环读取队列,读不到便阻塞。当有红外中断写队列后,阻塞被唤醒。
系统架构

信号量和互斥量
队列用于代替缓冲区,在任务之间或任务与中断之间传递数据。但有时我们只需要传递一个状态,这个状态值需要用一个数值表示。
信号量本质上起始就是队列,只不过不进行具体数据的传输。
队列: 信号量:
写:send(①拷贝数据②cnt++③唤醒) 写:give(①cnt++②唤醒)
读:receive(①拷贝数据②cnt--③唤醒) 读:take(①cnt--)
信号量分为计数信号量与二值信号量
创建信号量
/* 创建一个二进制信号量,返回它的句柄。 * 此函数内部会分配信号量结构体 * 返回值: 返回句柄,非NULL表示成功 */ SemaphoreHandle_t xSemaphoreCreateBinary( void ); /* 创建一个二进制信号量,返回它的句柄。 * 此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针 * 返回值: 返回句柄,非NULL表示成功 */ SemaphoreHandle_t xSemaphoreCreateBinaryStatic( StaticSemaphore_t *pxSemaphoreBuffer );
/* 创建一个计数型信号量,返回它的句柄。 * 此函数内部会分配信号量结构体 * uxMaxCount: 最大计数值 * uxInitialCount: 初始计数值 * 返回值: 返回句柄,非NULL表示成功 */ SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount); /* 创建一个计数型信号量,返回它的句柄。 * 此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针 * uxMaxCount: 最大计数值 * uxInitialCount: 初始计数值 * pxSemaphoreBuffer: StaticSemaphore_t结构体指针 * 返回值: 返回句柄,非NULL表示成功 */ SemaphoreHandle_t xSemaphoreCreateCountingStatic( UBaseType_t uxMaxCount, UBaseType_t uxInitialCount, StaticSemaphore_t *pxSemaphoreBuffer );
删除信号量
/* * xSemaphore: 信号量句柄,你要删除哪个信号量 */ void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );
信号量的take与give操作
Give操作(任务和中断中)
BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore ); //任务中
BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken );
优先级反转问题
高优先级的任务被低优先级任务阻塞,导致高优先级任务迟迟得不到调度。其他中优先级的任务却能抢到CPU资源。从现象上看,好像中优先级的任务比高优先级的任务具有更高的优先级。

解决方法
使用互斥量,具有优先级继承
让低优先级的线程在获得同步资源的时候(若有高优先级的线程也需要使用该同步资源),那么会临时提升低优先级的线程的优先级与高优先级的线程优先级相同,以使其更快地执行并释放同步资源,释放同步资源后再恢复其原来的优先级。
互斥量是一种特殊的二进制信号量。它具有一般二进制信号量不具有的某些特性。
- 互斥量必须是同一个任务申请,同一个任务释放。(谁上锁谁开锁)二值信号量一个任务申请后,可以由另一个任务释放。
- 互斥量具有优先级继承的性质
创建互斥量
/* 创建一个互斥量,返回它的句柄。 * 此函数内部会分配互斥量结构体 * 返回值: 返回句柄,非NULL表示成功 */ SemaphoreHandle_t xSemaphoreCreateMutex( void ); /* 创建一个互斥量,返回它的句柄。 * 此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针 * 返回值: 返回句柄,非NULL表示成功 */ SemaphoreHandle_t xSemaphoreCreateMutexStatic( StaticSemaphore_t *pxMutexBuffer );
获取释放与删除
/* * xSemaphore: 信号量句柄,你要删除哪个信号量, 互斥量也是一种信号量 */ void vSemaphoreDelete( SemaphoreHandle_t xSemaphore ); /* 释放 */ BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore ); /* 获得 */ BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait );

浙公网安备 33010602011771号