减少RTOS中的任务切换

描述

  任务切换是多任务处理的全部内容。然而,由于以下原因,太多可能会破坏:

  它增加了 RTOS 开销

  它增加了功耗

  它降低了响应能力,尤其是低优先级任务

  它增加了调试的复杂性

  它可能会导致错误

  它可以抑制未来的扩张

  因为一个好的 RTOS 在低端 50 MHz Cortex-M3 上每秒可以实现大约 50,000 次任务切换,所以第一个问题对于大多数系统来说可能并不重要。然而,对于电池供电的设备,即使是很小的效率改进也很重要。设备用户可能会注意到低优先级人机界面任务的响应速度降低。不必要的任务切换会混淆已经很复杂的调试过程,因此如果没有其他原因的话,也是不可取的。

  第五个原因尤为重要。如果您没有意识到可能会发生任务切换,那么事情可能不会总是按照预期的顺序发生。这可能会导致难以找到的零星错误。最后,随着新功能的添加,空闲时间可能会变得很宝贵,尤其是在处理器性能刚开始的时候。有充分的理由减少过多的任务切换。让我们看几个。

  优先提升

  一种流行的方法是在选择运行时自动提高任务的优先级。这在以下示例中显示:

  void sys_init(void) {

  TCB_PTR t2a; // task a of priority 2

  t2a = smx_TaskCreate(t2a_main, PRI_2, 500, SMX_FL_NONE, “t2a”);

  smx_TaskHook(t2a, t2a_entry, t2a_exit);

  smx_TaskStart(t2a);

  }

  void t2a_entry(void) {

  smx_TaskBump(self, 3);

  }

  void t2a_exit(void) {

  smx_TaskBump(self, 2);

  }

  void t2a_main(void) {

  while (1) {

  // perform t2a function at priority 3

  }

  }

  在此示例中,t2a_enter() 由调度程序透明地调用,在 t2a 恢复之前,并且 t2a_exit() 在 t2a 暂停之后类似地调用。t2a_enter() 将 t2a 移动到就绪队列级别 3 的前面并将其优先级提高到 3。t2a_exit() 将 t2a 移动到就绪队列级别 2 的末尾并将其优先级降低到 2。因此,t2a 不能被 a 抢占运行时的优先级 3 任务。但是具有更高优先级的任务可以抢占 t2a。在这种情况下,t2a 将回到优先级 2,从而允许任何等待优先级 3 的任务接下来运行。

  这种方法很有吸引力,因为一旦 t2a 开始运行,就应该允许它结束。但是,您可能想知道为什么不将 t2a 列为优先级 3 的任务。优先级提升的优点是所有优先级为 3 的任务将在 t2a 允许开始之前运行。然后它成为其中之一。这在三种情况下是有意义的:

  t2a 是一个简短的任务

  t2a 是一个重型切换器

  t2a 是超级用户

  如果 t2a 是一项很长的任务,那么将其提升到优先级 3 并不是一个好主意,因为它会影响真正的优先级 3 任务的响应能力。所以,t2a 应该很短。

  重型切换器的一个例子是浮点任务,与定点任务相比,它必须保存和恢复额外的 32 个寄存器。另一个例子是通过缓存从慢速内存执行的任务。然后,由于新任务使用不同的代码和变量,重度切换表现为缓存未命中和重新加载。即使在缓存环境中,提升的任务也应该很短。

  如果 t2a 以比其他任务更高的功率级别运行(例如,它在运行时打开外围设备),则显然将其运行时间最小化是有益的,假设节能很重要。这是优先级提升的一个很好的用途。

  除了优先级反转之外,优先级提升的另一个缺点是它增加了使用它的任务的切换开销。对于上面的示例,任务切换从每秒 50,000 次下降到 26,000 次,这不是问题。

  任务锁定

  任务锁定是防止不必要的任务切换的另一种方法。以下示例说明了这一点:

  TCB_PTR t2a, t3a; // tasks

  SCB_PTR sa; // semaphore

  void t3a-main(void) {

  smx_SemTest(sa);

  // do something

  }

  void t2a-main(void) {

  smx_SemSignal(sa);

  }

  这个例子除了说明隐藏的任务切换之外没有任何用处。由于 t3a 具有更高的优先级,它首先运行,然后等待信号量,sa。然后 t2a 运行并向 sa 发出信号。t2a 并没有像预期的那样停止,而是立即被 t3a 抢占,它做某事,然后停止。然后,t2a 再次运行,除了停止之外什么都不做。您现在可以看到这是一个浪费的任务切换。可以而且应该通过以下方式加以预防:

  void t2a-main(void) {

  smx_TaskLock();

  smx_SemSignal(sa);

  }

  现在,在 t2a 停止并释放其锁定之前,t3a 无法抢占。请注意,这实际上是调度程序锁定,但我更喜欢称它为任务锁定,因为这更能描述它的作用。

  更少的优先级

  如果你发现你的系统做了太多的任务切换,最好简单地减少优先级的数量。为此,您显然需要一个允许任务共享优先级的 RTOS。假设您有这样的 RTOS,下一步是重新考虑相关的任务优先级。

  当一项任务一直在等待时,它的紧迫性就会增加。降低优先级意味着可能已经抢占它的任务,从而使其等待更长时间,现在将在它之后运行。较低的优先级实际上可以实现更流畅的操作,以及减少不必要的任务切换。

  试图过度控制活动而不是让它们自然发生是一种常见的冲动。不太有力的控制可能更能适应不可预见的情况,因此可能会产生更坚固的解决方案。值得一试。

  循环调度

  在降低优先级的过程中,引入相同优先级的任务的循环调度可能会有所帮助。这可以按如下方式完成:

  void t2a_main(void) {

  while (smx_TestSem(sa)) {

  // do function a

  smx_TaskBump(self, NO_PRI_CHG);

  }

  }

  void t2b_main(void) {

  while (smx_TestSem(sb)) {

  // do function b

  smx_TaskBump(self, NO_PRI_CHG);

  }

  }

  当每个任务完成一些工作时,它会将自己撞到就绪队列级别 2 的末尾,以便其他任务可以运行,如果它有工作的话。如果两个任务都没有工作,则两者都将在各自的信号量上挂起,并且可以运行优先级较低的任务。当然,更高优先级的任务可以随时抢占 t2a 和 t2b。在这个例子中,这两个任务可能大部分时间都在等待工作。但是当他们的工作量增加时,他们会交替工作,从而公平地关注所有客户。

  不可抢占的任务

  通常一次性任务(没有像普通任务那样的内部循环)非常短,使它们不可抢占是有意义的,如下所示:

  TCB_PTR t2a;

  t2a = smx_TaskCreate(t2a_main, PRI_2, 0, SMX_FL_LOCK, “t2a”);

  smx_TaskStart(t2a);

  void t2a_main(void) {

  // do something simple and stop

  }

  因为 t2a 是在创建时设置了它的 start-locked 标志,所以它在开始运行时将被锁定,因此在它停止或自行解锁之前是不可抢占的。这类任务非常适合更改关键控制结构之类的事情,因此您不希望它们被抢占。如果这样的任务要等待某个东西,它就会失去它的锁,从而变成可抢占的。

  在许多情况下,优先级提升对于提高系统性能很有用。然而,更简单的方法,如任务锁定、降低优先级、循环调度和使用非抢占式任务在某些情况下也很有效。

  审核编辑:郭婷

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分