干货

FreeRTOS学习笔记 (4)任务间通信

分类名:经验日期:2018-04-29作者:cruelfox
分享到
微博
QQ
微信
LinkedIn

  前面和大家分享了 FreeRTOS 的任务是如何建立,以及CPU是如何从一个任务切换至另一个任务的。要发挥多任务的长处,光有调度器和时间片管理还不够,必须有机制让多个任务能协同工作
  比如,设想串口输出字符串信息的如下场景:
  (1) 一个任务是专门管理串口发送的,它在没有数据需要操作 UART 硬件的时候处不会被执行,收到发送请求时需要得到执行,将要发送的字符串存入自己的缓冲区,再按顺序写入 UART 硬件  FIFO,写满后立即暂停执行
  (2)  另外还有几个任务需要从串口输出信息,当某一任务要求从串口输出一个字符串时,如果串口非空闲(上次的字符串没有发送完,或者其它任务的字符串正在发送),则该任务被阻塞,等待机会;当输出字符串的请求被处理后,任务继续执行
  (3) UART 硬件在传输时,CPU时间交给了其它的任务,直到传输完成的中断到来,再继续执行负责串口的任务
  这里需要有任务间的通信——将数据(字符串)从一个任务传递给另一个任务;需要任务同步——只有收到请求、请求得到响应,才继续执行,也就是CPU执行上下文的切换;需要任务互斥——一个任务申请到了串口使用权,其它任务就不能申请到,避免输出交织混乱;还需要用中断来触发任务切换的方法。

  FreeRTOS 任务间通信有两种方法:一是直接发通知(Notification),一是使用通信对象(Communication objects). 主要区别在于通知是发向一个指定的任务的,直接改变该任务TCB的某些变量;通信对象是独立于任务的实体,有单独的存储空间,可以实现数据传递和较复杂的同步、互斥功能。

1. 任务通知 (Notification)

  在启用了任务通知(这是默认的)以后,任务TCB数据结构会多两个成员:
#if( configUSE_TASK_NOTIFICATIONS == 1 )
        volatile uint32_t ulNotifiedValue;
        volatile uint8_t ucNotifyState;
#endif
其中一个是记录任务通知的状态,一个是通知的数据。当然我们写程序不需要直接操作这些变量,而是用 FreeRTOS 的 API,例如最常用的是这两组函数:

发送方目标任务
xTaskNotify()        xTaskNotifyWait()
xTaskNotifyGive()        ulTaskNotifyTake()

注:还有 xTaskNotifyFromISR() 和 xTaskNotifyGiveFromISR() 两个 API 是 ISR 专用的版本。本篇暂不讨论 ISR,所以下面也不提及各种名称如 xxxxFromISR() 的 API 函数,下一篇单独讨论中断。

  对于一个需要等待事件的任务,调用 xTaskNotifyWait() 来等待其它任务(或者中断ISR)给它一通知。如果已经有通知(pending状态),则立即返回;否则任务切换到阻塞状态,直到通知到来或者超时。通知的内容是32位整型数,用法也有几种,在API参数中说明。简化版本的 xTaskNotifyGive() 和 ulTaskNotifyTake() 将通知内容作为一个计数器。任务的 ucNotifyState 有三种状态,如下图。

  对于通知的发送方,是没有阻塞功能的,也就是不能等待目标任务的通知状态变化,这一点和使用某些通信对象有所不同。和通信对象比起来通知的实现更简单,增加的内存开销也很小。

2. 信号量 (Semaphore)

  信号量是操作系统中的概念,在实现任务或进程、线程同步过程中扮演重要的角色。FreeRTOS 提供以下四种类型信号量:

