信号量和互斥量都是基于队列的基本数据结构实现的 。虽然它们都派生自队列,但它们在应用场景和机制上存在区别。二值信号量容易出现优先级翻转问题,而互斥量没有这个问题 。
二值信号量适用于进程间同步,计数信号量适用于多个共享资源的访问控制,互斥量适用于对一个资源的互斥访问控制。
二值信号量就是只有一个项的队列,这个队列要么是空要么是满,相当于只有0和1两种值 。二值信号量就像是一个标志,非常适合用于进程间同步的通信 。
使用二值信号量可以直接使任务进入阻塞等待状态,当信号量有效时立刻就绪;而不是一般的状态位那样不断轮询查询状态位的值。一次使用二值信号量进行同步的效率更高。
在数据采集中,ADC中断ISR读取转换结果并写入缓冲区,随后释放(Give)二值信号量 。数据处理任务总是去尝试获取(Take)二值信号量;信号量无效时进入阻塞状态等待,一旦有效则立刻进入就绪状态参与任务调度,进而读取缓冲区数据进行处理 。
计数型信号量是有固定长度的队列,每个项都是一个标志 。它通常用于对多个共享资源的访问进行控制 。
创建一个计数型信号量时设置的初值代表可用的共享资源数量 。当任务申请资源(获取信号量)时,计数值减1;当计数值为0时,后续任务需要等待 。当任务释放资源(释放信号量)时,计数值加1,表示可用资源数量增加 。
互斥量不能在 ISR 中使用。因为互斥量具有优先级继承机制,而 ISR 不是任务。
互斥量是对二值信号量的一种改进,它引入了优先级继承机制,可以减缓(但不能完全消除)优先级翻转问题,从而提升系统的实时性 。
和二值信号量不同的是,同一个任务对互斥量既有获取操作,也有释放操作,而二值信号量通常一个进程只能进行其中一种操作。
假设我们有三个不同的任务,分别为低中高优先级(L、M、H)。
最开始,L 运行,并占据了一个资源。
一段时间后,M 运行,L 被阻塞,但是此时资源依然在 L 手中。
接着,H 运行,H 想要访问资源,但是资源此时在 L 手里,而 L 又被 M 阻塞无法释放资源。这就造成了 H 的运行完全受制于 M。 高优先级的运行时间完全受制于中优先级任务,这就是“优先级翻转”。
当 H 发现自己需要的 Mutex 被 L 拿着时,系统会临时把 L 的优先级提升到和 H 一样高。
互斥量相当于管理互斥型共享资源(如串口)的一把钥匙 。一个任务获得互斥量后将独占对该资源的访问,访问完成前其他想要获取该互斥量的任务只能等待 。
递归互斥量同样不能在 ISR 中使用。
此外,不建议使用递归互斥量;当你的程序不得不使用递归互斥量的时候,你应该检查一下自己的系统设计是否有问题。
递归互斥量是一种特殊的互斥量,适用于需要递归调用的函数中 。与普通互斥量不同,任务在获得递归互斥量之后,还可以再次获得它,但必须保证每次获取都与一次释放配对使用 。
想象你有一个串口打印任务,这个任务在运行时会获取串口的互斥量。
现在,这个串口需要将一个复杂结构体打印出来,所以它调用了另一个专门做这件事的函数。
这个函数为了安全,也会尝试获取串口的互斥量。但是它失败了。
如果你没有做超时等待判断的话,这会造成自己锁死自己的情况:任务在等待函数执行完,函数也在等待任务释放互斥量。就算你做了超时等待,这也会造成串口打印失败。
而递归互斥量除了普通互斥量的功能外,还会额外记录是谁获取了该互斥量以及计数:当发现在互斥量已经被一个任务获取的情况下,该任务再次尝试获取时会将计数加一。只有在计数为 0 时,其它任务才能获取该互斥量。
这有点像我们说过的临界区控制。
xSemaphoreCreateBinary()该函数的静态版本为xSemaphoreCreateBinaryStatic()。后面几个函数(除了删除函数)都有对应的静态版本,不再赘述。
#define xSemaphoreCreateBinary() xQueueGenericCreate( ( UBaseType_t ) 1, semSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_BINARY_SEMAPHORE )可以看到二值信号量实际上是一个长度为 1,数据单元大小为 0 的队列。创建完成后,该信号量初始为空。
有一个叫
vSemaphoreCreateBinary的函数也是创建二值信号量,不过它创建的信号量初始为满。不建议使用这个函数,因为它已经被标记为过时(deprecated)。
如果只是单纯地想实现“一个 ISR 唤醒一个特定的 Task”这种单对单的同步, 建议使用 任务通知 (Task Notifications)。
xSemaphoreCreateCounting()略。
xSemaphoreCreateMutex()略。
xSemaphoreCreateRecursiveMutex()略。
vSemaphoreDelete()使用该函数可以删除以上这4种信号量或互斥量 。
xSemaphoreGive()可用于释放二值信号量、计数型信号量或互斥量 ,该函数实际上是xQueueGenericSend。
对二值信号量进行多次连续地释放时,只有第一次有效,后几次会丢失。
xSemaphoreGiveFromISR()用于中断中,但不能用于互斥量。
该函数会通过参数指针返回是否有高优先级任务(指优先级比进入中断时的任务高)因为此次释放被解锁,如果返回pdTRUE,就需要调用 portYIELD_FROM_ISR来申请调度以及时切换任务(因为推出 ISR 后不会自动进行任务切换,如果不这么做,高优先级任务就得等到下一次时间片调度才会被执行)。
void Ethernet_RX_ISR( void ) {
// 1. 定义一个局部变量,初始化为 pdFALSE
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// 2. 清除硬件的中断标志位(省略具体硬件代码)
Clear_Hardware_Interrupt_Flag();
// 3. 释放信号量,并把刚才定义的变量的地址传进去
xSemaphoreGiveFromISR( xBinarySemaphore, &xHigherPriorityTaskWoken );
// 4. 检查变量是否被底层的 Give 函数改写成了 pdTRUE?
// 如果是,说明唤醒了高优先级任务,建议立刻进行任务切换
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
// (注:在某些移植架构如 ARM Cortex-M 中,宏名可能叫 portEND_SWITCHING_ISR)
}xSemaphoreGiveRecursive()退出一次当前递归互斥量的嵌套,如果所有嵌套都已退出,则释放该互斥量。
xSemaphoreTake()可用于获取二值信号量、计数型信号量或互斥量 。
xSemaphoreTakeFromISR()用于中断中,但同样不能用于互斥量 。
该函数也会通过参数指针返回是否需要在 ISR 中进行任务调度。
xSemaphoreTakeRecursive()尝试获取一个递归互斥量。 如果这个互斥量已经被当前任务获取过,它可以再次成功获取,而不会导致自己被阻塞。
使用 uxSemaphoreGetCount(xSemaphore) 可以返回计数型信号量或二值信号量当前的值。对于二值信号量,返回1代表有效,0代表无效;对于计数信号量,返回的是当前剩余可用资源的个数 。
使用 xSemaphoreGetMutexHolder() 可以返回互斥量当前的持有者(holder),用于确定当前任务是否是某个互斥量的持有者。该函数也有对应的 FromISR 版本 。
可以配合
xTaskGetCurrentTaskHandle查看当前正在执行的任务,从而自己实现递归互斥量。