嵌入式操作系统漫议:任务间通信之队列

嵌入式操作系统中任务之间的交互与Linux同一进程的不同线程之间的交互完全类似,可以通过全局变量和任务间通信机制两种方法来实现。这种交互包括任务间的数据传递、状态通知和动作同步等等。

任务间通信主要用于以下几个目的:

  • 在任务之间,或任务与中断处理程序之间传递数据。
  • 共享资源管理。如果一个资源为多个任务、或者任务与中断处理程序所访问,该资源即为共享资源。共享资源可以是一个全局变量、全局数据结构,或者是一个外设,比如GPIO、串口等等。为了保证数据一致性和访问逻辑的完整性,必须保证一个任务、或中断处理程序在访问时的排他性。
  • 任务同步。是指不同任务的相关处理按用户设计的先后顺序执行。比如有两个任务:任务A负责从UART接收数据,并放入一块共享数据区,当数据区放满以后,通知任务B,然后进入等待状态;任务B从该共享数据区读取数据,等共享数据区被全部读出以后,通知任务A,然后进入等待状态。图1这样通过任务之间的同步,两个不同任务的操作就可以按设计的顺序执行。

 

1 任务同步

任务间数据传递

第一种方式是通过全局变量进行交互,涉及到多个任务对同一个数据区访问的问题。为了保证数据的一致性,必须通过中断禁止加访问控制(也可以使用信号量等其他操作系统机制,但这实际上是任务间通信了)。这种供多个任务访问的数据区叫临界区(critical section)。与临界区访问相关的问题一直是多任务(线程)编程的梦靥,这种问题往往没有确定的再现方法,而且同一个问题导致的现象也可能多种多样,非常难于定位。而利用操作系统的任务间通信服务,可以有效避免避免临界区访问控制的复杂性,提高系统运行的确定性。

队列传递

另一种方式是采用操作系统的队列(Queue)机制,有的操作系统也叫邮箱(Mailbox)。队列是由多个元素构成的一个有顺序的序列,其中的每个元素叫数据项,也可以叫消息,如图 2所示(以FreeRTOS为例)。

 

图 2 任务间通过队列传递数据

队列具有以下几个特点:

  • 数据项的大小固定。
  • 可以容纳的数据项个数(也叫队列长度)也是固定的。
  • 数据项的大小及队列长度在初始化可以配置。但一旦确定,在运行中一般不可修改。
  • 队列中的数据项按先进先出(FIFO)的形式组织。因此往队列中追加数据项时,新数据项总是放在队列的末尾;从队列中读出数据项时,总是从队列的开头读取,被读取的数据项从队列中删除。
  • 队列用于多个任务之间的单向通信,也即一个或多个任务往队列里写入数据,一个任务从队列里读出数据。但也可以实现为多个任务从队列中读取数据,这在平行处理的多任务系统中是有用的,可以为相同的处理启动多个任务,从而加快系统整体的数据处理。

在很多嵌入式应用中,一个任务的基本结构通常如图 3所示。拥有一个接收队列,接收一个数据项,执行相应的处理,然后又将处理结果写入别的任务的队列。在这样的结构中,队列一般为接收任务所独占,其他任务需要向该任务发送数据时,将数据写入该队列。接收任务一般在该队列上调用阻塞读操作:如果接收队列非空,则取出第一个数据项,立即返回;如果接收队列为空,则该任务就进入等待状态,直到其接收队列非空。当接收到数据时,该任务进入就绪状态,等待被调度执行。

 

图 3 任务结构和消息投递

往队列的数据项写入时,有两种方式:拷贝入队/引用入队,这两种方式的传递过程如图 4所示。

如采用拷贝入队,发送任务在自己的缓存中生成需要发送的数据,并将该缓存中的数据拷贝到队列的数据项中;接收任务将队列数据项中的数据拷贝到自己的缓存中,再进行处理。

如采用引用入队,发送任务申请一块公共缓存,在该缓存中生成需要发送的数据,同时把该缓存的指针放入队列的数据项中;接收任务从数据队列中取出该公共缓存的指针,然后可以直接使用该缓存中的数据,待处理结束后,再释放缓存。

这两种方式各有其优缺点,详细参见表 1。

 

图 4 拷贝入队和引用入队

 

1 拷贝入队和引用入队的比较

 

拷贝入队

引用入队

缓存管理

接收任务和发送任务不共享缓存,分别管理各自的缓存

接收任务和发送任务共享缓存,接收任务在使用完数据后,需要释放共享缓存

