FreeRTOS学习笔记(三)—— 信号量应用

信号量的概念及其作用

信号量(semaphores)是20世纪60年代中期Edgser Dijkstra发明的。使用信号量的最初目的是为了给共享资源建立一个标志,该标志表示该共享资源被占用情况。这样,当一个任务在访问共享资源之前,就可以先对这个标志进行查询,从而在了解资源被占用的情况之后,再来决定自己的行为。 实际的应用中,信号量的作用又该如何体现呢?比如有个30人的电脑机房,我们就可以创建信号量的初始化值是30,表示30个可用资源,不理解的初学者表示信号量还有初始值?是的,信号量说白了就是共享资源的数量。另外我们要求一个同学使用一台电脑,这样每有一个同学使用一台电脑,那么信号量的数值就减一,直到30台电脑都被占用,此时信号量的数值就是0。如果此时还有几个同学没有电脑可以使用,那么这几个同学就得等待,直到有同学离开。有一个同学离开,那么信号量的数值就加1,有两个就加2,依此类推。刚才没有电脑用的同学此时就有电脑可以用了,有几个同学用,信号量就减几,直到再次没有电脑可以用,这么一个过程就是使用信号量来管理共享资源的过程。

FreeRTOS中计数信号量的源码实现是基于消息队列实现的。

平时使用信号量主要实现以下两个功能:

  1. 两个任务之间或者中断函数跟任务之间的同步功能,这个和前面章节讲解的事件标志组是类似的。其实就是共享资源为1的时候。
  2. 多个共享资源的管理,就像上面举的机房上机的例子。 针对这两种功能,FreeRTOS分别提供了二值信号量和计数信号量,其中二值信号量可以理解成计数信号量的一种特殊形式,即初始化为仅有一个资源可以使用,只不过FreeRTOS对这两种都提供了API函数,而像RTX,uCOS-II和III是仅提供了一个信号量功能,设置不同的初始值就可以分别实现二值信号量和计数信号量。当然,FreeRTOS使用计数信号量也能够实现同样的效果。

FreeRTOS信号量有计数信号量、二值信号量、互斥信号量;而像RTX,uCOS-II和III是仅提供了一个信号量功能。二值信号量是计数信号量的一个子集。但是二值信号量与互斥信号量是有区别的。

互斥信号量的主要作用是对资源实现互斥访问,使用二值信号量也可以实现互斥访问的功能,但是互斥信号量可以防止优先级翻转,而二值信号量不支持。

信号量有用于任务中也有用在中断中的,但是互斥信号量只能用于FreeRTOS任务中,不能用于中断中

优先级反转问题

FreeRTOS学习笔记(三)—— 信号量应用_第1张图片

运行条件:

  • 创建3个任务Task1,Task2和Task3,优先级分别为3,2,1。也就是Task1的优先级最高。
  • 任务Task1和Task3互斥访问串口打印printf,采用二值信号实现互斥访问。
  • 起初Task3通过二值信号量正在调用printf,被任务Task1抢占,开始执行任务Task1,也就是上图的起始位置。

运行过程描述如下:

  • 任务Task1运行的过程需要调用函数printf,发现任务Task3正在调用,任务Task1会被挂起,等待Task3释放函数printf。
  • 在调度器的作用下,任务Task3得到运行,Task3运行的过程中,由于任务Task2就绪,抢占了Task3的运行。优先级翻转问题就出在这里了,从任务执行的现象上看,任务Task1需要等待Task2执行完毕才有机会得到执行,这个与抢占式调度正好反了,正常情况下应该是高优先级任务抢占低优先级任务的执行,这里成了高优先级任务Task1等待低优先级任务Task2完成。所以这种情况被称之为优先级翻转问题。
  • 任务Task2执行完毕后,任务Task3恢复执行,Task3释放互斥资源后,任务Task1得到互斥资源,从而可以继续执行。

上面就是一个产生优先级翻转问题的现象。

FreeRTOS任务间计数信号量的实现

FreeRTOS学习笔记(三)—— 信号量应用_第2张图片
FreeRTOS中断方式计数信号量的实现FreeRTOS学习笔记(三)—— 信号量应用_第3张图片

互斥信号量解决互斥资源访问以及优先级反转问题

FreeRTOS学习笔记(三)—— 信号量应用_第4张图片

运行条件:

  • 创建2个任务Task1和Task2,优先级分别为1和3,也就是任务Task2的优先级最高。
  • 任务Task1和Task2互斥访问串口打印printf。  使用FreeRTOS的互斥信号量实现串口打印printf的互斥访问。

