FreeRTOS
中提供的任务调度器是基于优先级的全抢占式调度:在系统中除了中断处理函数、调度器上锁部分的代码和禁止中断的代码是不可抢占的之外,系统的其他部分都是可以抢占的。
系统理论上可以支持无数个优先级(0 ~ N
),优先级数值越小的任务优先级越低,0
为最低优先级,分配给空闲任务使用,一般不建议用户来使用这个优先级。假如使能了 configUSE_PORT_OPTIMISED_TASK_SELECTION
这个宏(在 FreeRTOSConfig.h
文件中定义),一般强制限定最大可用优先级数目为32。在一些资源比较紧张的系统中,可以根据实际情况选择只支持 8
个或 32
个优先级的系统配置。在系统中,当有比当前任务优先级更高的任务就绪时,当前任务将立刻被换出,高优先级任务抢占处理器运行。
一个操作系统如果只是具备了高优先级任务能够“立即”获得处理器并得到执行的特点,那么它仍然不算是实时操作系统。因为这个查找最高优先级任务的过程决定了调度时间是否具有确定性,例如一个包含 n
个就绪任务的系统中,如果仅仅从头找到尾,那么这个时间将直接和 n
相关,而下一个就绪任务抉择时间的长短将会极大的影响系统的实时性。
FreeRTOS
内核中采用两种方法寻找最高优先级的任务,第一种是通用的方法,在就绪链表中查找从高优先级往低查找 uxTopPriority
,因为在创建任务的时候已经将优先级进行排序,查找到的第一个 uxTopPriority
就是我们需要的任务,然后通过 uxTopPriority
获取对应的任务控制块。
第二种方法则是特殊方法,利用计算前导零指令 CLZ
,直接在 uxTopReadyPriority
这个 32
位的变量中直接得出 uxTopPriority
,这样子就知道哪一个优先级任务能够运行,这种调度算法比普通方法更快捷,但受限于平台(在 STM32
中我们就使用这种方法)。
FreeRTOS
内核中也允许创建相同优先级的任务。相同优先级的任务采用时间片轮转方式进行调度(也就是通常说的分时调度器),时间片轮转调度仅在当前系统中无更高优先级就绪任务存在的情况下才有效。为了保证系统的实时性,系统尽最大可能地保证高优先级的任务得以运行。
任务调度的原则是一旦任务状态发生了改变,并且当前运行的任务优先级小于优先级队列组中任务最高优先级时,立刻进行任务切换(除非当前系统处于中断处理程序中或禁止任务切换的状态)。
FreeRTOS
系统中的每一任务都有多种运行状态。系统初始化完成后,创建的任务就可以在系统中竞争一定的资源,由内核进行调度。
任务状态通常分为以下四种:
Ready
):该任务在就绪列表中,就绪的任务已经具备执行的能力,只等待调度器进行调度,新创建的任务会被初始化为就绪态。Running
):该状态表明任务正在执行,此时它占用处理器,FreeRTOS
调度器选择运行的永远是处于最高优先级的就绪态任务,当任务被运行的一刻,它的任务状态就变成了运行态。Blocked
):如果任务当前正在等待某个时序或外部中断,那么这个任务就处于阻塞状态,该任务不在就绪列表中。包含任务被挂起、任务被延时、任务正在等待信号量、读写队列或者等待读写事件等。Suspended
):处于挂起态的任务对调度器而言是不可见的,让一个任务进入挂起状态的唯一办法就是调用 vTaskSuspend()
函数;而把一个挂起状态的任务恢复的唯一途径就是调用 vTaskResume()
或 vTaskResumeFromISR()
函数。挂起态与阻塞态的区别,当任务有较长的时间不允许运行的时候,可以挂起任务,这样调度器就不会管这个任务的任何信息,直到调用恢复任务的 API
函数;而任务处于阻塞态的时候,系统还需要判断阻塞态的任务是否超时,是否可以解除阻塞。
我们知道 FreeRTOS
系统中的每一个任务都有多种运行状态,下面来看一下状态之间的转换关系,任务迁移图如下图所示:
vTaskSuspend()
函数都可以将处于任何状态的任务挂起,被挂起的任务得不到 CPU 的使用权,也不会参与调度,除非它从挂起态中解除。vTaskResume()
或 vTaskResumeFromISR()
函数,如果此时被恢复任务的优先级高于正在运行任务的优先级,则会发生任务切换,将该任务将再次转换任务状态,由就绪态变成运行态。作为一个嵌入式开发人员,要对自己设计的嵌入式系统要了如指掌,任务的优先级信息,任务与中断的处理,任务的运行时间、逻辑、状态等都要知道,才能设计出好的系统,所以,在设计的时候需要根据需求制定框架。在设计之初就应该考虑下面几点因素:任务运行的上下文环境、任务的执行时间合理设计。
FreeRTOS
中程序运行的上下文包括:
中断服务函数是一种需要特别注意的上下文环境,它运行在非任务的执行环境下(一般为芯片的一种特殊运行模式(也被称作特权模式))。在这个上下文环境中不能使用挂起当前任务的操作,不允许调用任何会阻塞运行的 API
函数接口。
另外需要注意的是,中断服务程序最好保持精简短小,快进快出,一般在中断服务函数中只做标记事件的发生,然后通知任务,让对应任务去执行相关处理,因为中断服务函数的优先级高于任何优先级的任务,如果中断处理时间过长,将会导致整个系统的任务无法正常运行。所以在设计的时候必须考虑中断的频率、中断的处理时间等重要因素,以便配合对应中断处理任务的工作。
任务看似没有什么限制程序执行的因素,似乎所有的操作都可以执行。但是做为一个优先级明确的实时系统,如果一个任务中的程序出现了死循环操作(此处的死循环是指没有阻塞机制的任务循环体),那么比这个任务优先级低的任务都将无法执行,当然也包括了空闲任务,因为死循环的时候,任务不会主动让出 CPU
,低优先级的任务是不可能得到 CPU
的使用权的,而高优先级的任务就可以抢占 CPU
。这个情况在实时操作系统中是必须注意的一点,所以在任务中不允许出现死循环。
如果一个任务只有就绪态而无阻塞态,势必会影响到其他低优先级任务的执行,所以在进行任务设计时,就应该保证任务在不活跃的时候,任务可以进入阻塞态以交出 CPU
使用权,这就需要我们明确知道什么情况下让任务进入阻塞态,保证低优先级任务可以正常运行。在实际设计中,一般会将紧急的处理事件的任务优先级设置得高一些。
空闲任务(idle 任务)是 FreeRTOS
系统中没有其他工作进行时自动进入的系统任务。
因为处理器总是需要代码来执行,所以至少要有一个任务处于运行态。FreeRTOS
为了保证这一点,当调用 vTaskStartScheduler()
时,调度器会自动创建一个空闲任务,空闲任务是一个非常短小的循环。
用户可以通过空闲任务钩子方式,在空闲任务上钩入自己的功能函数。通常这个空闲任务钩子能够完成一些额外的特殊功能,例如系统运行状态的指示,系统省电模式等。除了空闲任务钩子,FreeRTOS
系统还把空闲任务用于一些其他的功能,比如当系统删除一个任务或一个动态任务运行结束时,在执行删除任务的时候,并不会释放任务的内存空间,只会将任务添加到结束列表中,真正的系统资源回收工作在空闲任务完成,空闲任务是唯一一个不允许出现阻塞情况的任务,因为 FreeRTOS
需要保证系统永远都有一个可运行的任务。
对于空闲任务钩子上挂接的空闲钩子函数,它应该满足以下的条件:
任务的执行时间一般是指两个方面,一是任务从开始到结束的时间,二是任务的周期。
在系统设计的时候这两个时间都需要考虑。例如,事件 A
对应的服务任务 Ta
,系统要求的实时响应指标是 10ms
,而 Ta
的最大运行时间是 1ms
,那么 10ms
就是任务 Ta
的周期了,1ms
则是任务的运行时间。简单来说任务 Ta
在 10ms
内完成对事件 A
的响应即可。
如果此时系统中还存在着以 50ms
为周期的另一任务 Tb
,它每次运行的最大时间长度是 100us
。在这种情况下,即使把任务 Tb
的优先级抬到比 Ta
更高的位置,对系统的实时性指标也没什么影响,因为即使在 Ta
的运行过程中,Tb
抢占了 Ta
的资源,等到 Tb
执行完毕,消耗的时间也只不过是100us
,还是在事件 A
规定的响应时间内(10ms)
,Ta
能够安全完成对事件A
的响应。但是假如系统中还存在任务 Tc
,其运行时间为 20ms
,假如将Tc
的优先级设置比 Ta
更高,那么在 Ta
运行的时候,突然间被 Tc
打断,等到 Tc
执行完毕,那 Ta
已经错过对事件 A(10ms)
的响应了,这是不允许的。
因此,在我们设计的时候,必须考虑任务的时间,一般来说处理时间更短的任务优先级应设置更高一些。