1. 简介
任务管理(或称进程管理)是所有操作系统内核的最基本组成模块之一,FreeRTOS也不例外。想要了解一个操作系统,不得不理解其任务管理的设计和实现。任务管理的介绍由两篇文章组成,第一篇先介绍了FreeRTOS的任务管理的重要概念和外部特性以及相关联的重要实现,第二篇介绍任务管理实现的细节(关键数据结构和内部函数的实现)。
温馨提示:
- 由于文章较长,可当作工具文使用,即仅挑选感兴趣的部分阅读;
- 为了解释FreeRTOS系统调用的行为,文中难免会涉及一些操作系统原理、ARM体系结构相关的概念,请读者自行查阅资料。当然,若不关心内核实现,可自行跳过。
在FreeRTOS中,可能是为了凸显出其与进程和线程的区别,它使用了任务(Task)这个概念作为它的一个最基础的调度单元。FreeRTOS的任务与进程最大的区别是,它并没有实现内存地址空间隔离,所有的任务都共享相同的内存地址空间。通常,这也是RTOS与普通的通用操作系统的重要差异之处。下面先给出一张图,概述了FreeRTOS中任务管理的设计思路。这里只是先建立一个总体的印象,具体的细节会在后面的小节逐步展开。
2. 任务函数(任务入口)
任务函数(又称任务入口)指的是一个任务实际执行的函数体。它是该任务运行时实际执行的内容。类似于Linux的线程函数,每个任务一定有一个对应的任务函数。
Task函数原型:
typedef void (*TaskFunction_t)(void *);
#define portTASK_FUNCTION_PROTO( vFunction, pvParameters ) void vFunction( void *pvParameters )
这里使用了宏的形式进行定义,目的是为了支持某些体系结构需要增加特定的属性信息。
Task函数定义模板:
void TaskName_Entry(void *pvParameters)
{
vTaskDelete(NULL);
}
我们将希望该任务执行的内容在任务函数中实现,并在任务函数退出之前,必须进行任务的自我销毁,否则会使得系统崩溃,详情可参看创建任务。而vTaskDelete()
便是完成销毁的接口,这将在删除任务部分介绍。
3. 任务状态
FreeRTOS的任务状态机如下图所示:
FreeRTOS在实现任务状态时,每个状态都对应着一个(或多个)任务队列,详情可参看任务队列小节。
下面将分别介绍这几个状态的含义。
3.1. Running State
即任务正在运行,由于目前官方的FreeRTOS发行版不支持多核,因此任何时候,只有一个任务处于Running状态。
3.2. Blocked State
正在等待事件的发生。或者主动进入睡眠。
在FreeRTOS中,等待事件时还可以设定等待时限,此时处于Blocked的任务会被同时放入等待事件的任务队列和Delayed任务队列。
3.3. Suspended State
挂起状态,通常情况不会使用该状态,这是一种不能通过任何外部事件唤醒的状态,只能通过vTaskSuspend()
进入,vTaskResume()
退出。所以,尽管任务可能无时限等待某个事件的发生,但不能认为此时是Suspended状态。
3.4. Ready State
可以被调度的状态。需要注意的是,FreeRTOS的任务被创建时就是Ready状态。
3.5. Deleted State
任务已经被删除,但TCB和任务栈所占用的内存资源未被回收,类似于Linux的僵尸进程。最终会由系统服务完成资源回收。
4. 创建任务
通常,在使用FreeRTOS时,我们都会在运行时动态创建任务。
动态创建任务的函数原型:需要开启configSUPPORT_DYNAMIC_ALLOCATION = 1
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
const char * const pcName,
const uint16_t usStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask ) PRIVILEGED_FUNCTION;
当创建任务的时候,FreeRTOS会把新创建的任务直接放入到Ready队列。
每当创建一个任务,FreeRTOS会为其分配两片内存区域,一个用于存储任务的数据结构,也即TCB;而另一片则作为该任务的栈。与Linux的进程不同,这两片内存区域是未受到任何保护的,也即可被其他任意任务访问。
需要注意的是,usStackDepth
指的是栈的深度,具体实现为可存储的BaseType_t
变量的数量,而不是字节数。FreeRTOSv10后,usStackDepth
的数据类型由configSTACK_DEPTH_TYPE
来表示,以支持更大的栈大小。
pvParameters
是为任务函数所用的参数,由任务的创建者传入,最终给到任务入口函数使用。
uxPriority
申明了任务的基础优先级。如果开启了MPU功能,则可附加portPRIVILEGE_BIT
位用于标识这是一个系统级的任务(或称为系统服务)。
pxCreatedTask
用于接收新创建任务的任务句柄。
下面详细介绍一下任务创建的实现(非MPU实现)。因为任务创建的过程涉及到了一些比较重要的概念和行为,因此在介绍实现之前,需要先介绍这几个关键的概念和行为。
关键的概念:
- 任务的控制块(TCB):记录一个任务的所有元信息,详情可查看进程控制块。
- 任务的上下文:指一个任务运行时所需要的所有状态信息,例如CPU寄存器(通用寄存器、浮点寄存器等)、FreeRTOS的相关全局信息(临界区深度、FPU标志等)。在任务切出时,这些信息会保存在任务的栈中;在任务切入时,会将信息恢复到对应的寄存器或全局变量中。详情可查看任务切换小节。
关键的行为:
- 临界区的进入与退出(
taskENTER_CRITICAL()
/taskENTER_CRITICAL_FROM_ISR()
和taskEXIT_CRITICAL()
/taskEXIT_CRITICAL_FROM_ISR()
):临界区的进入和退出是与体系结构强相关的行为。临界区内,屏蔽所有低优先级中断。详情可参看临界区和挂起调度器。 - 任务栈的初始化(
StackType_t *pxPortInitialiseStask( StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters )
):使用初始信息填充任务的上下文,使得它就像是被调度器切换出去一样。在ARMv7的Port中,该实现需要满足AAPCS。另外,从其具体的实现就可以看出,在ARMv7体系结构中,FreeRTOS的任务是运行在系统模式下的。
其中: - 初始的CPSR设置为
portINITIAL_SPS
。这个宏在ARMCA9的port实现为0x1f
,具体含义则是:处理器模式为System Mode、使用ARM指令集运行模式、IRQ和FIQ都处于开启状态。 - 将R14设置为
portTASK_RETURN_ADDRESS
,通常是NULL
。因为任务的初始状态调用栈是空的。
下面将详细介绍创建Task的实现。先来看一张相对宏观的活动图:
-
申请TCB和任务栈所需的内存空间(通过调用pvPortMalloc()
接口完成):如果申请内存失败,则直接返回errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY
。
-
初始化TCB的内容和初始化任务栈(static void prvInitialiseNewTask( TaskFunction_t pxTaskCode, const char * const pcName, const uint32_t ulStackDepth, void * const pvParameters, UBaseType_t uxPriority, TaskHandle_t * const pxCreatedTask, TCB_t * pxNewTCB, const MemoryRegion_t * const xRegions )
):
初始化TCB的流程:(初始化任务栈参看前面的介绍[任务栈的初始化])
-
将新申请的任务加入Ready任务队列(static void prvAddNewTaskToReadyList( TCB_t *pxNewTCB
)。如果该任务是第一个申请的任务,则还需要初始化所有任务队列。其活动图如下:
(在v10中引入)除了动态创建任务之外,FreeRTOS还支持静态创建方式,函数原型如下:需要开启configSUPPORT_STATIC_ALLOCATION = 1
TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode,
const char * const pcName,
const uint32_t ulStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
StackType_t * const puxStackBuffer,
StaticTask_t * const pxTaskBuffer ) PRIVILEGED_FUNCTION;
特别地,为了实现任务栈内存的保护机制,FreeRTOS还提供了MPU版本的任务创建。函数原型如下:需要开启portUSING_MPU_WRAPPERS = 1
BaseType_t xTaskCreateRestricted( const TaskParameters_t * const pxTaskDefinition, TaskHandle_t *pxCreatedTask ) PRIVILEGED_FUNCTION;
这里需要用到任务参数结构体(TaskParameters_t
),用于申明该任务所使用的MPU内存区。
另外,FreeRTOS还为MPU的任务提供了类似于Linux虚拟内存的方法。函数原型如下:
void vTaskAllocateMPURegions( TaskHandle_t xTask, const MemoryRegion_t * const pxRegions ) PRIVILEGED_FUNCTION;
内存区域结构体参数(pxRegions
)申明了需要重新映射的目标内存区域。
5. 任务优先级
创建任务时,需要设置优先级。该优先级还可以在运行时进行更改。FreeRTOS的优先级数值与Linux的相反,即值越大,优先级越高。
在FreeRTOS的调度策略中,总是确保最高优先级的任务被调度,相同优先级则使用时间片轮转调度。
FreeRTOS在实现任务队列时,Ready任务队列是由多个优先级队列构成的,即一个优先级对应一个队列。因此最大优先级应该尽可能的小,即configMAX_PRIORITIES
应该尽可能的小,防止内存被过度占用。详情可参看任务队列小节。
此外,为了实现互斥锁中的优先级继承机制,FreeRTOS需要记录任务实际继承的优先级(实际优先级),以及任务的原始优先级(Base优先级)。
在ARMCA9的实现中,使用优先级位图的概念记录了系统当前所有任务的优先级。详情可参看任务管理的私有宏函数。
6. 系统中的特殊任务(或称系统服务)
6.1. Idle Task
Idle任务在开启任务调度器的时候被创建,它的优先级被设置为tskIDLE_PRIORITY
,通常是最低优先级。其主要职责如下:
- 确保总有一个任务在执行;
- 回收僵尸任务占用的系统资源;
- 实现系统级低功耗功能。
其函数原型如下:
#define portTASK_FUNCTION_PROTO( prvIdleTask, pvParameters );
它的函数体是一个永无止境的循环。在查看其具体实现之前,先来看看一个用于实现低功耗功能的关键行为:
-
计算预期的Idle Ticks(static TickType_t prvGetExpectedIdleTime( void )
):
{
初始化`uxHigherPriorityReadyTasks = pdFALSE`,`uxLeastSignificantBit = 0x01`。
如果当前系统等待调度的最高优先级是否大于Idle任务的优先级(`uxTopReadyPriority > uxLeastSignificantBit`),则设置局部调度Flag`uxHigherPriorityReadyTasks = pdTRUE`。
如果系统当前的任务优先级大于Idle的优先级,或存在与Idle优先级相同的任务,或局部调度Flag不为`pdFALSE`,则表明系统当前不能进入休眠,设置返回值`xReturn = 0`;
否则,进入休眠的时间为`xReturn = xNextTaskUnblockTime - xTickCount`。
返回预期的Idle时间`xReturn`。
}
下面便来看看在Idle任务中具体做了什么。宏观上,Idle任务的任务函数的活动图如下:
-
回收处于Deleted状态的任务的资源(prvCheckTasksWaitingTermination()
):
-
(仅开启USE_PREEMPTION && IDLE_SHOULD_YIELD)若存在其他相同优先级的任务(listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ tskIDLE_PRIORITY ] ) ) > 1
),主动发起一次调度(taskYIELD()
),即触发一次软件中断(SWI 0
)。
-
(仅开启IDLE_HOOK)调用vApplicationIdleHook()
。
-
(仅开启TICKLESS_IDLE)进入系统低功耗模式:
在实现portSUPPESS_TICKS_AND_SLEEP()
时,需要确保系统会被外部中断唤醒,否则会导致系统漏掉重要的外部中断信号。
在FreeRTOSv10后,支持设置Idle任务的名字。
6.1.1. Idle Application Hook
Idle Application Hook是一个钩子函数,为程序员提供了在Idle上下文中调用的钩子,它可以实现以下内容:
- 执行低优先级、后台、或需要持续处理的函数;
- 度量空闲的处理能力;
- 将处理器进入低功耗模式。
因为这个钩子运行在Idle上下文,所以FreeRTOS对其实现有一定的限制:即不能进入Blocked和Suspended状态,防止系统中没有任务可被调度。
6.2. Timer Task
主要职责:实现FreeRTOS的系统级软件Timer。
创建时机:在开启调度器的时候创建。
这个任务还会在后续的FreeRTOS学习系列笔记-软件定时器章节中进行详细介绍。
7. 任务调度
7.1. 调度方法
7.1.1. 调度方法配置
相关配置:
configUSE_PREEMPTION
configUSE_TIME_SLICING
configUSE_TICKLESS_IDLE
:用于低功耗模式
具体用法在几种调度方法中介绍。
7.1.2. 几种调度方法
7.1.2.1. Prioritized Pre-emptive Scheduling with Time Slicing(最常用)
配置:configUSE_PREEMPTION = 1
&& configUSE_TIME_SLICING = 1
特点:
- 优先调度高优先级任务;
- 可抢占;
- 相同优先级的任务则采用时间片轮转调度。
7.1.2.2. Prioritized Pre-emptive Scheduling (without Time Slicing)
配置:configUSE_PREEMPTION = 1
&& configUSE_TIME_SLICING = 0
特点:
- 优先调度高优先级任务;
- 可抢占;
- 相同优先级任务需要等待Running的任务主动放弃CPU才能触发调度。
7.1.2.3. Co-operative Scheduling
配置:configUSE_PREEMPTION = 0
&& configUSE_TIME_SLICING = 任意
特点:
- 优先调度高优先级任务;
- 非抢占,即需要Runing的任务主动放弃CPU才可触发调度。
7.2. 任务切换
在FreeRTOS中,支持多种触发任务调度的方式:
- Tick中断周期性触发:顾名思义,每当产生Tick中断时,触发一次任务的调度,以实现时间片轮转。
- 由某些系统调用而间接引发的调度:某些系统调用会改变任务的状态,比如由Running状态转换为Delayed,那这种情况必然会引发一次任务的调度。
- 由中断间接引发的调度:中断的产生使得任务被唤醒,进而触发一次调度。详情可参看中断处理流程小节。
无论是哪种触发方式,如果在本次调度中发现需要进行任务切换,那这个任务切换的过程是一致的,所以,先介绍一下任务切换的过程。
在任务切换过程中,最重要的就是需要将当前切出的任务的上下文进行保存(寄存器->栈),并将切入的任务的上下文进行恢复(栈->寄存器)。那么,任务的上下文就是这个过程中最重要的概念。
任务的上下文:指一个任务运行时所需要的所有状态信息,例如CPU寄存器(通用寄存器、浮点寄存器等),FreeRTOS的相关全局信息(临界区深度、FPU标志等)。在ARMv7体系结构中,FreeRTOS的任务上下文在任务栈中的内存分布模型如下(假设:栈的上顶端为0xFFFFFFF0
,栈的格式符合AAPCS,且使用了浮点寄存器):
地址 | 内容 | 备注 |
---|
0xFFFFFFFC | SPSR_<p_mode> | 在切换为系统模式之前的模式的SPSR,即异常返回时的CPSR |
0xFFFFFFF8 | R14_<p_mode> | 在切换为系统模式之前的模式的LR,即异常的返回地址 |
0xFFFFFFF4 | R14 | |
0xFFFFFFF0 | R12 | |
0xFFFFFFEC | R11 | |
… | … | |
0xFFFFFFC4 | R1 | |
0xFFFFFFC0 | R0 | |
0xFFFFFFBC | ulCriticalNesting | |
0xFFFFFFB4 | D15 | |
0xFFFFFFAC | D14 | |
… | … | |
0xFFFFFF44 | D1 | |
0xFFFFFF3C | D0 | |
0xFFFFFF34 | D31 | |
0xFFFFFF2C | D30 | |
… | … | |
0xFFFFFEC4 | D17 | |
0xFFFFFEBC | D16 | |
0xFFFFFEB8 | FPSCR | 浮点状态和控制寄存器 |
0xFFFFFEB4 | ulPortTaskHasFPUContext | 当前SP指向的位置,也是pxCurrentTCB 指向的位置 |
在这个基础上,理解任务的切换过程便简单多了:
-
保存被切出任务的上下文(portSAVE_CONTEXT
):按照上下文在栈中的内存分布模式来填充任务栈。
-
切换当前系统的任务上下文(vTaskSwitchContext( void )
):
{
如果调度器被挂起(`uxSchedulerSuspended != pdFALSE`),则设置调度挂起请求标志后直接返回(`xYieldPending = pdTRUE`)。
否则:
{
清空调度挂起标志(`xYieldPending = pdFALSE`)。
(仅开启GENERATE_RUN_TIME_STATS):
{
更新系统总运行时长(`ulTotalRuntime = portGET_RUN_TIME_COUNTER_VALUE()`)。
如果发现当前系统总运行时长大于任务切入时间(`ulTotalRuntime > ulTaskSwitcedInTime`),则更新该任务运行时计时(`pxCurrentTCB->ulRunTimeCounter += ( ulTotalRunTime - ulTaskSwitchedInTime )`)。
更新任务切入时间(`ulTaskSwitchedInTime = ulTotalRunTime`)。
}
检查任务栈是否存在溢出:即判断栈底是否已经被更改(`taskCHECK_FOR_STACK_OVERFLOW()`)。
}
选择一个优先级最高的任务作为被调度任务(`taskSELECT_HIGHEST_PRIORITY_TASK()`):即选取最高优先级队列中的队列首部的任务。并更新系统当前TCB指针(`pxCurrentTCB`)。
(仅开启USE_NEWLIB_REENTRANT)切换NewLib的任务相关信息。
}
-
恢复切入任务的上下文(portRESTORE_CONTEXT
):
{
第一步便是恢复SP:即将`pxCurrentTCB`的值赋给SP。
如果发现FPU上下文,则恢复FPU寄存器。
恢复临界区深度`ulCriticalNesting`。
恢复中断屏蔽系统寄存器:确保为0xFF或`ulMaxAPIPriorityMask`。
恢复R0-R12,R14。
恢复PC和CPSR(`RFEIA`)。
}
7.3. 调度器
在FreeRTOS中,调度器只是一个逻辑概念,而不是一个相对独立存在的模块,它的实现与任务管理紧耦合。而调度器的开启则意味着FreeRTOS的任务管理功能开始运行,即FreeRTOS将开始接管CPU。此外,调度器还支持挂起(或称加锁)和恢复操作,下面将分小节介绍。
7.3.1. 开启调度器
调度器控制是FreeRTOS最基础的功能,要问基础到啥程度?基础到你不能裁剪它,因为裁了后系统就跑不起来了。
开启调度器函数接口:
void vTaskStartScheduler( void ) PRIVILEGED_FUNCTION;
这是一切的开始,开启FreeRTOS的调度器意味着FreeRTOS内核开始接管整个CPU的控制流。它将会开启FreeRTOS的系统服务,包括:
这个函数不会直接返回。
下面则来看看其实现:宏观的活动图如下:
-
创建Idle Task系统服务:这个服务在Idle Task中已介绍,这是为了让OS始终有任务存在。
-
创建Timer Task系统服务:这个服务在Timer Task中已介绍,是系统级软件Timer的服务进程。
-
FreeRTOSv10后引入,调用freertos_tasks_c_additions_init()
执行用户自定义初始化。
-
初始化系统级变量:代码翻译如下
{
关闭低优先级中断(`portDISABLE_INTERRUPTS()`)。
(仅开启USE_NEWLIB_REENTRANT)设置与任务相关的NewLib结构体。
设置下一个任务唤醒时刻为无穷远。(`xNextTaskUnblockTime = portMAX_DELAY`)
设置调度器的运行状态为Running。(`xSchedulerRunning = pdTRUE`)
初始化系统的Tick为0。(`xTickCount = 0U`)
(仅开启GENERATE_RUN_TIME_STATS)开启运行时统计定时器。
}
-
初始化Tick硬件定时器:因为这个是与平台相关的行为,所以调用平台级接口xPortStartScheduler()
。代码翻译如下
{
检查平台支持的最大中断优先级数量。
Assert当前CPU未运行在用户模式。
Assert中断Binary point寄存器为抢占位。
屏蔽所有中断。(`portCPU_IRQ_DISABLE()`)。
初始化Tick硬件Timer:
{
中断控制器初始化。
定时器中断配置,设置FreeRTOS的Tick中断响应函数`FreeRTOS_Tick_Handler()`。
初始化Tick定时器硬件。
开启Tick定时器。
使能Tick定时器中断。
}
}
-
开始执行第一个任务(void vPortRestoreTaskContext( void )
):即做一次任务切换的任务切入。可以看到,这个函数是不会返回的。
{
切换CPU运行模式为系统模式。
切入第一个任务(portRESTORE_CONTEXT)。
}
在这个过程中,我们可以看到,在ARMv7中,FreeRTOS是运行在系统模式下的。
在FreeRTOSv10后,支持FREERTOS_TASKS_C_ADDITIONS_INIT
宏,如果开启,则会在开启调度器流程的最后调用static void freertos_tasks_c_additions_init( void )
实现一些自定义的初始化流程。
停止任务调度器函数接口:
void vTaskEndScheduler( void ) PRIVILEGED_FUNCTION;
这个接口体现了FreeRTOS的超强可操控性,因为它甚至连调度器都可以关闭。它将完成以下主要的行为:
- 停止Tick中断;
- 删除所有任务;
- 清理所有FreeRTOS申请的资源;
- 跳转到调用
vTaskStartScheduler()
的位置,也即实现了vTaskStartScheduler()
的返回。
需要注意的是,这个退出过程是需要用户自己完成的。在Port layer的vPortEndScheduler()
完成最终的退出行为。
7.3.2. 挂起和恢复调度器
在开启了调度器之后,FreeRTOS便能进行正常的任务调度了。但在许多系统调用中,为了防止某些系统全局信息被篡改,要求调度器暂停工作,并在系统调用完成全局信息访问后恢复,因此,为了更好的理解这些系统调用,还需要详细介绍一下调度器的挂起和恢复。
挂起调度器函数接口:
void vTaskSuspendAll( void ) PRIVILEGED_FUNCTION;
个人愚见, 这个名字取得不太好. 乍一看还以为是与vTaskSuspend()
类似, 用于控制任务的挂起, 其实不然, 它操作的对象实际上是Scheduler.
它要实现的功能是挂起调度器, 但不会禁止外部中断. 那些自私的任务可以通过调用这个接口实现CPU的长期占用. 需要小心的是, 在挂起调度器后不能调用可能导致上下文切换的接口, 例如: vTaskDelayUntil()
, xQueueSend()
等.
其具体的实现很简单:
{
++uxSchedulerSuspended;
}
从代码可以看出,它仅仅是将表示调度器挂起的标志uxSchedulerSuspended
自增1。由于这个标志的类型是BaseType_t
,这个行为是原子性的,所以不需要在临界区内执行该操作。这个标志会在许多系统调用中引用,如果发现不为0(pdFALSE
),则表示调度器被挂起。
当调度器被挂起时,所有的任务切换请求都会被挂起。另外,在调度器挂起的时候,任何中断都不能操作TCB的xStateListItem
、以及xStateListItem
指向的队列。而如果在调度器挂起的时候,确实有任务需要被唤醒,那他会被放到PendingReady任务队列,当调度器被唤醒时才真正地被加入到Ready任务队列中。
恢复调度器函数接口:
BaseType_t xTaskResumeAll( void ) PRIVILEGED_FUNCTION;
和上面的那位一样,名字也取得不好。它要完成的是任务调度器的恢复,需要特别注意的是,它并不会把处于Suspended的任务唤醒。
它的返回值告诉我们该函数内部是否发生了调度,如果发生了则返回pdTRUE
. 这是因为在挂起调度器的这段时间内, 很有可能其他被阻塞的, 更高优先级的任务已经达成了唤醒的条件。
因为恢复调度器的流程涉及到系统下一次唤醒时刻变量的更新,先来看看这个流程:
-
更新下一次系统唤醒时刻变量(statc void prvResetNextTaskUnblockTime( void )
): 代码翻译如下:
{
如果切换后新的Delayed任务队列为空(`listLIST_IS_EMPTY( pxDelayedTaskList != pdFALSE`):则设置下一次唤醒时刻为无限大(`xNextTaskUnblockTime = portMAX_DELAY`)。
否则,即不为空:
{
从Delayed任务队列中获取最近需要唤醒的任务(`pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList )`)。
并将下一次唤醒时刻设置为这个任务的唤醒时刻(`xNextTaskUnblockTime = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) )`)。
}
其具体的实现如下:(代码翻译)
{
初始化`pxTCB = NULL`。
初始化`xAlreadyYielded = pdFALSE`。
Assert `uxSchedulerSuspended != pdTRUE`。
进入临界区(`taskENTER_CRITICAL()`)。
减少调度器挂起标志(`--uxSchedulerSuspended`)。
如果挂起标志递减为0(`uxSchedulerSuspended == pdFALSE`),即调度器挂起深度为0,且当前系统中的任务总数大于0(`uxCurrentNumberOfTasks > 0`):
{
循环将PendingReady任务队列中的任务加入到Ready任务队列(`while ( listLIST_IS_EMPTY( &xPendingReadyList ) == pdFALSE`):
{
从PendingReady任务队列中取一个任务`pxTCB`。
将该任务从事件任务队列和Blocked任务队列中删除。
将该任务加入到Ready任务队列(`prvAddTaskToReadyList( pxTCB )`。
如果该任务的优先级大于系统当前的任务优先级,则设置调度挂起标志`xYieldPending = pdTRUE`。
}
因为在调度器挂起的时候唤醒一个任务时,并没有更新系统下一次唤醒任务的时刻变量,那么现在就需要更新:即如果PendingReady队列不为空(`pxTCB != NULL`),则需要更新下一个唤醒任务的时刻(`prvResetNextTaskUnblockTime()`)。
处理在调度器挂起时流失的系统Tick:
{
为了避免破坏系统变量,初始化变量`uxPendedCounts = uxPendedTicks`。
循环:如果`uxPendedCounts > 0`:
{
递增系统Tick(`xTaskIncrementTick()`),如果返回`pdTRUE`,则表明需要发起调度,则设置`xYieldPending = pdTRUE`。
`uxPendedCounts--`。
}
清空系统调度挂起Tick计数(`uxPendedTicks = 0`)。
}
如果存在挂起的调度,即`xYieldPending = pdTRUE`:
{
设置`xAlreadyYielded = pdTRUE`。
(仅开启USE_PREEMPTION)发起一次调度(`taskYIELD_IF_USING_PREEMPTION()`)。
}
}
退出临界区(`taskEXIT_CRITICAL()`)。
返回`xAlreadyYielded`。
}
从实现可以看出,调度器的挂起和恢复行为是可以嵌套的,只有当嵌套深度为0时,才会真正退出挂起状态。
8. 时间的度量和Tick中断
在任务切换中提到,任务调度可以由Tick中断触发,周期性的tick中断使得调度器周期性执行任务调度。由此可以看出,任务占用CPU的最小时间片是tick中断的周期。
在嵌入式设备中,通常配置Tick中断的周期为1ms,即需配置宏configTICK_RATE_HZ = 1000
。
Tick硬件定时器是在开启调度器的时候开启的,一旦开启Tick定时器后,便会定期触发Tick中断。而在Tick中断中,便会调用FreeRTOS的Tick中断响应函数FreeRTOS_Tick_Handler()
进行处理,这其中会进行任务调度和Tick计数的更新等。
先来看看一些关键的行为:
下面便来看看在Tick中断处理中具体做了什么。宏观的活动图如下:
-
屏蔽低优先级的中断:
屏蔽CPU的所有中断(`portCPU_IRQ_DISABLE`)。
设置GIC中断优先级屏蔽寄存器。
恢复CPU的所有中断(`portCPU_IRQ_ENABLE`)。
-
增加系统Tick计数(BaseType_t xTaskIncrementTick()
):看似简单的行为,实际上还需要考虑是否需要唤醒处于Blocked状态的任务,Tick溢出处理等。活动图如下:
-
如果步骤2中触发了调度请求(xTaskIncrementTick() != pdFALSE
),则设置系统调度触发标志ulPortYieldRequired = pdTRUE
。
-
恢复中断优先级屏蔽标志(portCLEAR_INTERRUPT_MASK()
):
{
屏蔽CPU的所有中断(`portCPU_IRQ_DISABLE`)。
恢复GIC中断优先级屏蔽寄存器(`portICCPMR_PRIORITY_MASK_REGISTER = portUNMASK_VALUE`)。
恢复CPU的所有中断(`portCPU_IRQ_ENABLE`)。
}
-
清除Tick硬件定时器中断标志位(configCLEAR_TICK_INTERRUPT()
)。
由于ulPortYieldRequired
会在汇编级别的中断响应函数中进行处理,即如果该标志位被置位,则会在汇编级别的中断响应函数中进行任务切换。详情可参看后续笔记的中断处理流程小节。
FreeRTOSv10后,支持从非0开始计数。
9. 任务控制
本小节主要介绍对任务进行控制的API。
9.1. 睡眠操作
睡眠函数原型:需要设置INCLUDE_vTaskDelay = 1
void vTaskDelay( const TickType_t xTicksToDelay ) PRIVILEGED_FUNCTION
使调用该函数的Task睡眠一定的tick。睡眠时,该Task进入Blocked状态,实际的睡眠时长取决于configTICK_RATE_HZ
的设置。
该API通常与portTICK_PERIOD_MS()
一起使用,它将更易于理解的毫秒计时转换为tick计数。
在介绍具体实现之前,需要先理解内核如何当前任务加入Delayed任务队列(static void prvAddCurrentTaskToDelayedList( TickType_t xTicksToWait, const BaseType_t xCanBlockIndefinitely )
):
{
初始化本地变量`xConstTickCount = xTickCount`。
(仅开启TaskAbortDelay)清空当前任务的`ucDelayAborted = pdFALSE`。
将当前任务从Ready任务队列中移除,如果移除任务后该优先级的Ready任务队列为空:
{
更新Ready优先级位图`uxTopReadyPriority`,即该优先级从位图中清零。(`portRESET_READY_PRIORITY( pxCurrentTCB->uxPriority, uxTopReadyPriority )`)。
}
(仅开启vTaskSuspend)如果`xTicksToWait == portMAX_DELAY`且`xCanBlockIndefinitely != pdFALSE`:
{
将该任务插入Suspended队列。
}
否则:
{
计算唤醒时刻`xTimeToWake = xConstTickCount + xTicksToWait`。
记录该任务的唤醒时刻(`listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake)`)。
如果`xTimeToWake`溢出(`xTimeToWake < xConstTickCount`):
{
将任务插入OverflowDelayed任务队列。
}
如果未溢出:
{
将任务插入Delayed任务队列。
如果该任务的唤醒时刻比系统下一次唤醒时刻早(`xTimeToWake < xNextTaskUnblockTime`),则更新系统下一次唤醒时刻为该任务的唤醒时刻(`xNextTaskUnblockTime = xTimeToWake`)。
}
}
}
下面来看看睡眠的具体实现:
{
初始化`xAlreadyYielded = pdFALSE`。
如果`xTicksToDelay > 0`:
{
挂起调度器(`vTaskSuspendAll()`)。
将当前的调用者任务加入到Delayed任务队列( `prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE )` )。
恢复调度器(`xAlreadyYielded = vTaskResumeAll()`)。
}
如果`xAlreadyYielded == pdFALSE`,则强制触发一次调度`portYIELD_WITHIN_API()`。
}
可以看到,即使传入的xTicksToDelay = 0
,该API也会触发一次调度,因此需要特别注意这个行为是否符合预期。
需要注意的是,该睡眠函数的睡眠时间是相对于调用vTaskDelay()
的时刻而言的。因此,该函数并不适合用于实现一些需要高度精确的周期性的操作,因为如果在调用该函数前,该任务被其他任务或中断抢占,那么此时的周期性就不那么精确了。
于是才有了下面的函数原型:需要设置INCLUDE_vTaskDelayUntil = 1
void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement ) PRIVILEGED_FUNCTION;
从参数可以看出与前者的不同,需要注意的是,第一次传入pxPreviousWakeTime
时,需要将其设置为当前的TickCount(xTaskGetTickCount()
),之后该函数将会维护其值。
我们再来看看这个函数的实现:
{
初始化`xShouldDelayed = pdFALSE`。
入参检查:Assert`pxPreviousWakeTime != NULL`,`xTimeIncrement > 0`
Assert 调度器不处于挂起状态。
挂起调度器(`vTaskSuspendAll()`)。
初始化`xConstTickCount = xTickCount`。
计算唤醒时刻`xTimeToWake = *pxPreviousWakeTime + xTimeIncrement`。
如果当前系统时钟溢出(`xConstTickCount < *pxPreviousWakeTime`):
{
如果唤醒时刻比当前系统Tick晚(`(xTimeToWake < *pxPreviousWakeTime) && (xTimeToWake > xConstTickCount)`),则设置`xShouldDelay = pdTRUE`。
}
如果没有溢出:
{
如果唤醒时刻比当前系统Tick晚(`(xTimeToWake < *pxPreviousWakeTime) || (xTimeToWake > xConstTickCount)`),则设置`xShouldDelay = pdTRUE`。
}
更新上一次唤醒时刻为本次计算的唤醒时刻`*pxPreviousWakeTime = xTimeToWake`。
如果设置了Delay标志(`xShouldDelay != pdFALSE`):
{
将当前任务加入Delayed任务队列(`prvAddCurrentTaskToDelayedList( xTimeToWake - xConstTickCount, pdFALSE )`。
}
恢复调度器(`xAlreadyYielded = xTaskResumeAll()`);
因为调用者已经主动进入睡眠,所以,如果`xAlreadyYielded == pdFALSE`,则强制触发一次调度(`portYIELD_WITHIN_API()`)。
}
从实现中可以看到,这个睡眠是以pxPreviousWakeTime
为基准而进行唤醒时刻计算的,因此可以解决上一个API的问题。
如果想中途唤醒睡着的任务怎么办?贴心的FreeRTOS还提供了相应的API:需要设置INCLUDE_vTaskAbortDelay = 1
BaseType_t xTaskAbortDelay( TaskHandle_t xTask ) PRIVILEGED_FUNCTION;
该函数能将任务从睡眠状态唤醒,需要特别说明一下,该函数不仅可以唤醒vTaskDelay()/vTaskDelayUntil()
引起的Blocked的任务,还包括由于调用xQueueReceive()
和ulTaskNotifyTake()
而被动休眠的任务。
下面看看其具体实现:
{
初始化`pxTCB = ( TCB_t * ) xTask`,`xReturn = pdFALSE`。
参数校验:Assert `pxTCB != NULL`。
挂起调度器(`vTaskSuspendAll()`)。
如果该任务处于Block状态(`eTaskGetState( xTask ) == eBlocked`):
{
*FreeRTOSv10修正,设置`xReturn = pdPASS`*。
将任务从当前状态任务队列中删除。
进入临界区(`taskENTER_CRITICAL()`)。
如果任务正在等待某个事件(`listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL`):
{
将任务从该事件任务队列中移除。
设置任务的延迟中止标志(`pxTCB->ucDelayAborted = pdTRUE`)。
}
退出临界区(`taskEXIT_CRITICAL()`)。
将任务加入到Ready队列(`prvAddTaskToReadyList( pxTCB )`)。
(仅开启USE_PREEMPTION)如果被中止休眠的任务的优先级比当前任务优先级高(`pxTCB->uxPriority > pxCurrentTCB->uxPriority`),则设置调度器请求标志(`xYieldPending = pdTRUE`)。
}
恢复调度器(`xTaskResumeAll()`)。
返回`xReturn`。
}
9.2. 优先级操作
获取任务优先级的函数原型:需要设置INCLUDE_uxTaskPriorityGet = 1
UBaseType_t uxTaskPriorityGet( TaskHandle_t xTask ) PRIVILEGED_FUNCTION;
UBaseType_t uxTaskPriorityGetFromISR( TaskHandle_t xTask ) PRIVILEGED_FUNCTION;
获取指定任务的优先级。用NULL
作为实参可表示当前任务。
可以看到,这个系统调用有两个版本,一个带有FromISR
后缀,另一个则没有。这是FreeRTOS的一个特性,在使用时FreeRTOS的某些系统调用,使用者需要区分调用者的运行上下文,即是运行在Task上下文还是中断上下文,这是为了防止在中断上下文调用了一些耗时较长的API。而带有FromISR
后缀的就是中断上下文中调用的版本。详情可参看在ISR中使用FreeRTOS API。
在实现时,对于任务上下文的版本,需要在临界区获取这个任务的优先级信息,然后返回给调用者;对于中断上下文的版本,需要在屏蔽低优先级中断的情况下获取这个任务的优先级信息,然后返回给调用者。
修改任务的优先级的函数原型:需要配置INCLUDE_vTaskPrioritySet = 1
void vTaskPrioritySet( TaskHandle_t xTask, UBaseType_t uxNewPriority ) PRIVILEGED_FUNCTION;
需要注意的是,如果修改后的任务优先级比当前正在执行的任务的高,那么将会发生一次调度。
下面来看看这个函数的实现细节:
{
初始化`xYieldRequired = pdFALSE`。
参数检查:Assert `uxNewPriority < configMAX_PRIORITIES`.
进入临界区(`taskENTER_CRITICAL()`)。
(仅开启USE_MUTEXES)本地暂存任务当前的base优先级(`uxCurrentBasePriority = pxTCB->uxBasePriority`)。
如果新设置的优先级与当前的优先级不相同(`uxCurrentBasePriority != uxNewPriority`):
{
如果新的优先级大于当前的优先级:
{
如果该任务不是系统当前执行的任务(`pxTCB != pxCurrentTCB`),且新的优先级不小于系统当前执行的任务的优先级,则设置调度标志`xYieldRequired = pdTRUE`。
}
否则,即如果不大于:
{
如果该任务就是系统当前执行的任务(`pxTCB == pxCurrentTCB`),则表明可能有更高优先级的任务可以被调度,设置调度标志`xYieldRequired = pdTRUE`。
}
获取任务当前的实际优先级`uxPriorityUsedOnEntry = pxTCB->uxPriority`。
(仅开启USE_MUTEXES)更新优先级继承时的任务优先级:
{
如果该任务未进行优先级继承(`pxTCB->uxBasePriority == pxTCB->uxPriority`),则更新实际优先级为新的优先级(`pxTCB->uxPriority = uxNewPriority`)。
更新任务的Base优先级为新优先级(`pxTCB->uxBasePriority = uxNewPriority`)。
}
如果任务的event list item value没有被占用(`listGET_LIST_ITEM_VALUE( &( pxTCB->xEventListItem ) ) & taskEVENT_LIST_ITEM_VALUE_IN_USE ) == 0UL`),那么设置该值为`configMAX_PRIORITIES - uxNewPriority`(为了让任务可以在事件任务队列中按照优先级顺序排列)。
如果该任务正处于某个Ready任务队列(`listIS_CONTAINED_WITHIN( &( pxReadyTasksLists[ uxPriorityUsedOnEntry ] ), &( pxTCB->xStateListItem ) ) != pdFALSE`):
{
将该任务从当前这个优先级Ready任务队列中删除,如果此时该优先级队列已经为空,则更新优先级位图(`portRESET_READY_PRIROITY( uxPriorityUsedOnEntry, uxTopReadyPriority )`)。
将任务添加到新的Ready任务队列(`prvAddTaskToReadyList( pxTCB )`)。
}
如果调度标志位被置位(`xYieldRequired != pdTRUE`),则触发一次调度(`taskYIELD_IF_USING_PREEMPTION()`)。
}
退出临界区(`taskEXIT_CRITICAL()`)。
}
9.3. 任务状态操作
获取任务当前所处的状态的函数原型:需要设置INCLUDE_eTaskGetState = 1
eTaskState eTaskGetState( TaskHandle_t xTask ) PRIVILEGED_FUNCTION;
返回的是任务状态枚举类型。注意入参不能为NULL
。
由于在TCB中,并没有直接存储任务当前状态的数据成员,因此需要通过该任务所在的任务队列来判断它所处的状态。具体可查看如下实现:
{
入参检查:Assert `pxTCB != NULL`。
如果`pxTCB == pxCurrentTCB`,表示该任务处于Running状态。(`eReturn = eRunning`)。
否则:
{
进入临界区(`taskENTER_CRITICAL()`)。
获取该任务所在的状态任务队列(`pxStateList = ( List_t * ) listLIST_ITEM_CONTAINER( &( pxTCB->xStateListItem ) )`)。
退出临界区(`taskEXIT_CRITICAL()`)。
如果该队列是Delayed任务队列(`( pxStateList == pxDelayedTaskList ) || ( pxStateList == pxOverflowDelayedTaskList )`),则表示任务处于Blocked状态。(`eReturn = eBlocked`)。
(仅开启vTaskSuspend)若该队列是Suspended任务队列,且任务没有在事件任务队列中,则认为该任务处于Suspended状态(`xReturn = eSuspended`);否则,处于Blocked状态(`eReturn = eBlocked`)。
(仅开启vTaskDelete)若该队列是Deleted任务队列或任务不在任何队列中,则认为该任务处于Deleted状态。(`xReturn = eDeleted`)。
否则,则认为任务处于Ready状态(`xReturn = eReady`),所以PendingReady也被任务是Ready。
}
返回`xReturn`。
}
9.4. 任务挂起与恢复
挂起任务的函数原型:需要设置INCLUDE_vTaskSuspend = 1
void vTaskSuspend( TaskHandle_t xTaskToSuspend ) PRIVILEGED_FUNCTION;
将指定的任务挂起。被挂起的任务将进入Suspended状态,除了vTaskResume()
无人能把它唤醒。
下面看看挂起时FreeRTOS主要做了啥:
{
进入临界区(`taskENTER_CRITICAL()`)。
将任务从Ready任务队列中删除,如果此时该队列为空,则需要更新优先级位图。
如果该任务还处于事件任务队列中,将任务从中删除。
将任务插入Suspended任务队列中。
(仅开启USE_TASK_NOTIFICATIONS,*FreeRTOSv10后引入*)若当前任务正在等待某个通知(`pxTCB->ucNotifyState == taskWAITING_NOTIFICATION`),则复位其等待状态为`taskNOT_WAITING_NOTIFICATION`。
退出临界区(`taskENTER_CRITICAL()`)。
如果调度器未被挂起(`xSchedulerRunning != pdFALSE`):
{
进入临界区(`taskENTER_CRITICAL()`)。
更新系统下一次唤醒的时刻(`prvResetNextTaskUnblockTime()`)。
退出临界区(`taskEXIT_CRITICAL()`)。
}
如果该任务是正在运行的任务(`pxTCB == pxCurrentTCB`):
{
若调度器未被挂起(`xSchedulerRunning != pdFALSE`),则发起一次调度(`portYIELD_WITHIN_API()`)。
若调度器被挂起:
{
若系统中所有的任务都被挂起(`listCURRENT_LIST_LENGTH( &xSuspendedTaskList ) == uxCurrentNumberOfTasks`),则设置系统当前的任务为`NULL`。
否则,强制切换系统当前运行的任务(`vTaskSwitchContext()`)。
}
}
}
唤醒任务的函数原型: 需要设置INCLUDE_vTaskSuspend = 1
void vTaskResume( TashHandle_t xTaskToResume ) PRIVILEGED_FUNCTION;
唤醒被挂起的任务。
先来看看关键的行为:
-
检查任务是否处于挂起状态(static BaseType_t prvTaskIsTaskSuspended( const TaskHandle_t xTask )
):
{
初始化`pxTCB = ( TCB_t * ) xTask`
参数校验:Assert `pxTCB != NULL`。
如果任务处于Suspended任务队列(`listIS_CONTAINED_WITHIN( &xSuspendedTaskList, &( pxTCB->xStateListItem ) ) != pdFALSE`),且任务不在PendingReady任务队列(`listIS_CONTAINED_WITHIN( &xPendingReadyList, &( pxTCB->xEventListItem ) ) == pdFALSE`),并且任务不在事件任务队列中(`listIS_CONTAINED_WITHIN( NULL, &( pxTCB->xEventListItem ) ) != pdFALSE`),则返回`pdTRUE`;
否则返回`pdFALSE`。
}
下面看看它具体做了什么:
{
入参检查:Assert `xTaskToResume != NULL。`
若`pxTCB != pxCurrentTCB`:
{
进入临界区(`taskENTER_CRITICAL()`)。
如果任务被挂起(`prvTaskIsTaskSuspended( pxTCB ) != pdFALSE`):
{
将任务从Suspended任务队列中删除。
将任务加入到Ready任务队列中(`prvAddTaskToReadyList( pxTCB )`)。
如果被唤醒的任务的优先级不低于当前运行任务的优先级,则发起一次调度(`taskYIELD_IF_USING_PREEMPTION()`)。
}
退出临界区(`taskEXIT_CRITICAL()`)。
}
}
可以看出,任务的挂起和恢复并不支持嵌套调用,也就是说,调用一次vTaskResume()
便可将其唤醒,不管前面调用了多少次vTaskSuspend()
。
此外, FreeRTOS还提供了中断上下文版本的唤醒接口:
BaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume ) PRIVILEGED_FUNCTION;
该版本的实现与任务上下文中的版本没啥不同. 但需要注意, 不建议使用这个接口来实现Task和中断的同步, 因为中断有可能在任务调用vTaskSuspend()
前就触发了, 这样的话这个任务可能就此长眠.
另外, 它的返回值需要特别说明一下: 返回TRUE
表示在退出中断处理后需要触发一次任务调度, 也就是说被唤醒的家伙具有比被当前中断打断的任务更高的优先级. 如果返回FALSE
就不用触发了.
其实现如下:
{
初始化`xYieldRequired = pdFALSE`。
参数检查:Assert `xTaskToResume != NULL`。
检查中断优先级屏蔽设置是否正确(`portASSERT_IF_INTERRUPT_PRIORITY_INVALID()`)。
屏蔽低优先级中断(`uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR()`)
如果该任务处于挂起状态(`prvTaskIsTaskSuspended( pxTCB ) != pdFALSE`):
{
如果调度器未被挂起(`uxSchedulerSuspended == pdFALSE`):
{
如果该任务的优先级不低于系统当前的任务优先级,设置调度标志`xYieldRequired = pdTRUE`。
将任务从Suspended队列中删除。
将任务插入到Ready任务队列中(`prvAddTaskToReadyList( pxTCB )`)。
}
否则,即调度器被挂起,将任务加入到PendingReady任务队列(`vListInserEnd( &xPendingReadyList, &( pxTCB->xEventListItem ) )`)。
}
恢复低优先级中断屏蔽(`portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus )`)
返回调度请求标志`xYieldRequired`。
}
9.5. 任务相关的工具接口
获取当前的TickCount函数原型:
TickType_t xTaskGetTickCount( void ) PRIVLEGED_FUNCTION;
这里返回的TickCount是自调用vTaskStartScheduler()
开始计算的。另外,该函数还提供了中断上下文的版本:
TickType_t xTaskGetTickCountFromISR( void ) PRIVILEGED_FUNCTION;
设置和获取Application task hook函数的原型:需要配置configUSE_APPLICATION_TASK_TAG = 1
void vTaskSetApplicationTaskTag( TaskHandle_t xTask, TaskHookFunction_t pxHookFunction ) PRIVILEGED_FUNCTION;
TaskHookFunction_t xTaskGetApplicationTaskTag( TaskHandle_t xTask) PRIVILEGED_FUNCTION;
调用Application Task Hook的函数原型:需要配置configUSE_APPLICATION_TASK_TAG = 1
BaseType_t xTaskCallApplicationTaskHook( TaskHandle_t xTask, void *pvParameter ) PRIVILEGED_FUNCTION;
与上面的函数接口一同使用, 调用指定Task的钩子函数。// TODO: 需要进一步研究有啥用。
外部传入的Task私有存储:需要设置configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0
void vTaskSetThreadLocalStoragePointer( TaskHandle_t xTaskToSet, BaseType_t xIndex, void *pvValue) PRIVILEGED_FUNCTION;
void *pvTaskGetThreadLocalStoragePointer( TaskHandle_t xTaskToQuery, BaseType_t xIndex) PRIVILEGED_FUNCTION;
FreeRTOS提供了一个可被外部获取的Task私有存储机制:每个任务包含一组指针(该指针的数量由configNUM_THREAD_LOCAL_STORAGE_POINTERS
决定),FreeRTOS内核并不会使用它,而是由外部来决定如何使用它们。// TODO: 非常神奇,不知道有啥用途。
获取Idle Task的句柄的函数原型:需要设置INCLUDE_xTaskGetIdleTaskHandle = 1
TaskHandle_t xTaskGetIdleTaskHandle( void ) PRIVILEGED_FUNCTION;
很显然,这个函数必须在开启了调度器后才能调用,否则压根就找不到Idle Task这玩意儿。
9.6. 调试相关操作
FreeRTOS提供了一些操作系统相关的调试信息获取方式,便于做一些简单的系统监控。
获取当前创建的任务总数的函数原型:
UBaseType_t uxTaskGetNumberOfTasks ( void ) PRIVILEGED_FUNCTION;
需要注意的是,僵尸任务(即被Deleted,但还未被Idle Task回收的任务)也会被统计。
获取任务的名字的函数原型:
char *pcTaskGetName( TaskHandle_t xTaskToQuery ) PRIVILEGED_FUNCTION;
通过任务名字查找任务句柄的函数原型:需要设置INCLUDE_xTaskGetHandle = 1
TaskHandle_t xTaskGetHandle( const char *pcNameToQuery ) PRIVILEGED_FUNCTION;
需要注意的是,查找字符串是个比较耗时的行为。
获取任务的剩余堆栈空间:需要设置INCLUDE_uxTaskGetStackHighWaterMark = 1
UBaseType_t uxTaskGetStackHighWaterMark( TaskHandle_t xTask ) PRIVILEGED_FUNCTION;
剩余空间的度量单位和vTaskCreate()
中的StackDepth
一致,都是使用机器字作为单位。
获取任务的详细状态信息:需要设置configUSE_TRACE_FACILITY = 1
void vTaskGetInfo( TaskHandle_t xTask, TaskStatus_t *pxTaskStatus, BaseType_t xGetFreeStackSpace, eTaskState eState) PRIVILEGED_FUNCTION;
pxTaskStatus
用于接收状态信息。信息的详情可查看任务状态结构体。
由于获取栈的剩余空间是一个比较耗时的过程,这将会导致系统短时间内不响应其他请求,因此,可通过传入xGetFreeStackSpace = pdFALSE
跳过这个过程。
同理,获取当前的任务状态也不是简单的过程,只有当传入eState = eInvalid
时才会使能获取当前任务状态过程。
想要一次性获取FreeRTOS当前所有的Task的任务状态信息?了解一下下面这个函数原型:需要设置configUSE_TRACE_FACILITY = 1
UBaseType_t uxTaskGetSystemState( TaskStatus_t * const pxTaskStatusArray, const UBaseType_t uxArraySize, uint32_t * const pulTotalRuntime ) PRIVILEGED_FUNCTION;
需要注意的是,该方法只应该在Debug的时候使用,因为它会将调度器挂起。pxTaskStatusArray
用于接收返回的信息,可通过uxTaskGetNumberOfTasks()
先获取当前的任务数量,然后申请足够大的空间来存储返回的信息。uxArraySize
指该数组中任务状态结构体的个数,而不是字节数。
如果配置了configGENERATE_RUN_TIME_STATS = 1
,pulTotalRuntime
则可获取到开机后的总运行时间。为了实现这个统计功能,还需要外部提供portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()
和portGET_RUN_TIME_COUNTER_VALUE()
的实现,分别用于配置硬件Timer统计时间信息,和获取当前的时间信息。需要注意的是,这个硬件Timer的时钟频率至少得是FreeRTOS Tick的10倍。
如果调用正常,该函数会返回任务状态结构体的数量,正常来说,应该和uxTaskGetNumberOfTasks()
的结果是一致的;如果uxArraySize
过小,则会返回0.
现如今,大家都喜欢一键式解决方案,上面提供的信息都还不能算是一键式打印系统状态,下面这个才是真正意义上的一键式打印系统状态的函数接口:需要设置configUSE_TRACE_FACILITY = 1
和configUSE_STATS_FORMATTING_FUNCTIONS = 1
void vTaskList( char * pcWriteBuffer ) PRIVILEGED_FUNCTION;
void vTaskGetRunTimeStats( char * pcWriteBuffer ) PRIVILEGED_FUNCTION;
以上两个函数会直接以字符串的形式打印系统的状态,它在内部调用了uxTaskGetSystemState()
,并进行了翻译。通常来说,他们只用于Demo演示,正常情况下,还是建议使用uxTaskGetSystemState()
来获取信息。
9.7. 与调度器的实现相关的操作(出于移植的需要而暴露)
以下的接口不能在应用程序代码中使用,因为会导致系统的行为混乱。
增加Tick计数的函数原型:
BaseType_t xTaskIncrementTick( void ) PRIVILEGED_FUNCTION;
该函数会增加tick count,并检查是否有等待超时的Blocked任务需要被唤醒。如果函数返回了非零值,那么说明发生了以下两种情况中的一种:
- 有一个任务超时了,需要被唤醒。
- 如果采用时间片调度算法,则说明有一个相同优先级的任务需要被调度了。
详细实现可参看时间的度量和Tick中断部分。
下面介绍与事件队列相关的API:必须在关闭中断的情况下调用
void vTaskPlaceOnEventList( List_t * const pxEventList, const TickType_t xTicksToWait ) PRIVILEGED_FUNCTION;
void vTaskPlaceOnUnorderedEventList( List_t * pxEventList, const TickType_t xItemValue, const TickType_t xTicksToWait ) PRIVILEGED_FUNCTION;
该函数将调用者从Ready队列中移除,并放置到等待事件任务队列(pxEventList
)和延迟任务队列中。只有当相应事件发生,或超时时才能被唤醒。
FreeRTOS为队列中的任务提供了两种排序方式的实现:
- unorder:将链表项的值设置为入参
xItemValue
的值,并将链表项插入到链表的末尾。用于消息队列的实现。 - ordered:使用Task的优先级作为链表项的排列依据,因此新插入的Task也将放置到合适的位置。用于事件组的实现。
xTicksToWait
指的是Task希望等待的最长时限,单位是Tick Count。可与portTICK_PERIOD_MS
一同使用。
需要注意的是,以上两个函数都只支持任务的等待事件的时间是确定的。而FreeRTOS还提供了不确定等待时限的版本:
void vTaskPlaceOnEventListRestricted( List_t * const pxEventList, TickType_t xTicksToWait, const BaseType_t xWaitIndefinitely ) PRIVILEGED_FUNCTION;
当xWaitIndefinitely
为pdTRUE
时,则采用不确定时限的等待模式;否则采用确定时限模式。其余行为与vTaskPlaceOnEventList()
一致。
有添加就一定会有删除,而从事件任务队列中删除任务的函数原型如下:必须在关闭中断的情况下调用
BaseType_t xTaskRemoveFromEventList( const List_t * const pxEventList ) PRIVILEGED_FUNCTION;
BaseType_t xTaskRemoveFromUnorderedEventList( ListItem_t * pxEventListItem, const TickType_t xItemValue ) PRIVILEGED_FUNCTION;
这两个函数会在指定事件发生,或者等待超时时被调用。他们主要负责将任务从任务事件队列和Blocked任务队列中移除,并放入Ready任务队列中。他们分别与vTaskPlaceOnEventList()
和vTaskPlaceOnUnorderedEventList()
对应。xTaskRemoveFromEventList()
则将任务从头部删除,因为它一定是最高优先级的任务;而xTaskRemoveFromUnorderedEventList()
会将链表项的值设置为xItemValue
。FreeRTOSv10后,xTaskRemoveFromUnorderedEventList()
修改为void vTaskRemoveFromUnorderedEventList(...)
。
这些接口的详细实现可参看队列控制和等待和查询事件组部分。
下面要介绍调度器的重要功能,上下文切换的函数原型:
void vTaskSwitchContext( void ) PRIVILEGED_FUNCTION;
它要完成的功能其实很简单,就是找到当前Ready队列中优先级最高的任务,并把当前的TCB指针指向该任务。其实现参看任务切换部分。
复位Task的Event链表项的Item value:
TickType_t uxTaskResetEventItemValue( void ) PRIVILEGED_FUNCTION;
在后续介绍数据结构的章节(链表)中会说明,链表项拥有者和链表项之间是互联的,将这个概念应用到任务队列里面则是:事件任务队列的链表项和对应的任务是互联的,也就是说每个任务自身包含了指向事件任务队列链表项的指针,而该接口可复位链表项的xItemValue
,使得它可以被其他其他模块(例如:队列和信号量)使用。该函数将返回复位前的xItemValue
值。
获取当前任务句柄的函数原型:
TaskHandle_t xTaskGetCurrentTaskHandle( void ) PRIVILEGED_FUNCTION;
获取当前的Tick信息的函数原型:
void vTaskSetTimeOutState( TimeOut_t * const pxTimeOut) PRIVILEGED_FUNCTION;
超时状态结构体仅用于内部实现。该函数与以下函数配合使用:
BaseType_t xTaskCheckForTimeOut( TimeOut_t * const pxTimeOut, TickType_t * const pxTicksToWait ) PRIVILEGED_FUNCTION;
该函数会与vTaskSetTimeOutState()
捕获的时间信息进行比较,查看是否超时。如果超时,则返回pdTRUE
;否则返回pdFALSE
。该接口用于队列的实现。
设置存在挂起的调度请求的函数原型:用于队列的实现。
void vTaskMissedYield( void ) PRIVILEGED_FUNCTION;
该函数可用于避免不必要的taskYIELD()
调用。该接口用于队列的实现。
获取调度器的状态的函数原型:
BaseType_t xTaskGetSchedulerState( void ) PRIVILEGED_FUNCTION;
返回调度器当前的状态:
taskSCHEDULER_RUNNING
:正在运行taskSCHEDULER_NOT_STARTED
:未启动taskSCHEDULER_SUSPENDED
:调度器被挂起
FreeRTOS也支持任务优先级继承的机制,函数原型:
BaseType_t xTaskPriorityInherit( TaskHandle_t const pxMutexHolder ) PRIVILEGED_FUNCTION;
该功能是为互斥锁而设计的。每当有高优先级的任务申请互斥锁,且该锁被低优先级的任务占有时,则会调用该函数进行优先级的继承,即低优先级任务继承高优先级任务的优先级。
在发生过优先级继承的任务释放锁时,还需要将优先级还原。此时则需要调用如下函数接口:
BaseType_t xTaskPriorityDisinherit( TaskHandle_t const pxMutexHolder ) PRIVILEGED_FUNCTION;
这里还存在一个特殊情况:当高优先级的任务等待超时后,它将会放弃申请互斥锁,这时,也需要恢复低优先级任务的优先级。
void vTaskPriorityDisinheritAfterTimeout( TaskHandle_t const pxMutexHolder, UBaseType_t uxHighestPriorityWaitingTask ) PRIVILEGED_FUNCTION;
需要注意的是,因为仍然有可能存在其他高优先级的任务在等待该锁,所以需要将目前处于等待的最高优先级任务的优先级通过uxHighestPriorityWaitingTask
传入。
对于互斥锁的实现,FreeRTOS还提供了一个增加互斥锁持有数量的接口:
void *pvTaskIncrementMutexHeldCount( void ) PRIVILEGED_FUNCTION;
该函数会为Task持有的互斥锁数量增1,并且,返回持有该锁的Task的句柄。
这些接口会在资源管理章节中详细介绍。
获取任务编号的函数原型:
UBaseType_t uxTaskGetTaskNumber( TaskHandle_t xTask ) PRIVILEGED_FUNCTION;
该函数返回xTask的uxTCBNumber
。
上面介绍了getter,下面就介绍setter,设置任务编号的函数原型:
void vTaskSetTaskNumber( TaskHandle_t xTask, const UBaseType_t uxHandle ) PRIVILEGED_FUNCTION;
下面介绍的两个函数原型只在Tickless idle或低功耗模式中使用。
设置需要跳过的Tick数的函数原型:需要设置configUSE_TICKLESS_IDLE = 1
void vTaskStepTick( const TickType_t xTickToJump ) PRIVILEGED_FUNCTION;
当处于Tickless idle模式的idle周期时,tick中断将会被暂停。而Tick count是在tick中断处理函数中更新的,一旦tick中断被暂停,系统的tick计数就不再更新了。因此,为了确保tick计数正常,当系统退出idle周期时,需要手动更新一次tick count。而该函数就是为了实现这个功能。xTickToJump
的值应该恰好等于idle的周期。
确认当前是否可进入Sleep模式的函数原型:需要设置configUSE_TICKLESS_IDLE = 1
eSleepModeStatus eTaskConfirmSleepModeStatus( void ) PRIVILEGED_FUNCTION;
FreeRTOS的低功耗机制中,portSUPPRESS_TICKS_AND_SLEEP()
提供了进入低功耗的接口。这是一个平台相关的宏,它的语义是让处理器进入等待状态(例如ARM中的WFI)。而该宏只是在调度器挂起的情况下调用的,因此这个过程中有可能被中断处理打断而发生上下文切换。因此,该函数为这个平台宏的实现提供了一个确认是否满足睡眠条件的接口,并且要求该函数在临界区被调用。
10. 删除任务
删除任务这个行为很好理解,类似于Linux终止进程的过程。
函数原型:需要配置INCLUDE_vTaskDelete = 1
开启功能
void vTaskDelete( TaskHandle_t xTaskToDelete ) PRIVILEGED_FUNCTION;
将指定的任务从内核中删除,这意味着不管它在Ready队列、Blocked队列、Suspended队列或事件队列中,它都会被删除。而Idle Task将负责收回该任务在创建时申请的两片内存。
与Linux不同的是,FreeRTOS的任务没有独立的堆,因此,删除任务前一定要手动释放动态申请的内存。
下面来看看它的实现。该函数的活动图如下:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)