运行过程描述如下:

  • 低优先级任务Task1执行过程中先获得互斥资源printf的执行。此时任务Task2抢占了任务Task1的执行,任务Task1被挂起。任务Task2得到执行。
  • 任务Task2执行过程中也需要调用互斥资源,但是发现任务Task1正在访问,此时任务Task1的优先级会被提升到与Task2同一个优先级,也就是优先级3,这个就是所谓的优先级继承(Priority inheritance),这样就有效地防止了优先级翻转问题任务Task2被挂起,任务Task1有新的优先级继续执行。
  • 任务Task1执行完毕并释放互斥资源后,优先级恢复到原来的水平。由于互斥资源可以使用,任务 Task2获得互斥资源后开始执行。

这里优先级继承可能就是导致互斥信号量只能用于FreeRTOS任务中,不能用于中断中,而二值信号量可以用于中断中的原因所在

任务计数信号量的实现

FreeRTOS计数信号量的另一种实现方式----基于任务通知(Task Notifications)的计数信号量,这里我们将这种方式实现的计数信号量称之为任务计数信号量。任务计数信号量效率更高,需要的RAM空间更小。但他的缺点就是没有普通计数信号量实现的功能全面。

任务通知的介绍

FreeRTOS每个已经创建的任务都有一个任务控制块(task control block),任务控制块就是一个结构体变量,用于记录任务的相关信息。结构体变量中有一个32位的变量成员ulNotifiedValue是专门用于任务通知的。 通过任务通知方式可以实现计数信号量,二值信号量,事件标志组和消息邮箱(消息邮箱就是消息队列长度为1的情况)。使用方法与前面章节讲解的事件标志组和信号量基本相同,只是换了不同的函数来实现。任务通知方式实现的计数信号量,二值信号量,事件标志组和消息邮箱是通过修改变量ulNotifiedValue实现的:

  • 设置接收任务控制块中的变量ulNotifiedValue可以实现消息邮箱。
  • 如果接收任务控制块中的变量ulNotifiedValue还没有被其接收到,也可以用新数据覆盖原有数据 ,这就是覆盖方式的消息邮箱。
  • 设置接收任务控制块中的变量ulNotifiedValue的bit0-bit31数值可以实现事件标志组。
  • 设置接收任务控制块中的变量ulNotifiedValue数值进行加一或者减一操作可以实现计数信号量和二值信号量。

采用这种方式有什么优势呢?根据官方的测试数据,唤醒由于信号量和事件标志组而处于阻塞态的任务,速度提升了45%,而且这种方式需要的RAM空间更小。但这种方式实现的信号量和事件标志组也有它的局限性,主要表现在以下两个方面:

  • 任务通知方式仅可以用在只有一个任务等待信号量,消息邮箱或者事件标志组的情况,不过实际项目项目中这种情况也是最多的。
  • 使用任务通知方式实现的消息邮箱替代前面讲解的消息队列时,发送消息的任务不支持超时等待,即消息队列中的数据已经满了,可以等待消息队列有空间可以存新的数据,而任务通知方式实现的消息邮箱不支持超时等待。

任务计数信号量

计数信号量就是对一个变量进行计数,变量的范围是从0到用户创建计数信号量时所设置的大小。当计数变量大于0的时候计数信号量管理的资源才可以使用,计数变量的具体数值就是可用的资源大小。这里的任务计数信号量要实现的与普通的计数信号量功能一样,不同的是调用的函数和使用的计数变量不同。

  • 任务计数信号量的计数变量是通过任务控制块中的一个32位变量ulNotifiedValue实现计数。前面的普通计数信号量创建后会有自己的计数变量。
  • 任务计数信号量是通过函数ulTaskNotifyTake()替代前面普通计数信号量的函数xSemaphoreTake()实现资源获取,即对计数信号量数值进行减一操作。
  • 任务计数信号量是通过函数xTaskNotifyGive() 和vTaskNotifyGiveFromISR()替代前面普通计数信号量的函数xSemaphoreGive() 和 xSemaphoreGiveFromISR()实现资源释放,即对计数信号量的数值进行加一操作。

任务计数信号量API函数
使用如下9个函数可以实现FreeRTOS的任务信号量(含任务计数信号量和任务二值信号量):

  • xTaskNotifyGive()
  • vTaskNotifyGiveFromISR()
  • ulTaskNotifyTake()
  • xTaskNotify()
  • xTaskNotifyAndQuery()
  • xTaskNotifyAndQueryFromISR()
  • xTaskNotifyFromISR()
  • xTaskNotifyWait()
  • xTaskNotifyStateClear()

任务二值信号量的实现

多次调用函数xTaskNotifyGive ()难免会出现计数值大于1的情况,用作任务二值信号量时,我们可以将所有大于1的计数理解为一种情况,即二值信号量管理的资源可用。因此,不管当前的计数是多少,大于0的计数在通过函数ulTaskNotifyTake()获取二值信号量的时候统一清零,这样就实现了二值信号量的功能。

实际项目中,如果使用二值信号量和任务二值信号量都能实现相应功能,强烈建议使用任务二值信号量。

你可能感兴趣的:(FreeRTOS)