分享

TEP106 任务调度

 幸福的乐土 2012-04-16

1、介绍

TinyOS有二个基本的计算抽象:异步事件和任务。Tinyos早些版本提供单一的类型任务,没有参数且只能FIFO调度。将任务调度表现成TINYOS组件更容易制定,将任务表现成TINYOS接口可扩展任务类型。TINYOS2.0采用这二种方法,这份文本记录了其是如何以简单的机制来提高系统可靠性。

2、TinyOS1.x任务调度 TinyOS中的任务是可延迟的调用过程DPC,可以使某程序延迟计算或操作。TOS任务一次运行完毕,任务间不可抢占。这二个约束条件意味着任务代码是同步的。也就是说,任务是原子性的。 在tinyos1.x中,nesC语言通过二种机制支持任务,任务声明和任务发布表达post task void computeTask() { // Code here } result_t rval = post computeTask();

TinyOS1.x提供单一的任务类型,无参数函数及单一FIFO的调度策略。Post语句可返回FAIL,表明TinyOS发布任务失败。可发布任务多次。例如,如果某一任务连续发布了二次,第一次成功但第二次失败,此任务将会被运行一次。因为这样,虽然一次发布失败,但任务仍可能运行。

Tinyos1.x调度器由sched.c文件中的C函数集实现的。若要修改调度器则需替代或修改此文件。另外,因为任务仅通过nesC中的task关键字声明和post关键字支持,假设是无参数函数,不能修改语句或任务功能。 Tinyos1.x的任务队列是由固定大小的函数指针类型的循环缓冲实现。发布任务就是将此任务的函数指针放入下个空缓冲区中。如果没有空的缓冲区,发布任务将返回失败。这类模型有几个问题:

1) 某些组件针对发布任务失败没有合适的响应 2) 某给出的任务能发布多次,这将占用多个缓冲区 3) 所有组件的所有任务共享单一资源:某个有问题的组件可能导致其他组件发布任务失败。 从根本上来,为了使组件A在发布任务失败后重新发布任务,另外一个组件B必须调用A的函数(命令或事件)??。例如组件A必须调用定时器,或从客户端重试。然而,系统中许多组件也依靠此任务(如定时器),任务队列溢出有可能导致整个系统失效。

以上三个问题组合意味着某个问题组件会导致tinos整个系统挂起。例如:这种情况(在Telos平台上真实遇到的问题)。

基于数据包的硬件收发,只在发送完一个数据包后触发中断

networking组件处理中断来发布任务进而发singal SendMsg.sendDone.事件。

sensing组件处理ADC.dataReady事件时,发布任务

application组件发送数据包,然后ADC采样频率设置得太高

发生这种情况,sensing组件将以更快的速率处理事件。将开始发布任务来处理接收到的数据,直到队列满。在某时,射频发送数据包完成并singal其中断。Networking组件不能发布任务来signal SendMsg.sendDone(),事件丢失。Application组件不会重发另外的数据包直到知道其发送完成(因此可重用缓冲区)。因sendDone()事件丢失。不会再发送其他数据包,应用程序停止网络通信。

如果发布失败,解决TinyOS 1.x中这类特殊问题的方法是在无线发送中断程序中完成signal sendDone()事件:这将与sync/async界限冲突,但合理之处是极少见的资源冲突相对某一错误而言好些。另一种解决方法是使用中断源来周期性的重新发布任务。这不违反sync/async,直到发布成功系统不能再发送数据包。The TinyOS 1.x model prevents it from doing any better.。因tinyos1.x模型限制没有其他更好的办法。

3、tinyos2.x任务 2.x任务机制与1.x不同。这是针对1.x模型中出现的限制和运行错误而制定的改变。在tiny2.x中,发布任务post失败,只有在任务已经发布且还没运行的情况下。

任务可以经常运行,但任何时候只能发布一次。