类型创建方法
普通型xSemaphoreCreateBinary()
计数型xSemaphoreCreateCounting()
互斥锁xSemaphoreCreateMutex()
嵌套互斥锁xSemaphoreCreateRecursiveMutex()


  信号量是一种通信对象,需要创建后才可以使用。若不再需要可以调用 vSemaphoreDelete()  将它删除,释放占用的内存。前三类(除了 recursive mutex)信号量都是用 xSemaphoreTake() 和 xSemaphoneGive() 两个 API 分别进行"获取"和"给予"操作。最普通的信号量只有两个状态(有/无),计数型的可以是0到某个数之间的整数,代表资源的余量。它们看起来和任务通知的用法很像,也的确经常可以用任务通知替代。区别在于 "Give" 信号量的时候并不需要知道是哪个任务想 "Take" 它,也的确可以支持多个任务 "Take" 同一个信号量。

  互斥锁(mutex, 这个词是 mutual exclusion 缩写而来)也只有两个状态,但用法不同。互斥锁用来避免多个任务争用同一资源的问题,让一个任务获取它以后别的任务都不能再获取而只能阻塞,直到取得它的任务交还出来。也就是说,互斥锁是同一个任务在 "Take" 和 "Give",用过必须归还;而普通信号量是“施”与“受”分开的,往往还是某个中断 ISR 在“施”。

  互斥锁使用不当可能造成死锁(deadlock),比如有两个任务:甲和乙,都需要互斥锁A和B代表的资源;甲先取得A锁,等待B锁,但B锁已由乙取得,而乙还在等待A锁。
  嵌套互斥锁(recursive mutex)是让同一个任务可以重复申请已取得的互斥锁,避免自己造成死锁这种不合逻辑的现象。对应的操作函数是 xSemaphoreTakeRecursive() 和 xSemaphoreGiveRecursive(). 例如,某任务先获得这个锁,然后调用一个子程序,子程序中又再次申请获得这个锁,那么既然资源是自己独占的,这个申请立即成功。子程序进行一些操作后释放该锁,但更早的申请还有效,资源仍然属于这个任务独占。

3. 队列 (Queue)

  FreeRTOS 的队列除了提供任务同步机制外,本身就是一个数据传递的通道。实际上信号量的实现也是通过队列,这一点研究一下 FreeRTOS 代码就知道。用 xQueueCreate() 函数创建一个队列时,需要指定队列长度和队列元素大小(每一项数据字节数),以分配队列的数据存储空间。在不需要用某个队列的时候,也最好调用 xQueueDelete() 将它清除。

  队列操作函数主要有:

API 函数名功能同步作用
xQueueReceive()        接收队列头的数据无数据时阻塞
xQueuePeek()获取队列头部数据,但不移除无数据时阻塞
xQueueSend(), xQueueSendToBack()在队列尾添加数据无剩余空间时阻塞
xQueueSendToFront()在队列头添加数据无剩余空间时阻塞
uxQueueMessagesWaiting()返回队列已用量
uxQueueSpacesAvailable()返回队列剩余量
xQueueReset()清空队列

  和任务通知、普通/计数型的信号量相比,在任务同步作用上队列可以使发送方阻塞。
  另外还有一个特殊的函数 xQueueOverwrite(), 用于长度为1的队列,如果队列有数据(满)也强行改写。

  当多个任务同时等待读一个队列,或者多个任务同时等待写一个队列时,任务的优先级也会起作用,让优先级高的任务获得资源(而不是“先来先服务”)。

4. 队列集合 (Queue Set)

  尽管队列作为通信对象可以多任务共用,消息发送方和接收方可以是一对多,也可以是多对一的关系,在消息类型不同(不同的数据结构)不一样的时候,用多个队列从代码编写角度是更好的选择。FreeRTOS 提供队列集合,用于选择多个队列以及信号量进行“监听”,只要其中不管哪一个有消息到来,都可以让任务退出阻塞状态。这个功能和 TCP/IP socket 库函数中的 select() 有相似之处。

  用 xQueueCreateSet() 创建队列集合,再用 xQueueAddToSet() 将若干队列(或信号量)添加进队列集合之后,就可以用 xQueueSelectFromSet() 来等待其中任何一个队列有新数据。不过还有一些限制,例如一个队列只能加入一个队列集合;例如队列被加入到队列集合之后,就不能像以前那样自由使用。

