[返回]
中国计算机报2000年第49期

实时操作系统任务间通信的设计与分析

卢燕青 张宇飞

  实时操作系统的核心是支持并发任务调度、提供任务同步和通讯机制。在具体实现时,一般应考虑尽可能减少系统本身的开销,任务调度、任务间通信和中断处理等系统公用程序也应精练有效。其任务间的通信,主要涉及共享数据结构的选择、任务间通信机制的实现、临界区域的保护以及死锁的预防等问题。


  共享数据结构


  在多任务系统中,共享内存是任务间通信最简单、最迅速的方法。特别是在实时操作系统环境下,高优先级任务与低优先级的任务共享同一块内存时,经常会造成共享数据的冲突。因此,在设计任务间的通信时必须考虑特殊的缓冲区数据结构来避免共享数据冲突。

  “乒乓”缓冲结构

  在传送与时间相关的数据时(例如数据处理速度大于数据的输入速度),一般采用“乒乓”缓冲结构。它由两块缓冲区构成,通过硬件或软件来控制两个缓冲区间的切换。其典型应用有磁盘控制器、图形接口卡等设备。

  环缓冲结构

  环缓冲结构类似于FIFO表,但它比FIFO表易于管理。在环缓冲结构中,并发的输入和输出可用通过头尾指针来控制。数据从尾指针处写入,从头指针处读出。

  环缓冲结构与信号灯一起使用可以控制资源的并发使用。例如在访问内存、打印机等资源时,可以将资源的请求置于尾指针指向的存储区,资源分配程序从头指针中取出数据后按照请求分配资源。


  任务间通信的设计


  邮箱及其实现

  邮箱是大多数多任务操作系统任务间通信的一种方式。它是公认的一块内存区域,由一个集中调度者来控制各任务对其的读写,从而实现任务间传递数据的目的。任务可以通过post操作来写这块内存,或通过pend操作来读取这块内存的数据。这种pend操作与简单轮询邮箱的区别在于:前者在等待数据时处于挂起(suspend)状态,不占用任何CPU资源;后者则是占用CPU,不停地检查邮箱。邮箱传递的数据一般是一个标志(flag)、单个数据,或者是指向链表或队列的指针。在具体实现时,数据一旦从邮箱读出来,邮箱就置成空状态。这样,尽管有多个任务能对同一个邮箱执行pend操作,但只有一个任务能从邮箱中取出数据。

  在基于任务控制块模型的任务管理系统中,邮箱是最容易实现的。所谓任务控制块,就是保存有任务的各种信息的数据结构。当任务状态发生改变时,其任务控制块内容相应发生变化。在这种模型中,一般都有一个监管任务和两个列表(任务资源列表和资源状态列表),任务资源列表和资源状态列表保持协调一致。例如,在表1和表2中,存在着的资源有打印机及两个邮箱。打印机正在被任务100使用,邮箱1被任务102使用,邮箱2处于空闲状态。

表1 任务资源列表

任务id

资源

状态

100

打印机

占用

102

邮箱1

占用

104

邮箱2

pend



表2 资源状态表

资源

状态

使用者

打印机

100

邮箱1

102

邮箱2