2.x中通过为每个任务分配一个字节来表示任务状态,(假设系统任务最多255个)实现这机制。若任务太多,这将占用太大开销,也就没多大意义。若某组件要多次发布某任务,任务逻辑最后可根据需要报告自己情况。 例如: post processTask(); ... task void processTask() { // do work if (moreToProcess) { post processTask(); } }

这个方式有好几个问题,比如当任务队列满时,不能发信号通知分相split-phase事件完成,初始化时任务队列溢出,组件多次发布某任务导致分配任务不公平unfair task allocation。

TinyOS2.x中采用的方式是:任务基本操作要保持简单容易,但也允许在任务基本使用的情况下引入新的任务类型。tinyOS实现的方法是:在基本任务中使用post 和task关键字,为增加的任务引入任务接口。

任务接口允许用户扩展任务语法和语义。一般,任务接口有async命令,发布和事件,run。这些函数的确切形式取决于接口中定义。例如:某一任务接口参数是整数类型。

interface TaskParameter { async error_t command postTask(uint16_t param); event void runTask(uint16_t param); }

使用这个任务接口,组件需发布带有uint16_t参数的任务。当调度器运行任务时,其中包含任务逻辑,通过传递参数signal通知runTask事件。注意这并不保存RAM信息:调度器必须为参数分配RAM空间。另外,在任何时候只能运行一个任务,即可在组件中简单的储存变化。例如,而不是: call TaskParameter.postTask(34); ... event void TaskParameter.runTask(uint16_t param) { ... } one can: uint16_t param; ... param = 34; post parameterTask(); ... task void parameterTask() { // use param }

这二个模型代码的主要区别是如果组件发布任务二次,将在TaskParameter例子中的使用旧参数,但在basic task例子中使用新参数。若组件想用最先的参数(oldest)可用如下方式: if (post myTask() == SUCCESS) { param = 34; }

4、tinyos2.x调度

调度器是TinyOS组件。每个调度器必须支持nesC任务。也可支持多个传统任务接口。调度器组件可协调调度各种类型任务如最早截止时间型任务、具有优先级任务。

Tinyos2.x普通的任务是不带参数的FIFO任务。任务执行和发布仍支持nesC,即声明接口和连接到调度器组件都用nesC语言。附录A描述了这些nesC语言缩写配置方式。若调度器提供的任务接口是带参数的,则每个任务连接到接口时,需要一个unique()函数来获取惟一的标识符,调度器通过标识符来调度任务。 module SchedulerBasicP { provides interface Scheduler; provides interface TaskBasic[uint8_t taskID]; uses interface McuSleep; } 调度器

必须提供带参数的TaskBasic接口,调用TaskBasic.postTask()返回成功时,调度器终会有任务运行,而不必关心饥饿没任务运行。当是第一次调用TaskBasic.postTask()时,调度器必需返回SUCCESS到TaskBasic.postTask(),若不是第一次则勿须返回,因为已经有信号通知过TaskBasic.runTask()事件了。McuSleep接口用于微处理器能源管理。其工作方式在TEP112有详细说明。

调度器必须提供调度接口。调度接口Scheduler inerface中有用于初始化和运行任务的命令集。

interface Scheduler { command void init(); command bool runNextTask(bool sleep); command void taskLoop(); }

init()命令函数用于初始化任务队列和调度器数据结构。runNextTask()须一次执行完成,不管理调度器所调用的下一个任务是何任务:返回值表明是否在运行任务。bool sleep参数表明当没有任务运行时,调度器应该干嘛。当sleep=FALSE,runNextTask()命令函数将立刻返回FALSE值,当sleep=TRUE,runNextTask()命令函数直到有任务执行时才返回,将使CPU进行休眠直到有新任务到达,调用runNextTask(FALSE)可能返回TRUE或FALSE,runNextTask(TRUE)总返回TRUE。taskLoop()命令通知调度器进入无限任务运行循环,当处理器空闲无任务时,MCU进入低功耗状态:没有返回值。

为高效利用电源,调度主要职责是将处理器进入休眠状态sleep,通过调用休眠可提高任务循环效率, TaskBasic interface:

interface TaskBasic { async command error_t postTask(); void event runTask(); } 当组件声明任务时用在nesC中task关键字,也是寓味着使用了TaskBasic接口的实例,任务主体是runTask事件。当组件使用post关键词寓味着调用了postTask命令。每个TaskBasic连接到调度器组件时必须带有参数(惟一标识),这个标识符是从unique函数获取而来,这是"TinySchedulerC.TaskBasic"一大关键含义。

SchedulerBasicP将从unique函数得到的标识符作为任务队列元素。当tinyos通知调度器运行任务时,将取出队列的下个标识,并在调用带参数的TaskBasic接口时使用。

当tinyos默认使用FIFO调度策略时,Tinyos组件不能采取FIFO策略。如果二个任务必须以特定顺序运行,这需在前个任务发布下个任务。

5、替换调度器

Tinyos调度器是以TinySchedulerC.组件实现的。默认的tinyos调度器实现是SchedulerBasicP模块,默认的调度器组件是某一配置(configuration),可连接到SchedulerBasicP。

因特殊应用要替换调度器,开发者需将TinySchedulerC配置放入应用程序目录:这将取代默认。调度器组件提供了配线方法来实现所需的调度方式。所有的调度器实现必须提供带参数的TaskBasic接口,像SchedulerBasicP一样;也要支持nesC中post语句和任务声明,使能tinyos内核操作。总之,Tinyos不要修改内核代码来实现新的调度方法。所有的调度器实现都要提供调度接口。

例如,假设某一调度器提供最早截止时间任务,由TaskEdf接口提供。 interface TaskEdf { async command error_t postTask(uint16_t deadlineMs); event void runTask(); } 这个调度器命名为:SchedulerEdfP,并同时提供TaskBasic 和TaskEdf二个接口。 module SchedulerEdfP { provides interface Scheduler; provides interface TaskBasic[uint8_t taskID]; provides interface TaskEdf[uint8_t taskID]; } 应用程序想将SchedulerEdfP代替SchedulerBasicP及TinySchedulerC,配置,可以输出所有的SchedulerEdfP接口。 configuration TinySchedulerC { provides interface Scheduler; provides interface TaskBasic[uint8_t taskID]; provides interface TaskEdf[uint8_t taskID]; } implementation { components SchedulerEdfP; Scheduler = SchedulerEdf; TaskBasic = SchedulerEdfP; TaskEDF = SchedulerEdfP; }

当模块有最早截止时间任务时,需使用TaskEdf接口,其配置需连接到TinySchedulerC。重要的是取得任务惟一标识符是语句“TinySched-ulerC.TaskInterface”, TaskInterface是由调度器提供的一个新任务接口。通常为使字符一致,使用#define。例如:TaskEDF.nc可能包括:

  1. define UQ_TASK_EDF "TinySchedulerC.TaskEdf"

下面这个例子包含二个EDF任务: configuration SomethingP { ... } implementation { components SomethingP, TinySchedulerC; SomethingP.SendTask -> TinySchedulerC.TaskEdf[unique(UQ_TASK_EDF)]; SomethingP.SenseTask -> TinySchedulerC.TaskEdf[unique(UQ_TASK_EDF)]; }

模块SomethingP也有普通任务。nesC编译器自动将task关键字转换成BasicTask interfaces并完成相关配线连接wire。因此,作为普通任务,组件用户可以使用task和 post关键字,也可使用BasicTask interface。只要有可能组件应该使用关键字,但不能在given task中混淆这二个语句。(???) 在普通任务中使用关键字实现SomethingP实例: module SomethingP { uses interface TaskEdf as SendTask uses interface TaskEdf as SenseTask } implementation { // The TaskBasic, written with keywords task void cleanupTask() { ... some logic ... } event void SendTask.runTask() { ... some logic ... } event void SenseTask.runTask() { ... some logic ... } void internal_function() { call SenseTask.postTask(20); call SendTask.postTask(100); post cleanupTask(); } }

为保证普通任务不会处于饥饿状态,调度器支持EDF任务必须保证普通任务最终能运行,虽然有无数截止时间短的任务执行。最终到底是多久很难确定,近似约为MCU周期的1%。 如果调度器提供同一任务的二个实例,通过as声明了二个不同的接口名。例如: 调度器提供了二个TaskBasic实例:标准tasks及高优先级任务。调度器通常选择高优先级队列中的任务,再选择标准任务队列中任务。 configuration TinySchedulerC { provides interface Scheduler; provides interface TaskBasic[uint8_t taskID]; provides interface TaskBasic[uint8_t taskID] as TaskHighPriority; } 不能总是选择优先级高的任务因为这将导致普通任务饥饿得不到运行。组件使用优先级高的任务需通过关键字 “TinySchedulerC.TaskHighPriority” 连接TaskHighPriority。 configuration SomethingElseC {} implementation { components TinySchedulerC as Sched, SomethingElseP; SomethingElseP.RetransmitTask - > Sched.TaskHighPriority[unique("TinySchedulerC.TaskHighPriority")]; }

6、实现方法

调度相关文件有: SchedulerBasicP.nc:主要的tinyos调度器,提供带参数的TaskBasic接口 TinySchedulerC.nc:默认的调度配置即将SchedulerBasicP->McuSleepC 调度器原形是支持EDF任务Earliest DeadlineFirst

7、作者联系方式

8、引用

附件A:改变调度方法 nesC编译器可改变nesC接口中的post任务和任务关键字、连接和调用。 module a { ... } implementation { task x() { ... post x(); } } is e?ectively: module a { ... provides interface TaskBasic as x; } implementation { event void x.runTask() { ... call x.postTask(); } } -fnesc-scheduler=TinySchedulerC,TinySchedulerC.TaskBasic,TaskBasic,TaskBasic,runTask,postTask 因任务转化成接口是在nesC编译器中建立的。每个平台的.platform文件中通过选项-fnesc-scheduler可修改具体名称。

6个词汇解释: 1) 调度组件接口名:TinySchedulerC 2) 调度组件中带参数接口(带惟一字符):TinySchedulerC.TaskBasic 3) 调度组件接口名:TaskBasic 4) 接口类型名:TaskBasic 5) 运行任务事件函数名:runTask 6) 发布任务命令函数名:postTask

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多