5. 事件组 (Event Group)


  事件组这个通信对象和前几个又不相似,它的存储有点像硬件上的中断标志寄存器。虽然“事件”只用0或1表示,含有的信息有限,但事件组提供了队列不具有的一些功能:
  (1) 用于等待几个同步事件同时满足,而不是依次满足
  (2) 多个任务共享一个或几个事件的触发,同时离开阻塞状态

  至于事件是什么,有多少,完全是由应用程序自己决定的。FreeRTOS 一个事件组最多可以容纳24个事件(标志位),设置和清除事件的 xEventGroupSetBits() 与 xEvencGroupClearBits() 函数就像 GPIO 端口的位操作那样简单。
  在事件组的 API 中,用来起等待(同步)作用的是以下两个函数

xEventGroupWaitBits()        等待若干事件标志中一个或全部都为真
xEventGroupSync()置位指定的若干事件标志,并等待一组事件标志位同时为真



  以下是对实现细节一些粗略分析

  我写了个最简单的使用 binary semaphore 的例子,GDB 跟踪一下:


     xSemaphoreTake() 实际上调用的函数是 xQueueGenericReceive(),这也印证了信号量是由队列来实现的(似乎有点小题大做了)。查看 handle 确认我创建的信号量是一个特殊的队列,它的数据结构和队列是一样的。在 queue.c 里面定义了 xQUEUE 这一结构体:

作为简单的 semaphore, 用这么一个结构是浪费了些RAM.

  跟踪  xQueueGenericReceive() 的执行,发现关键之处在        
   vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToReceive ), xTicksToWait );
这一条操作,字面意思是把当前任务插入队列的“等待接收”的任务列表中。再看这个函数:
void vTaskPlaceOnEventList( List_t * const pxEventList, const TickType_t xTicksToWait )
{
        configASSERT( pxEventList );
        vListInsert( pxEventList, &( pxCurrentTCB->xEventListItem ) );
        prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );
}
它有两步操作:一是把当前任务 TCB 中 xEventListItem 这一项插入 xTasksWaitingToReceive 列表,二是把当前任务放到延迟执行的列表中(也就是从ready状态改为阻塞了)。再回到 xQueueGenericReceive() 当中,不久便执行任务调度。


  再回顾一下任务 TCB 数据结构,有两个 ListItem_t 类型的数据
  ListItem_t    xStateListItem;
  ListItem_t    xEventListItem;       
在前一贴介绍过,xStateListItem 是用来记录任务状态的。我大胆猜想一下,xStateListItem 是用来记录任务从阻塞到恢复需要的外部事件的——当事件发生时,顺藤摸瓜找到这个任务。

  接着,跟踪 xSemaphoreGive() 函数,也就是实际的 xQueueGenericSend() 函数里面,用了
xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive );
将等待列表中的任务移出,实际上是把该任务 TCB 中 xStateListItem 从所在列表删除,把该任务添加到就绪列表。

  从我观摩过的部分代码看来,FreeRTOS 用队列进行任务间通信、实现调度的CPU开销还是蛮大的。虽然我还没有仔细评估,感觉跟踪过的代码很频繁地用 vPortEnterCritical(), vPortExitCritical(). 倘若 ISR 要使用信号量来通知任务去处理,额外的开销就比中断进出本身多多了。


FreeRTOS学习笔记 (1)应用场景

FreeRTOS学习笔记 (2)堆栈——任务切换的关键

FreeRTOS学习笔记 (3)任务状态及切换

FreeRTOS学习笔记 (4)任务间通信

FreeRTOS学习笔记 (5)中断与任务切换

FreeRTOS学习笔记 (6)实验:串口后台打印

FreeRTOS学习笔记 (7)FreeRTOS的软件结构




关键字:FreeRTOS
阅读原文 浏览量:1794 收藏:1
此内容由EEWORLD论坛网友 cruelfox 原创,如需转载或用于商业用途需征 得作者同意并注明出处

上一篇: FreeRTOS学习笔记 (3)任务状态及切换
下一篇: FreeRTOS学习笔记 (5)中断与任务切换

评论

登录 | 注册 需要登陆才可发布评论    
评论加载中......
成王成成王
楼主辛苦,学习了!
电子工程世界版权所有 京ICP证060456号 京ICP备10001474号 电信业务审批[2006]字第258号函 京公海网安备110108001534 Copyright ? 2005-2017 EEWORLD.com.cn, Inc. All rights reserved