拷贝次数

两次数据拷贝

不需数据拷贝

传递数据灵活性

一次传递数据的长度必须小于队列数据项的长度,过长的数据必须分次传送

可以根据需要传递数据的长度申请缓存,因此使用比较灵活。需要传递的数据越长,引用入队的优势越明显

传递效率

需要拷贝两次数据,效率较低

不需要拷贝数据,效率较高。

 

综上所述,在需要传递的数据量较少的时候,采用拷贝入队,简化缓存的管理;而需要传递的数据量较多时,采用引用入队,提高效率。当然,这两种方式也完全可以同时使用,只要存入数据项的是缓存地址,就是引用入队。因此,只有在发送任务和接收任务间协商一致,可以灵活使用两种方式。

当然,在一些使用内存保护的系统中,如果两个任务间内存是完全分离、不能共享内存,则只能通过拷贝入队的方式传递数据。

队列流控机制

但往队列中写入速度快于从队列中读取的速度时,队列就会填满。但队列满时,需要适当的队列流控机制来保证整个系统的正常运行。基本的流控机制有以下几种:

  • 由发送任务抛弃入队失败的数据项。利用操作系统的数据发送函数就可以实现。比如在FreeRTOS中,采用以下API。

BaseType_t xQueueSendToBack(QueueHandle_t xQueue, const void *pvItemToQueue,  TickType_t xTicksToWait)

其中,xTicksToWait用于指定队列满时的等待时间。如果该值指定为0,则不等待,队列满时立即返回,由发送任务丢弃发送失败的数据。

  • 抛弃在队列中等待时间最长的数据项。这需要操作系统的支持,FreeRTOS提供了以下的API:

BaseType_t xQueueOverwrite(QueueHandle_t xQueue, const void *pvItemToQueue)

如果队列不满,则与xQueueSendToBack()相同操作;如果队列满了,则替换队列中最老的数据。

  • 挂起发送任务,也即发送任务进入等待状态,直至队列中有空的数据项。用xQueueSendToBack()发送数据项、且指定xTicksToWait不为0时,则如果队列满了,发送任务被挂起,最多等待xTicksToWait长的时间。如果等待时间超时,队列还未变空,则xQueueSendToBack()超时退出。
  • 用信号量实现流控。这在信号量的部分详细描述。

具体采用哪种策略,跟系统的应用特性有关。如果丢弃的数据可以通过外部的重传来恢复,那么前两种方案都是可接受的。比如一个系统从串口接收数据,然后该数据在不同的任务之间传递,如果在传递途中,由于队列满而不能写入,而且串口通信机制上有重传功能,则丢弃是可以的。还有,如果是系统内部定期生成的数据,而且只有最新的数据才有意义时(比如一些累积统计信息),则可以采用第二个策略。

后两种方案不丢失数据,可用在需要保证系统内数据完整性的场合。但如果应用程序设计不当,可能会导致系统死锁问题。

队列传递中的死锁问题

 在消息传递的过程中,会产生死锁的问题,如图 5所示的例子。

  • 在任务B的接收队列满时,任务A向B发送消息。由于不能写入,任务A被挂起,等待B的接收队列出现空数据项。
  • 任务B完成一个消息的处理,恰好需要向A发送消息。
  • 此时如果任务A的接收队列也满了(由于任务A被挂起,不能处理消息),此时任务B也被挂起。
  • 由于任务A、B都被挂起,不能处理消息,因此其接收队列就不会变空,进入死锁状态。
  • 其他任务如果需要向A、B发送消息,也会被挂起,导致整个系统进入死锁状态,停止运行。

为了避免系统死锁,可以采用几种方法。

  • 对所有的消息发送,都指定有效的超时时间。如果超时时间到,而接收队列还没有变空,系统会失败返回,从而发送任务可以继续运行。在发送任务中实现适当的发送失败处理,就可以避免死锁的情况。在像FreeRTOS系统中,必须指定超时时间(超时时间为0表示不等待,立即返回),从而可以有效避免死锁。

 

图 5消息传递导致的死锁

  • 在有的操作系统中,还提供了查询队列状态的API。如FreeRTOS,有以下的API:

UBaseType_t  uxQueueSpacesAvailable( const QueueHandle_t  xQueue);

用于查询当前队列中空数据项的数量。在发送之前,可用它先查询队列是否为满,如队列满,可以直接进行发送失败的处理。这样也可以避免死锁。

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值