任务切换是多任务处理的全部内容。然而,由于以下原因,太多可能会破坏:
它增加了 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 标志,所以它在开始运行时将被锁定,因此在它停止或自行解锁之前是不可抢占的。这类任务非常适合更改关键控制结构之类的事情,因此您不希望它们被抢占。如果这样的任务要等待某个东西,它就会失去它的锁,从而变成可抢占的。
在许多情况下,优先级提升对于提高系统性能很有用。然而,更简单的方法,如任务锁定、降低优先级、循环调度和使用非抢占式任务在某些情况下也很有效。
审核编辑:郭婷
全部0条评论
快来发表一下你的评论吧 !