Semaphore-and-Mutex
2025-09-10 20:47:26

数据的同步机制

数据的同步是指,如何能通知任务新数据的到来并同时提高CPU的利用率。
假设一个简单的生产者消费者模型–有两个任务,一个任务是数据的生产者(Producer)任务,一个任务是数据的消费者(Consumer)任务,消费者任务等待生产者任务产生的数据并进行处理。按照正常的思路,消费者任务和生产者任务会轮流执行,但是如果在消费者任务执行的时候数据还没有产生的话,消费者任务的运行就是无用的,会降低CPU的使用效率。
为此,FreeRTOS引入了信号量(Semaphore)概念,通过信号量的同步机制可以使消费者任务在数据还没到达的时候进入阻塞状态,并让出CPU资源给其他任务。信号量是一种同步机制,可以起到消息通知和提高CPU利用率的作用。
对于信号量的操作有两种,获取信号量(taking a semaphore)和给予信号量(giving a semaphore)。在生产者消费者模型中,生产者生产数据后给予信号量,消费者获取信号量后可以处理数据

信号量

  1. 二进制信号量
    信号量数目最多为1,最多只能通知解锁一个任务。
  2. 计数信号量
    信号量数目为多个,可以通知解锁多个任务。

Binary-and-Counting

常见API

  1. 创建二进制信号量
1
SemaphoreHandle_t xSemaphoreCreateBinary( void )
  • 创建成功返回信号量句柄,失败返回NULL
  1. 获取信号量
1
BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait )
  • xSemaphore: 信号量句柄
  • xTicksToWait:如果信号量不可用时任务处于阻塞态的最长时间。portMAX_DELAY一直等待。
  • 成功返回pdPASS,失败返回pdFALSE
  1. 给予信号量
1
BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore )
  • xSemaphore:信号量句柄
  • 成功返回pdPASS,失败返回pdFALSE

中断版本

1
BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore,BaseType_t *pxHigherPriorityTaskWoken )
  1. 创建计数信号量
1
2
SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount );
  • uxMaxCount:计数信号量包含信号量的最大值
  • uxInitialCount:计数信号量中信号量的初始值

信号量应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#全局变量共享
SemaphoreHandle_t xBinarySemaphore;

int main(void)
{
...
xBinarySemaphore = xSemaphoreCreateBinary();
...
}

void Task_Producer(void const * argument)
{
//生产者任务负责生产数据并给予信号量
for( ;; )
{
...
if(true==produce_some_data())
{
xSemaphoreGive(xBinarySemaphore);
}
...
}
}
void Task_Consumer(void const * argument)
{
//消费者任务等待获取信号量并进行数据处理
for( ;; )
{
xSemaphoreTake( xBinarySemaphore, portMAX_DELAY );
//下面是对数据的处理函数
...
}
}

二进制信号量适用于在数据产生的频率比较低的场合。如果数据产生的频率较高,因为信号量最多只能保存一个信号,更多产生的数据将会直接被忽略抛弃。此时需要使用计数信号量(counting semaphore)。

当有多个生产任务时,单纯依赖信号量,无法区分是谁生产的资源。可以使用队列传递资源信息。

信号量的资源保护作用

Semaphore-Protection
在上图中,有个低优先级的任务(LP)和高优先级的任务(HP),两个任务都可以对一个资源进行操作。为了对资源进行保护采用了信号量的机制。LP首先获得信号量可以对资源进行操作,在时刻1,HP因为优先级高在内核调度中抢占了LP,在时刻2,HP想获得信号量但失败因此进入了阻塞状态。之后LP可以继续对资源操作,在时刻3执行完毕后归还了信号量。在时刻4,HP因为优先级高在内核调度中抢占了LP并获取了信号量可以对资源可以操作。

优先级倒置

采用信号量保护资源的话会有个弊端,有时会产生一种现象叫做优先级倒置(Priority inversion )。

Priority-Inversion

改善-互斥量

互斥量是二进制信号量的变种,需在FreeRTOSConfig.h设置configUSE_MUTEXES为1。用于确保同一时间只有一个任务能访问共享资源,防止多任务并发访问导致的数据不一致问题。

特性 互斥量 信号量
所有权 只有获取锁的任务可以释放 任何任务都可以释放
优先级继承 支持 不支持
递归性 支持递归锁 不支持
用途 资源保护 同步和资源计数

递归锁:允许同一任务多次获取同一互斥量,避免自死锁
优先级继承:持有互斥量的低优先级任务临时继承等待该互斥量的最高优先级任务的优先级

相关API

创建互斥量:

1
SemaphoreHandle_t xSemaphoreCreateMutex( void )

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 首先申明个互斥量的全局变量
SemaphoreHandle_t xMutex;

int main(void)
{
...
xMutex = xSemaphoreCreateMutex();
...
}

void Function_Resource(void const * argument)
{
//要保护的资源函数
...
xSemaphoreTake( xMutex, portMAX_DELAY );
{
//对资源的处理
...
}
xSemaphoreGive( xMutex );
...
}

关键区

关键区(Critical Section)是资源保护的另一种方法。在FreeRTOS有两个宏定义,分别是taskENTER_CRITICAL()和taskEXIT_CRITICAL()。

1
2
3
taskENTER_CRITICAL();
...
taskEXIT_CRITICAL();

taskENTER_CRITICAL()宏定义会关闭所有中断包括内核切换所在的中断,taskEXIT_CRITICAL()宏定义会再次打开中断。所以处于宏定义之间的代码可以被独占地执行,这是保护资源的一种比较暴力的方式。