说信号量之前需要解释一下计算机系统中的资源的概念,这里所说的资源并不仅仅指CPU和内存,打印机、显示器、音响等都算是资源。只要是资源,就存在占用问题,例如如果有两个任务A、B在同一个时间段都想使用同一台打印机进行打印服务,如果系统不对A和B进行管理,两个任务同时向打印机发送数据,可能就会出现冲撞导致打印错误,如果系统先让A任务打印,然后再让B打印,这样就避免了任务之间的资源占用冲撞问题。

信号量在FreeRTOS系统中有三个,分别是二值信号量(Binary Semaphore)、计数信号量(Counting Semaphore)和互斥锁(Mutex)。信号量可以解决计算机系统的资源抢占以及线程同步问题。

这再里需要介绍一个概念:原子操作,这个概念应用在某个操作上表示该操作是无法拆分的,是原子级别的,例如:

GlobalVar |= 0x01;

上面这段程序就不属于原子操作,因为这段程序编译成汇编程序之后就成下面:

LDR r4,[pc,#284]
LDR r0,[r4,#0x08] /* Load the value of GlobalVar into r0. */
ORR r0,r0,#0x01 /* Set bit 0 of r0. */
STR r0,[r4,#0x08] /* Write the new r0 value back to GlobalVar. */

简简单单的一段C语言翻译成汇编就有4行,是一个典型的Read、Modify、Write操作,4行代码的执行过程可能会被打断,例如当执行完前两行的Read操作之后发生任务调度,在另一个任务中对GlobalVar 变量进行了一次赋值,当再次返回到这个任务的时候GlobalVar 的值其实已经改变了,这时候在执行下面的Modify和Write操作就会出现数据错误的情况。

为了解决上面的资源(CPU)占用问题,我们可以使用临界区、关闭任务调度器、互斥锁三种方式。

临界区可以使用类似 ENTER_CRITICAL() 函数/宏进入,使用 EXIT_CRITICAL() 退出临界区域,一般ENTER_CRITICAL是关闭总中断,EXIT_CRITICAL是打开总中断,这样一来在临界区域中执行代买就不会被中断事件打断,也就是不会出现上面例子中执行完Read操作就被高优先级的任务打断的情况。但是这种做法的缺点也是很明显的,关闭了总中断就会影响其他非常重要的事件的处理,即使这个非常重要的事件并没有与这个当前有资源争夺的情况,这时候互斥锁可以很好的解决这个问题。

关闭任务调度器可以防止系统发生任务调度,这种方式可以保证受保护区域的代码不会被其他任务所打断,但是不同意临界区,关闭任务调度器任然允许产生中断,这时候如果中断ISR和任务存在资源争夺的话也会导致错误。

互斥锁的原理:当资源被锁住的时候,资源就处于不可访问的状态,当资源处于解锁状态的时候,资源才可以被访问。这样假设A任务和B任务竞争一个资源C,A在访问C的时候会先查看C的锁是否处于锁住状态,如果处于锁住状态则进入Blocked状态,否则则占用C并将C的互斥锁锁住,这时候假如B任务抢占CPU,也对C进行访问,但是发现C的互斥锁处于锁住的状态,那么B就会进入Blocked状态,等到A使用完C之后会释放C的互斥锁,这才会将B任务从Blocked状态唤醒。

这里有必要说一下互斥锁(Mutex)和二值信号量(Binary Semaphore)的区别。其实两者的作用十分相似,互斥锁的取值只有Locked和Unlocked两种,二值信号量也只有0和1两个值,两者是能高度对应上的,但是Mutex解决了使用Binary Semaphore时的一个优先级反转的问题,通俗的说就是使用Binary Semaphore进行资源的独占访问的时候,低优先级的任务可能会优先于高优先级的任务运行,具体案例参考文章。而Mutex解决了这个问题,具体在内核中的实现方式不多做解释,简单来说就是在低优先级任务A占用资源(Take Mutex)的时候,如果有一个更高优先级的任务B希望访问相同的资源,那么这时候低优先级任务A的优先级会继承较高优先级的任务B的优先级,这叫做优先级继承(Inherit),以防止优先级介于A和B任务之间的任务C抢占任务A的CPU,使得更高优先级的B无法及时运行,因此,这种继承优先级的方式使得任务A会在尽可能短的时间内使用完资源并释放互斥锁(Give Mutex)。所以在资源独占访问上使用Mutex是比较合适的,但是我们要知道,相对于二值信号量,互斥锁只是在此基础上解决了优先级反转的问题。二值信号量一般用于线程同步,互斥锁一般用于互斥操作

参考FreeRTOS的Reference Manuel:Binary semaphores and mutexes are very similar, but do have some subtle differences.Mutexes include a priority inheritance mechanism, binary semaphores do not. This makes binary semaphores the better choice for implementing synchronization (between tasks or between an interrupt and a task), and mutexes the better choice for implementing simple mutual exclusion.

Binary Semaphores – A binary semaphore used for synchronization does not need to be ‘given’ back after it has been successfully ‘taken’ (obtained). Task synchronization is implemented by having one task or interrupt ‘give’ the semaphore, and another task ‘take’ the semaphore (see the xSemaphoreGiveFromISR() documentation). Note the same functionality can often be achieved in a more efficient way using a direct to task notification.

Mutexes – The priority of a task that holds a mutex will be raised if another task of higher priority attempts to obtain the same mutex. The task that already holds the mutex is said to ‘inherit’ the priority of the task that is attempting to ‘take’ the same mutex. The inherited priority will be ‘disinherited’ when the mutex is returned (the task that inherited a higher priority while it held a mutex will return to its original priority when the mutex is returned).

计数信号量(Counting  Semaphore)的应用场景如下:

Counting semaphores are typically used for two things:

1. Counting events.

In this usage scenario, an event handler will ‘give’ the semaphore each time an event occurs,and a handler task will ‘take’ the semaphore each time it processes an event.The semaphore’s count value will be incremented each time it is ‘given’ and decremented each time it is ‘taken’. The count value is therefore the difference between the number of events that have occurred and the number of events that have been processed.Semaphores created to count events should be created with an initial count value of zero,because no events will have been counted prior to the semaphore being created.

2. Resource management.

In this usage scenario, the count value of the semaphore represents the number of resources that are available.

To obtain control of a resource, a task must first successfully ‘take’ the semaphore. The action of ‘taking’ the semaphore will decrement the semaphore’s count value. When the count value reaches zero, no more resources are available, and further attempts to ‘take’ the semaphore will fail.

When a task finishes with a resource, it must ‘give’ the semaphore. The action of ‘giving’ the semaphore will increment the semaphore’s count value, indicating that a resource is available,and allowing future attempts to ‘take’ the semaphore to be successful.Semaphores created to manage resources should be created with an initial count value equal to the number of resource that are available.