FreeRTOS信号量
目录
- 1. 基本概念
- 2. 两种类型
- 2.1 计数型信号量 (Counting Semaphore)
- 2.2 二进制信号量 (Binary Semaphore)
- 3. 常规操作
- 3.1 Give 操作
- 3.2 Take 操作
- 4.和队列对比
- 5.函数
- 5.1 创建
- 5.1.1 动态创建
- 5.1.2 静态创建
- 5.2 删除
- 5.3 Give / Take 操作
- 5.3.1 Give 操作
- 5.3.2 Take 操作
- 6.示例
- 7.内部机制
- 7.1 创建
- 7.2 task
- 7.3 Give
- 疑问
- 疑问1
- 疑问2
1. 基本概念
信号量这个名字非常贴切,它包含两个方面的含义:
- 信号(Signal): 用于通知或提示某个事件的发生。
- 量(Count): 表示可用资源的数量或事件的累积次数。
因此,信号量不仅可以用来通知某个事件,还可以反映出系统中资源的剩余数量。
2. 两种类型
2.1 计数型信号量 (Counting Semaphore)
计数型信号量的计数值可以是任意正整数。 用于记录多个资源的数量或多个事件的累积次数。
- 事件累积: 每当有一个事件发生,就调用“give”操作(计数值加1);在处理事件时,先调用“take”操作(计数值减1),从而消费掉一个事件。
- 资源管理: 比如系统中有多个同类资源,任务在访问资源前“take”信号量(使计数值减1),访问结束后“give”信号量(计数值加1),这样可以确保同时只有有限个任务可以访问这些资源。
2.2 二进制信号量 (Binary Semaphore)
二进制信号量的计数值只有 0 和 1 两个取值。 它的功能类似于互斥量,但更多用于事件通知而非资源保护(虽然也可以用于简单的资源保护)。
- 二进制信号量是计数型信号量的一种特殊形式,只不过最大计数值被限定为 1。
- 适用于只关心事件是否发生(有或没有)的场合。
信号量的计数值都有限制:限定了最大值。如果最大值被限定为1,那么它就是二进制信号量;如果最大值不是1,它就是计数型信号量。
二进制信号量 | 技术型信号量 |
---|---|
被创建时初始值为0 | 被创建时初始值可以设定 |
其他操作是一样的 | 其他操作是一样的 |
3. 常规操作
3.1 Give 操作
给出资源/通知事件:当任务完成某项操作或产生某个事件后,调用“give”操作,信号量的计数值加 1。
在资源管理中,给出资源表示资源释放;在事件处理场景中,给出信号量表示一个事件已发生。
- 如果有任务因为信号量不足而处于阻塞状态,“give”操作会唤醒其中优先级最高的任务(如果多个任务的优先级相同,则唤醒等待时间最长的任务)。
3.2 Take 操作
获取资源/等待事件:在任务需要访问资源或等待事件时,调用“take”操作,将信号量的计数值减 1。
如果信号量计数值为 0(例如资源全部被占用或事件还未发生),任务可以选择进入阻塞状态等待,或者立即返回失败(取决于设置的超时时间)。
- 成功的“take”操作表示任务已经获得了资源或接收到事件通知,反之,则可能因为超时而返回错误代码。
信号量的"give"、"take"双方并不需要相同,可以用于生产者-消费者场合:
- 生产者为任务A、B,消费者为任务C、D
- 一开始信号量的计数值为0,如果任务C、D想获得信号量,会有两种结果:
- 阻塞:买不到东西咱就等等吧,可以定个闹钟(超时时间)
- 即刻返回失败:不等
- 任务A、B可以生产资源,就是让信号量的计数值增加1,并且把等待这个资源的顾客唤醒
- 唤醒谁?谁优先级高就唤醒谁,如果大家优先级一样就唤醒等待时间最长的人
信号量起到了桥梁作用:
- 生产者任务:负责产生资源或事件,调用“give”操作,使信号量计数值增加。
- 消费者任务:等待资源或事件,调用“take”操作,获取资源后进行处理。
4.和队列对比
队列 | 信号量 |
---|---|
可以容纳多个数据,创建队列时有2部分内存: 队列结构体、存储数据的空间 | 只有计数值,无法容纳其他数据。创建信号量时,只需要分配信号量结构体 |
生产者:没有空间存入数据时可以阻塞 | 生产者:用于不阻塞,计数值已经达到最大时返回失败 |
消费者:没有数据时可以阻塞 | 消费者:没有资源时可以阻塞 |
5.函数
- 创建与删除:
-
- 动态创建函数:
xSemaphoreCreateBinary
、xSemaphoreCreateCounting
; - 静态创建函数:
xSemaphoreCreateBinaryStatic
、xSemaphoreCreateCountingStatic
; - 删除函数:
vSemaphoreDelete
用于释放动态分配的内存。
- 动态创建函数:
- Give / Take 操作:
-
- 任务中使用:
xSemaphoreGive
和xSemaphoreTake
; - ISR 中使用:
xSemaphoreGiveFromISR
和xSemaphoreTakeFromISR
。 - Give 操作增加信号量计数,Take 操作减少信号量计数。
- 对于二进制信号量,其计数值只允许 0 和 1;计数型信号量则允许更大范围的计数,适用于资源计数和事件累积。
- 任务中使用:
- 使用场景:
-
- 事件通知: 当某个事件发生时,任务调用“give”通知另一个任务,通过“take”等待并接收事件。
- 资源管理: 当任务需要访问有限资源时,先调用“take”获得访问权限,用完后调用“give”释放资源。
5.1 创建
在使用信号量之前,必须先创建信号量,并获得其句柄。信号量可以通过动态创建或静态创建两种方式来获得。
5.1.1 动态创建
动态创建时,函数内部会自动分配所需的内存。常用的动态创建函数包括:
创建二进制信号量:
SemaphoreHandle_t xSemaphoreCreateBinary(void);
-
- 创建一个二进制信号量,初始计数值通常为 0(表示资源不可用,必须先通过“give”才能使用)。
- 返回一个信号量句柄,非 NULL 表示创建成功。
创建计数型信号量:
SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount);
-
- 创建一个计数型信号量,其中:
-
-
uxMaxCount
为最大计数值(资源或事件的最大累积数)。uxInitialCount
为初始计数值。
-
-
- 返回句柄,非 NULL 表示成功。
想要使用该函数需要配置**configUSE_COUNTING_SEMAPHORES**
为1,至于**configSUPPORT DYNAMIC_ALLOCATION**
并不需要我们去手动定义,配置文件中会把它配置为1,如果不需要动态分配内存的话需要自己把它定义为0(在FreeRTOSConfig.h中去定义宏)
曾经有个函数 vSemaphoreCreateBinary
(已过时),它创建的二进制信号量初始值为 1,但现在推荐使用 xSemaphoreCreateBinary
。
5.1.2 静态创建
如果不希望使用动态内存分配,可以采用静态创建方式。需要提前提供一个 StaticSemaphore_t
类型的结构体内存块。
静态创建二进制信号量:
SemaphoreHandle_t xSemaphoreCreateBinaryStatic(StaticSemaphore_t *pxSemaphoreBuffer);
-
- 使用用户提供的内存来创建二进制信号量,避免动态分配。
- 返回句柄,非 NULL 表示创建成功。
静态创建计数型信号量:
SemaphoreHandle_t xSemaphoreCreateCountingStatic(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount, StaticSemaphore_t *pxSemaphoreBuffer);
-
- 同样使用用户提供的内存来创建计数型信号量。
- 参数
uxMaxCount
和uxInitialCount
分别指定最大计数值和初始计数值。 - 返回句柄,非 NULL 表示成功。
5.2 删除
对于动态创建的信号量,在不再需要时应调用删除函数释放内存。
void vSemaphoreDelete(SemaphoreHandle_t xSemaphore);
-
- 删除指定的信号量,释放由动态分配内存创建的信号量所占用的内存。
- 静态创建的信号量内存由用户管理,不由此函数释放。
5.3 Give / Take 操作
“give”(释放或增加资源)和“take”(获取或消耗资源)
5.3.1 Give 操作
任务上下文中的 Give:
BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);
- 给出信号量,令其计数值增加 1。
- 如果信号量已有最大值(例如二进制信号量已经为 1,或计数型信号量达到最大值),则操作会失败并返回错误。
- 如果释放信号量后导致等待该信号量的任务中有更高优先级者进入就绪态,则系统会通知调度器进行上下文切换。
ISR 中的 Give:
BaseType_t xSemaphoreGiveFromISR(SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken);
- 在中断服务例程中给出信号量。
- 参数
pxHigherPriorityTaskWoken
指向一个变量,用于标识释放信号量是否使得有更高优先级的任务被唤醒(如果是,通常需要在 ISR 结束后进行上下文切换)。 - 返回值 pdTRUE 表示操作成功。
5.3.2 Take 操作
任务上下文中的 Take:
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait);
-
从信号量中获取资源,使计数值减 1。
-
如果信号量当前不可用(计数值为 0),任务可以选择阻塞等待,等待时间由
xTicksToWait
指定: -
xTicksToWait == 0
表示不阻塞,立即返回;xTicksToWait == portMAX_DELAY
表示无限等待,直到成功获得信号量;- 其他值表示阻塞指定的 Tick 数量(可以通过
pdMS_TO_TICKS()
将毫秒转换为 Tick 数)。
-
成功返回 pdTRUE,否则返回错误值。
ISR 中的 Take:
BaseType_t xSemaphoreTakeFromISR(SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken);
- 在 ISR 中尝试获取信号量,不会阻塞。
- 同样,
pxHigherPriorityTaskWoken
用于标识是否有更高优先级的任务因获取信号量而进入就绪态。 - 返回 pdTRUE 表示成功,否则表示失败(例如信号量不可用)。
6.示例
// 全局变量:用于任务间共享的数据和状态标志
static int sum = 0; // 全局变量,用于保存计算结果
static volatile int flagCalcEnd = 0; // 标志计算是否结束(示例中用于调试或状态标识)
static volatile int flagUARTused = 0; // 标志UART是否正在使用(本例中未具体使用)// 信号量句柄,用于任务同步与互斥
static SemaphoreHandle_t xSemCalc; // 计数型信号量,用于通知计算任务完成(Task1 -> Task2)
static SemaphoreHandle_t xSemUART; // 二进制信号量,用于控制对UART资源的互斥访问/*-----------------------------------------------------------* Task1Function:* 该任务负责执行大量的计算,将结果存储到全局变量sum中。* 当计算完成后,通过调用xSemaphoreGive(xSemCalc)通知其他任务(如Task2)* 计算已结束,然后删除自己,释放系统资源。*/
void Task1Function(void * param)
{volatile int i = 0; // 局部变量,用于计数(使用volatile防止编译器优化)while (1){// 模拟耗时计算:累加数值到全局变量sumfor (i = 0; i < 10000000; i++)sum++; // 累加计算// 计算完成后,通过信号量通知其他任务// xSemCalc是一个计数型信号量,初始值为0,// 此处调用xSemaphoreGive将其计数值加1,表示计算完成xSemaphoreGive(xSemCalc);// 任务完成后自我删除,释放资源vTaskDelete(NULL);}
}/*-----------------------------------------------------------* Task2Function:* 该任务负责等待来自Task1的计算完成通知,* 一旦获得通知后就打印全局变量sum的值。* 通过调用xSemaphoreTake阻塞等待xSemCalc信号量,* 当Task1完成计算并调用xSemaphoreGive后,* xSemaphoreTake会成功返回,然后Task2打印计算结果。*/
void Task2Function(void * param)
{while (1){// 清除状态标志(这里flagCalcEnd仅作示例,实际用途可根据需求定义)flagCalcEnd = 0;// 阻塞等待计算完成的信号量,// portMAX_DELAY表示任务会一直阻塞直到成功获得信号量xSemaphoreTake(xSemCalc, portMAX_DELAY);// 当成功获得信号量后,表示Task1已完成计算flagCalcEnd = 1;// 打印计算结果printf("sum = %d\r\n", sum);// 如果需要循环处理,可以继续等待下一个信号(但本例中Task1只运行一次)}
}/*-----------------------------------------------------------* TaskGenericFunction:* 该任务用于演示UART资源的互斥访问。* 任务在打印消息前,通过xSemaphoreTake获得UART信号量,* 确保在同一时刻只有一个任务可以使用UART输出,* 打印结束后通过xSemaphoreGive释放信号量,再延时一段时间。** 注意:本例中该任务被注释掉了,如有需要可以启用任务3和任务4*/
void TaskGenericFunction(void * param)
{while (1){// 获取UART信号量(二进制信号量),确保独占访问UARTxSemaphoreTake(xSemUART, portMAX_DELAY);// 打印任务参数中传入的字符串,显示当前任务正在使用UARTprintf("%s\r\n", (char *)param);// 释放UART信号量,使其他任务能够访问UARTxSemaphoreGive(xSemUART);// 延时1个Tick,避免任务频繁占用CPUvTaskDelay(1);}
}/*-----------------------------------------------------------* main函数:系统入口* 进行硬件初始化、信号量创建、任务创建,并启动调度器。*/
int main( void )
{TaskHandle_t xHandleTask1; // 用于保存Task1的任务句柄#ifdef DEBUGdebug();
#endif// 硬件初始化函数(根据具体平台实现)prvSetupHardware();printf("Hello, world!\r\n");// 创建计数型信号量xSemCalc:// 计数型信号量用于同步任务间的计算完成通知// 此处最大计数值为10,初始计数值为0,表示没有计算完成的事件xSemCalc = xSemaphoreCreateCounting(10, 0);// 创建二进制信号量xSemUART:// 用于互斥访问UART资源,二进制信号量的计数值只有0和1xSemUART = xSemaphoreCreateBinary();// 初始时通过调用xSemaphoreGive释放一次信号量,表示UART资源空闲可用xSemaphoreGive(xSemUART);// 创建Task1:计算任务xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1);// 创建Task2:等待并打印计算结果的任务xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL);// 以下任务用于演示UART互斥(本例中暂时注释掉,如有需要可启用)// xTaskCreate(TaskGenericFunction, "Task3", 100, "Task 3 is running", 1, NULL);// xTaskCreate(TaskGenericFunction, "Task4", 100, "Task 4 is running", 1, NULL);// 启动调度器,开始任务调度vTaskStartScheduler();// 如果调度器启动失败(如堆内存不足),程序将运行到此处return 0;
}
其中对于task1和task2利用信号量来演示一个同步功能,这部分的代码如果删除掉Task1Function
函数中的 vTaskDelete(NULL);
会发现打印的sum肯定不是1w,这部分可以尝试自己去修改实现。
而对于信号量实现的同步,不同的任务之间是无法传输数据的,它并不像队列那样,可以将task1的要传输的数据放进队列,task2再去读出,实现数据的传输。
要牢记信号量的设计初衷是用于同步和资源访问控制,而不是数据传输。它只是一个计数器,用来表示资源的可用性或者事件发生的次数,其内部只保存一个简单的计数值,并不提供存储数据的缓冲区。
7.内部机制
其实内部机制的实现其实和队列几乎是差不多的,无论是创建,还是读或者写。
信号量就是一种特殊的队列,只不过不用来传输数据,而是用来计数而已
7.1 创建
以xSemaphoreCreateBinary
为例进行分析:
#if ( configSUPPORT_DYNAMIC_ALLOCATION == 1 )#define xSemaphoreCreateBinary() xQueueGenericCreate( ( UBaseType_t ) 1, semSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_BINARY_SEMAPHORE )
#endif
诶?可以看到实际上是调用的是xQueueGenericCreate
,创建队列的函数,区别在于传入的参数:
- 参1:队列的长度为1
- 参2:一个宏定义,队列数据项的大小为0
- 参3:队列类型
其实创建出来的就只有Queue_t,而用于存储的队列buff实际上是没有的,因为参1*参2实际上会等于0:
7.2 task
以xSemaphoreTake
为例子:
#define xSemaphoreTake( xSemaphore, xBlockTime ) xQueueSemaphoreTake( ( xSemaphore ), ( xBlockTime ) )
BaseType_t xQueueSemaphoreTake( QueueHandle_t xQueue,TickType_t xTicksToWait )
{BaseType_t xEntryTimeSet = pdFALSE; /* 标记是否已初始化超时状态 */TimeOut_t xTimeOut; /* 用于记录进入阻塞前的时间状态 */Queue_t * const pxQueue = xQueue; /* 将通用队列句柄转换为内部队列结构指针 */#if ( configUSE_MUTEXES == 1 )BaseType_t xInheritanceOccurred = pdFALSE; /* 如果是互斥量,则记录是否发生了优先级继承 */#endif/* 检查队列指针是否为空 */configASSERT( ( pxQueue ) );/* 检查此队列确实是一个信号量。对于信号量,uxItemSize 必须为 0 */configASSERT( pxQueue->uxItemSize == 0 );/* 如果调度器处于挂起状态,则不允许阻塞等待 */#if ( ( INCLUDE_xTaskGetSchedulerState == 1 ) || ( configUSE_TIMERS == 1 ) ){configASSERT( !( ( xTaskGetSchedulerState() == taskSCHEDULER_SUSPENDED ) && ( xTicksToWait != 0 ) ) );}#endif/* 循环尝试获取信号量,直到成功或超时返回 */for( ; ; ){taskENTER_CRITICAL(); /* 进入临界区,防止并发访问共享数据 */{/* 对于信号量,实际存储的“消息数”就是信号量的计数值 */const UBaseType_t uxSemaphoreCount = pxQueue->uxMessagesWaiting;/* 检查信号量是否可用,即计数值是否大于 0 */if( uxSemaphoreCount > ( UBaseType_t ) 0 ){/* 跟踪接收事件 */traceQUEUE_RECEIVE( pxQueue );/* 信号量被“获取”后,减少计数值 */pxQueue->uxMessagesWaiting = uxSemaphoreCount - ( UBaseType_t ) 1;#if ( configUSE_MUTEXES == 1 ){/* 如果该信号量是互斥量,则需要记录优先级继承信息 */if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX ){/* 记录当前任务持有该互斥量,便于后续执行优先级继承 */pxQueue->u.xSemaphore.xMutexHolder = pvTaskIncrementMutexHeldCount();}else{mtCOVERAGE_TEST_MARKER();}}#endif /* configUSE_MUTEXES *//* 检查是否有任务正在等待“给”信号量(即等待增加信号量计数) */if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE ){/* 如果有等待任务,则移除等待列表中的最高优先级任务并唤醒它 */if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE ){queueYIELD_IF_USING_PREEMPTION();}else{mtCOVERAGE_TEST_MARKER();}}else{mtCOVERAGE_TEST_MARKER();}taskEXIT_CRITICAL(); /* 退出临界区 */return pdPASS; /* 成功获取信号量,返回 pdPASS */}else{/* 如果信号量计数为 0,即当前不可用 */if( xTicksToWait == ( TickType_t ) 0 ){/* 如果不允许阻塞等待,则直接返回失败 */#if ( configUSE_MUTEXES == 1 ){/* 对于互斥量,若发生了继承,则xInheritanceOccurred必须为 pdFALSE */configASSERT( xInheritanceOccurred == pdFALSE );}#endif /* configUSE_MUTEXES */taskEXIT_CRITICAL();traceQUEUE_RECEIVE_FAILED( pxQueue );return errQUEUE_EMPTY;}else if( xEntryTimeSet == pdFALSE ){/* 第一次进入等待状态,初始化超时状态结构体 */vTaskInternalSetTimeOutState( &xTimeOut );xEntryTimeSet = pdTRUE;}else{/* 已经设置了超时状态,继续等待 */mtCOVERAGE_TEST_MARKER();}}}taskEXIT_CRITICAL();/* 退出临界区后,允许中断和其他任务操作信号量 *//* 暂停任务调度,确保以下对队列的锁定操作原子执行 */vTaskSuspendAll();prvLockQueue( pxQueue );/* 检查超时状态,看是否已超时 */if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE ){/* 超时尚未到达 */if( prvIsQueueEmpty( pxQueue ) != pdFALSE ){/* 信号量仍不可用,进入阻塞状态等待信号量释放 */traceBLOCKING_ON_QUEUE_RECEIVE( pxQueue );#if ( configUSE_MUTEXES == 1 ){/* 如果该信号量是互斥量,则执行优先级继承操作 */if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX ){taskENTER_CRITICAL();{xInheritanceOccurred = xTaskPriorityInherit( pxQueue->u.xSemaphore.xMutexHolder );}taskEXIT_CRITICAL();}else{mtCOVERAGE_TEST_MARKER();}}#endif /* configUSE_MUTEXES *//* 将当前任务放入等待接收(获取信号量)的事件列表,并指定等待时间 */vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToReceive ), xTicksToWait );prvUnlockQueue( pxQueue ); /* 解锁队列 *//* 恢复任务调度,如果有更高优先级任务需要运行,则进行上下文切换 */if( xTaskResumeAll() == pdFALSE ){portYIELD_WITHIN_API();}else{mtCOVERAGE_TEST_MARKER();}}else{/* 信号量状态发生变化(不为空),重新解锁并再次尝试获取 */prvUnlockQueue( pxQueue );( void ) xTaskResumeAll();}}else{/* 如果等待超时 */prvUnlockQueue( pxQueue );( void ) xTaskResumeAll();/* 如果此时信号量仍不可用,则返回超时错误 */if( prvIsQueueEmpty( pxQueue ) != pdFALSE ){#if ( configUSE_MUTEXES == 1 ){/* 对于互斥量,如果曾经发生过优先级继承,* 则需要执行优先级回退(撤销继承) */if( xInheritanceOccurred != pdFALSE ){taskENTER_CRITICAL();{UBaseType_t uxHighestWaitingPriority;/* 获取等待该互斥量的任务中最高的优先级,* 并据此调整当前任务的优先级 */uxHighestWaitingPriority = prvGetDisinheritPriorityAfterTimeout( pxQueue );vTaskPriorityDisinheritAfterTimeout( pxQueue->u.xSemaphore.xMutexHolder, uxHighestWaitingPriority );}taskEXIT_CRITICAL();}}#endif /* configUSE_MUTEXES */traceQUEUE_RECEIVE_FAILED( pxQueue );return errQUEUE_EMPTY;}else{mtCOVERAGE_TEST_MARKER();}}} /* 循环结束,最终会返回成功或超时失败 */
}
/*-----------------------------------------------------------*/
实现几乎是和队列的差不多,也就是关中断,–操作,开中断
有个特别的地方就是: pxQueue->uxMessagesWaiting
,Queue_t结构体中的uxMessagesWaiting
成员被用来当作存放信号量的值
7.3 Give
以xSemaphoreGive
为例,这里就不啰嗦了,直接给出注释:
#define xSemaphoreGive( xSemaphore ) xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), NULL, semGIVE_BLOCK_TIME, queueSEND_TO_BACK )
BaseType_t xQueueGenericSend( QueueHandle_t xQueue,const void * const pvItemToQueue,TickType_t xTicksToWait,const BaseType_t xCopyPosition )
{/* 定义变量:* xEntryTimeSet: 标记是否已初始化超时计时(第一次进入阻塞等待时设置)* xYieldRequired: 标记是否需要立即上下文切换* xTimeOut: 用于记录超时状态的数据结构* pxQueue: 将通用队列句柄转换为内部队列结构指针*/BaseType_t xEntryTimeSet = pdFALSE, xYieldRequired;TimeOut_t xTimeOut;Queue_t * const pxQueue = xQueue;/* 断言:检查队列指针是否有效 */configASSERT( pxQueue );/* 断言:如果队列项大小不为0,则发送数据的指针不能为NULL */configASSERT( !( ( pvItemToQueue == NULL ) && ( pxQueue->uxItemSize != ( UBaseType_t ) 0U ) ) );/* 断言:当使用覆盖发送(queueOVERWRITE)时,队列长度必须为1 */configASSERT( !( ( xCopyPosition == queueOVERWRITE ) && ( pxQueue->uxLength != 1 ) ) );#if ( ( INCLUDE_xTaskGetSchedulerState == 1 ) || ( configUSE_TIMERS == 1 ) ){/* 断言:当调度器被挂起时,不允许阻塞等待 */configASSERT( !( ( xTaskGetSchedulerState() == taskSCHEDULER_SUSPENDED ) && ( xTicksToWait != 0 ) ) );}#endif/* 函数通过循环不断尝试发送数据,直到成功或者等待超时 */for( ; ; ){/* 进入临界区,防止多个任务同时访问队列数据结构 */taskENTER_CRITICAL();{/* 检查队列是否有空闲空间:* 当队列中的消息数小于队列容量,或者处于覆盖模式时(此时队列满也允许写入)*/if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) || ( xCopyPosition == queueOVERWRITE ) ){/* 跟踪发送操作(用于调试或性能统计) */traceQUEUE_SEND( pxQueue );#if ( configUSE_QUEUE_SETS == 1 ){/* 对于属于队列集合的队列,先保存之前的消息数 */const UBaseType_t uxPreviousMessagesWaiting = pxQueue->uxMessagesWaiting;/* 将数据复制到队列中,并返回是否需要触发上下文切换 */xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );/* 如果队列是队列集合的一部分 */if( pxQueue->pxQueueSetContainer != NULL ){/* 如果是覆盖模式且队列中原本已有数据,则不通知队列集合,* 因为此时队列中元素总数没有改变 */if( ( xCopyPosition == queueOVERWRITE ) && ( uxPreviousMessagesWaiting != ( UBaseType_t ) 0 ) ){mtCOVERAGE_TEST_MARKER();}/* 否则,尝试通知队列集合,如果通知成功并解除了高优先级任务,* 则触发上下文切换 */else if( prvNotifyQueueSetContainer( pxQueue ) != pdFALSE ){queueYIELD_IF_USING_PREEMPTION();}else{mtCOVERAGE_TEST_MARKER();}}else{/* 非队列集合:检查是否有任务在等待接收数据 */if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE ){/* 如果有等待接收数据的任务,则将其从等待列表中移除,并唤醒 */if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE ){/* 被唤醒的任务优先级更高,则进行任务切换 */queueYIELD_IF_USING_PREEMPTION();}else{mtCOVERAGE_TEST_MARKER();}}/* 特殊情况:如果 prvCopyDataToQueue 返回需要任务切换标志,* 也进行任务切换 */else if( xYieldRequired != pdFALSE ){queueYIELD_IF_USING_PREEMPTION();}else{mtCOVERAGE_TEST_MARKER();}}}#else /* configUSE_QUEUE_SETS 未启用 */{/* 直接将数据复制到队列中 */xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );/* 检查是否有任务在等待接收数据,如果有则唤醒等待任务 */if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE ){if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE ){/* 唤醒的任务优先级更高,触发上下文切换 */queueYIELD_IF_USING_PREEMPTION();}else{mtCOVERAGE_TEST_MARKER();}}/* 同样,如果 prvCopyDataToQueue 表示需要任务切换,也进行切换 */else if( xYieldRequired != pdFALSE ){queueYIELD_IF_USING_PREEMPTION();}else{mtCOVERAGE_TEST_MARKER();}}#endif /* configUSE_QUEUE_SETS *//* 退出临界区 */taskEXIT_CRITICAL();/* 数据发送成功,返回 pdPASS */return pdPASS;}else{/* 当队列满时的处理 */if( xTicksToWait == ( TickType_t ) 0 ){/* 如果不允许等待,则退出并返回队列已满错误 */taskEXIT_CRITICAL();traceQUEUE_SEND_FAILED( pxQueue );return errQUEUE_FULL;}else if( xEntryTimeSet == pdFALSE ){/* 第一次检测到队列满时,初始化超时计时结构体 */vTaskInternalSetTimeOutState( &xTimeOut );xEntryTimeSet = pdTRUE;}else{/* 如果已经设置了超时计时,则继续等待 */mtCOVERAGE_TEST_MARKER();}}}/* 退出临界区 */taskEXIT_CRITICAL();/* 退出临界区后,允许其他中断和任务对队列进行操作 *//* 暂停任务调度,保证后续的锁定操作的原子性 */vTaskSuspendAll();/* 锁定队列,确保在检查队列状态和操作等待列表时不会被其他任务中断 */prvLockQueue( pxQueue );/* 检查超时状态,更新等待时间 */if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE ){/* 超时未到:如果队列仍然满,则将当前任务放入等待发送数据的事件列表 */if( prvIsQueueFull( pxQueue ) != pdFALSE ){traceBLOCKING_ON_QUEUE_SEND( pxQueue );vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToSend ), xTicksToWait );/* 解锁队列,让其他事件能操作等待列表。* 注意:此时中断可能已将该任务从等待列表中移除,* 如果任务已准备就绪,则它可能已进入挂起就绪列表。 */prvUnlockQueue( pxQueue );/* 恢复任务调度,如果 xTaskResumeAll 返回 pdFALSE,则进行任务切换 */if( xTaskResumeAll() == pdFALSE ){portYIELD_WITHIN_API();}}else{/* 如果队列状态改变(不满),则解锁队列并恢复调度,* 然后再次尝试发送数据 */prvUnlockQueue( pxQueue );( void ) xTaskResumeAll();}}else{/* 如果等待超时 */prvUnlockQueue( pxQueue );( void ) xTaskResumeAll();traceQUEUE_SEND_FAILED( pxQueue );/* 返回错误码,表示队列仍然满且超时 */return errQUEUE_FULL;}} /* 循环结束,理论上会在成功发送数据或超时返回错误之前退出 */
}
和队列操作的几乎是一模一样的,关键是在于其中的prvCopyDataToQueue
函数:
static BaseType_t prvCopyDataToQueue( Queue_t * const pxQueue,const void * pvItemToQueue,const BaseType_t xPosition )
{BaseType_t xReturn = pdFALSE;UBaseType_t uxMessagesWaiting;/* This function is called from a critical section. */uxMessagesWaiting = pxQueue->uxMessagesWaiting;if( pxQueue->uxItemSize == ( UBaseType_t ) 0 ){#if ( configUSE_MUTEXES == 1 ){if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX ){/* The mutex is no longer being held. */xReturn = xTaskPriorityDisinherit( pxQueue->u.xSemaphore.xMutexHolder );pxQueue->u.xSemaphore.xMutexHolder = NULL;}else{mtCOVERAGE_TEST_MARKER();}}#endif /* configUSE_MUTEXES */}else if( xPosition == queueSEND_TO_BACK ){( void ) memcpy( ( void * ) pxQueue->pcWriteTo, pvItemToQueue, ( size_t ) pxQueue->uxItemSize ); /*lint !e961 !e418 !e9087 MISRA exception as the casts are only redundant for some ports, plus previous logic ensures a null pointer can only be passed to memcpy() if the copy size is 0. Cast to void required by function signature and safe as no alignment requirement and copy length specified in bytes. */pxQueue->pcWriteTo += pxQueue->uxItemSize; /*lint !e9016 Pointer arithmetic on char types ok, especially in this use case where it is the clearest way of conveying intent. */if( pxQueue->pcWriteTo >= pxQueue->u.xQueue.pcTail ) /*lint !e946 MISRA exception justified as comparison of pointers is the cleanest solution. */{pxQueue->pcWriteTo = pxQueue->pcHead;}else{mtCOVERAGE_TEST_MARKER();}}else{( void ) memcpy( ( void * ) pxQueue->u.xQueue.pcReadFrom, pvItemToQueue, ( size_t ) pxQueue->uxItemSize ); /*lint !e961 !e9087 !e418 MISRA exception as the casts are only redundant for some ports. Cast to void required by function signature and safe as no alignment requirement and copy length specified in bytes. Assert checks null pointer only used when length is 0. */pxQueue->u.xQueue.pcReadFrom -= pxQueue->uxItemSize;if( pxQueue->u.xQueue.pcReadFrom < pxQueue->pcHead ) /*lint !e946 MISRA exception justified as comparison of pointers is the cleanest solution. */{pxQueue->u.xQueue.pcReadFrom = ( pxQueue->u.xQueue.pcTail - pxQueue->uxItemSize );}else{mtCOVERAGE_TEST_MARKER();}if( xPosition == queueOVERWRITE ){if( uxMessagesWaiting > ( UBaseType_t ) 0 ){/* An item is not being added but overwritten, so subtract* one from the recorded number of items in the queue so when* one is added again below the number of recorded items remains* correct. */--uxMessagesWaiting;}else{mtCOVERAGE_TEST_MARKER();}}else{mtCOVERAGE_TEST_MARKER();}}pxQueue->uxMessagesWaiting = uxMessagesWaiting + ( UBaseType_t ) 1;return xReturn;
}
/*-----------------------------------------------------------*/
看最后一行pxQueue->uxMessagesWaiting = uxMessagesWaiting + ( UBaseType_t ) 1;
,就是实现信号值+1的操作
疑问
疑问1
有多个任务执行xSemaphoreTake,那么当其他任务执行xSemaphoreGive 时,唤醒哪个任务?
- 优先级最高的任务
- 优先级相同时,等待最久的任务
疑问2
使用队列也可以实现同步,为什么还要使用信号量呢?
- 使用队列可以传递数据,数据的保存需要空间
- 使用信号量时不需要传递数据,更节省空间
- 使用信号量时不需要复制数据,效率更高