空闲


  当超级任务被系统调用或硬件中断激活后,它首先检查是否有任务在邮箱中处于pend状态。如果邮箱中数据就绪就重启该任务。类似地,如果某任务已执行post操作,操作系统则确保数据置于邮箱中,并更新其状态。

  邮箱除了上述的post和pend操作外,还可以有accept操作。accept操作允许任务在邮箱数据就绪的情况下可立即读出数据,否则返回错误代码。此外,在邮箱的pend操作中还可以添加超时控制来防止死锁。

  队列的实现

  队列可以认为是由许多邮箱排列而成,因而可以由上述相同的资源表来实现。其操作相应地有qpost操作、qpend操作和qaccept操作。

  队列所传递的数据也应该是指针,而不是数组。其典型应用是共享设备的维护管理。例如,环缓冲区存储申请设备的命令,环缓冲区的头和尾采用队列来控制对缓冲区的访问。这种方案也同样适用于设计设备控制软件、后台程序等。

  信号灯的实现

  资源共享是多任务系统中主要关心的问题。在大多数情况下,有些资源在某一时刻仅能被某一任务使用,并且在使用过程中不能被其它任务中断。这些资源主要有特定的外设、共享内存以及CPU。当CPU禁止并发操作时,那些包含使用了CPU之外的共享资源的代码就不能同时被多个任务调用执行。这样的代码就称为“临界区域”。如果两个任务同时进入同一临界区域,就会导致一些意想不到的错误。一般操作系统都提供信号灯来保护临界区域。

  信号灯的基本操作有两种,即等待P(S)操作和信号V(S)操作。P(S)操作首先挂起原始调用任务,然后等待信号灯S变为FALSE;V(S)操作是将信号灯S置为FALSE。

  如果操作系统没有提供信号灯的功能,可以使用邮箱来实现信号灯功能。

  显然,采用邮箱来实现信号灯的P(S)操作时,pend操作替代了循环等待操作,节省了CPU资源。


  死锁的产生及防止


  死锁的产生

  当多个任务竞争同样的两个或多个临界资源时,就会出现死锁。死锁在多任务操作系统中是个很严重的问题,往往不可能靠测试来消除。由于死锁出现的概率很小,很难发现,解决死锁也往往要追溯前因后果。

  当任务在所分配的时隙内因得不到所需的资源而不能完成任务处理时,就会出现“饥荒”。饥荒与死锁的不同之处在于:饥荒至少有一个任务能利用其所需的资源,但是其它任务则得不到资源;而在死锁的情况下,所有的任务都因得不到所需的资源而被迫处于阻塞状态。

  在实时多任务操作系统环境下,互斥、循环等待、占有等待、禁止抢占都有可能产生死锁。

  对于某些不可共享的资源,必须采用互斥保护措施。当然,也可以通过其它技术手段来使得这些资源变为可共享资源,这时就可以去掉互斥的保护措施。

  当某一任务占用资源A并申请资源B,另一任务占用资源B并申请资源A时,就会出现循环等待。一个消除循环等待行之有效的方法就是:强加给资源一个次序,并且迫使所有的任务在申请资源时必须以递增的次序。例如,设计一个如表3所示的资源次序表。

表3 资源次序表

设备

次序号

磁盘

1

打印机

2

监视器

3


  如果某个任务希望使用打印机和监视器,它就必须先申请打印机,然后申请监视器。可以证明,采用这种方案可以消除死锁。

  当任务申请到某一可用的资源,并且在它能够申请到另一可用的资源之前,一直不释放前一个资源时,就会出现占有等待。一个可行的解决办法就是,在同一时间分配给任务所有需要(包括潜在需要)的资源,这样有可能导致其它任务产生饥荒。另一个办法就是决不允许任何任务在同一时刻锁住多个资源。

  最后,禁止抢占也会导致死锁。也就是说,如果一个低优先级的任务占有信号灯保护的某一资源,另一个高优先级的任务中断低优先级的任务的运行,并处于等待该信号灯状态时,由于低优先级的任务不可能释放其信号灯,这样高优先级的任务将一直等待下去。这就是所谓的“优先级逆转”。如果我们允许高优先级的任务能够抢占低优先级任务,就不会出现死锁。然而,这样也可能导致低优先级任务“饥荒”以及其它干扰问题。

  死锁的防止

  从上面的分析可知,只要不满足造成死锁的四个必不可少的条件,就可以避免死锁。一些常用的方法有:

  采用带有超时控制的信号灯。这样,信号灯在超时后不再保护临界资源,临界资源可以被其它任务使用。

  允许抢占。不足之处是可能会造成“饥荒”或I/O操作问题。

  消除占有等待状态。但这样有可能延长响应时间。

  采用“银行家算法”。其原理类似于一个小银行的存取款过程。这种算法能确保分配给所有任务的资源都不可能超过系统可用资源。这样就可以预留一部分可用资源来满足其它任务的需求。这种算法的实时性不是很好,并且任务所需的资源的优先级往往是未知的。