Freertos任务调度全流程解析
调用xTaskCreate函数创建任务,创建流程如下:否是否是否是开始申请TCB内存空间申请任务栈空间申请成功?返回创建失败初始化TCB成员设置任务参数设置任务名设置任务优先级设置栈底位置初始化任务栈模拟第一次调度现场将任务添加到就绪列表调度器已启动?结束(等待调度器启动)新任务优先级 >当前运行任务?结束(等待下次调度)触发任务切换(抢占CPU)结束TCB(Task Control Block,
文章目录
任务创建
调用xTaskCreate函数创建任务,创建流程如下:
任务控制块(TCB)详解
TCB(Task Control Block,任务控制块)是Freertos的核心数据结构,是操作系统内核用于描述和管理一个任务所有信息的数据结构。可以把它理解为一个任务的 “身份证” 或 “个人档案袋”,每个任务有且仅有一个独一无二的TCB。
/*
* Task control block. A task control block (TCB) is allocated for each task,
* and stores task state information, including a pointer to the task's context
* (the task's run time environment, including register values)
*/
typedef struct tskTaskControlBlock /* The old naming convention is used to prevent breaking kernel aware debuggers. */
{
volatile StackType_t *pxTopOfStack; /*< Points to the location of the last item placed on the tasks stack. THIS MUST BE THE FIRST MEMBER OF THE TCB STRUCT. */
#if (portUSING_MPU_WRAPPERS == 1)
xMPU_SETTINGS xMPUSettings; /*< The MPU settings are defined as part of the port layer. THIS MUST BE THE SECOND MEMBER OF THE TCB STRUCT. */
#endif
ListItem_t xStateListItem; /*< The list that the state list item of a task is reference from denotes the state of that task (Ready, Blocked, Suspended ). */
ListItem_t xEventListItem; /*< Used to reference a task from an event list. */
UBaseType_t uxPriority; /*< The priority of the task. 0 is the lowest priority. */
StackType_t *pxStack; /*< Points to the start of the stack. */
char pcTaskName[configMAX_TASK_NAME_LEN]; /*< Descriptive name given to the task when created. Facilitates debugging only. */ /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
#if ((portSTACK_GROWTH > 0) || (configRECORD_STACK_HIGH_ADDRESS == 1))
StackType_t *pxEndOfStack; /*< Points to the highest valid address for the stack. */
#endif
#if (portCRITICAL_NESTING_IN_TCB == 1)
UBaseType_t uxCriticalNesting; /*< Holds the critical section nesting depth for ports that do not maintain their own count in the port layer. */
#endif
#if (configUSE_TRACE_FACILITY == 1)
UBaseType_t uxTCBNumber; /*< Stores a number that increments each time a TCB is created. It allows debuggers to determine when a task has been deleted and then recreated. */
UBaseType_t uxTaskNumber; /*< Stores a number specifically for use by third party trace code. */
#endif
#if (configUSE_MUTEXES == 1)
UBaseType_t uxBasePriority; /*< The priority last assigned to the task - used by the priority inheritance mechanism. */
UBaseType_t uxMutexesHeld;
#endif
#if (configUSE_APPLICATION_TASK_TAG == 1)
TaskHookFunction_t pxTaskTag;
#endif
#if (configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0)
void *pvThreadLocalStoragePointers[configNUM_THREAD_LOCAL_STORAGE_POINTERS];
#endif
#if (configGENERATE_RUN_TIME_STATS == 1)
uint32_t ulRunTimeCounter; /*< Stores the amount of time the task has spent in the Running state. */
#endif
#if (configUSE_NEWLIB_REENTRANT == 1)
/* Allocate a Newlib reent structure that is specific to this task.
Note Newlib support has been included by popular demand, but is not
used by the FreeRTOS maintainers themselves. FreeRTOS is not
responsible for resulting newlib operation. User must be familiar with
newlib and must provide system-wide implementations of the necessary
stubs. Be warned that (at the time of writing) the current newlib design
implements a system-wide malloc() that must be provided with locks. */
struct _reent xNewLib_reent;
#endif
#if (configUSE_TASK_NOTIFICATIONS == 1)
volatile uint32_t ulNotifiedValue;
volatile uint8_t ucNotifyState;
#endif
/* See the comments in FreeRTOS.h with the definition of
tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE. */
#if (tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0) /*lint !e731 !e9029 Macro has been consolidated for readability reasons. */
uint8_t ucStaticallyAllocated; /*< Set to pdTRUE if the task is a statically allocated to ensure no attempt is made to free the memory. */
#endif
#if (INCLUDE_xTaskAbortDelay == 1)
uint8_t ucDelayAborted;
#endif
#if (configUSE_POSIX_ERRNO == 1)
int iTaskErrno;
#endif
} tskTCB;
主要作用:
1、任务调度的依据。TCB中保存了任务优先级,任务状态,调度器根据这些信息决定是否调度。
2、调度上下文管理。TCB中记录了任务栈的栈顶位置,在进行任务上下文切换时,通过栈顶指针保存任务的运行状态(寄存器值),在切换回该任务时,通过栈顶指针恢复运行状态。
3、内核对象链接锚点。通过内嵌的双向链表节点(ListItem_t)实现与内核管理数据结构的连接:
- xStateListItem:将任务挂载至系统级状态列表(就绪列表、阻塞列表、挂起列表)。
- xEventListItem:将任务挂载至同步对象(如信号量、队列、事件组)的等待队列。
StackType_t 是什么?
StackType_t 是FreeRTOS在移植层(port layer)定义的架构相关的数据类型,用于表示堆栈单元的大小。StackType_t 的大小就是处理器堆栈指针操作的最小对齐单位。不同CPU架构的定义不同。
// 在ARM Cortex-M(32位)上(portmacro.h中):
#define portSTACK_TYPE uint32_t
typedef portSTACK_TYPE StackType_t;
为什么申请任务栈以StackType_t 为单位申请?
1、内存对齐要求,现代处理器堆栈的指针必须满足特定的对齐要求,否则会导致:
- 硬件异常(如ARM的Alignment Fault)
- 性能下降(未对齐访问需要多次内存操作
- 原子操作失败(某些架构要求对齐访问才能原子操作)
2、上下文保存的便利性
任务切换时,需要将CPU的寄存器保存在堆栈中,这些寄存器的大小通常是StackType_t的倍数。
任务调度
调用vTaskStartScheduler函数开启调度器,调用流程:
空闲任务的作用
空闲任务是 FreeRTOS 调度器在调用 vTaskStartScheduler() 时自动创建的一个特殊任务。它始终存在,优先级被设为系统最低(通常为 0)。当系统中没有其他用户任务处于就绪状态时,空闲任务就会获得 CPU 控制权。它的主要作用如下:
1、维持系统的基本运行
- 防止 CPU 空转:操作系统需要有一个可运行的任务来保证调度器正常工作。如果没有空闲任务,当所有用户任务都阻塞或挂起时,调度器将无任务可选,系统会陷入未定义状态。
- 提供调度入口:空闲任务的运行状态为调度器提供了持续进行上下文切换的基础,使 Tick 中断能够照常触发,系统时间得以更新。
2、清理被删除任务的资源
- 延迟释放内存:当任务调用 vTaskDelete() 自我删除时,它无法立即释放自己的堆栈和任务控制块(TCB),因为需要上下文切换后才能安全回收。
- 空闲任务执行清理:被删除的任务会被移入“待销毁”列表,空闲任务在每次循环中检查该列表,并负责释放这些资源(内存)。这是空闲任务最重要的职责之一,若不及时清理,将导致内存泄漏。
3、作为系统负载的指示器
- 统计 CPU 利用率:通过测量空闲任务执行的时间比例,可以估算系统的 CPU 负载。空闲任务运行得越少,说明系统越繁忙;反之,则说明 CPU 有较多空闲时间。
软件定时器的作用
软件定时器是操作系统提供的基于系统时钟节拍(Tick)的软件功能组件。它允许任务在未来的某个时刻执行指定的回调函数,无需任务主动轮询时间。软件定时器可通过 configUSE_TIMERS 配置开启关闭,具体功能:
1、执行周期性或一次性任务:
- 周期性:可以设定一个定时器每隔100ms翻转一次LED灯。
- 一次性:可以设定一个定时器在5秒后执行关机的动作。
2、替代硬件定时器:
- 节省硬件资源:芯片的硬件定时器数量有限,软件定时器可以创建多个(仅受内存限制),缓解硬件资源竞争。
- 简化操作:无需直接操作硬件寄存器,通过API函数即可管理。
3、处理超时机制:
- 常用于协议栈(如等待ACK超时重传)或用户交互(如按键长按检测)中,简化了时间相关的状态机设计。
软件定时器流程图:
需要注意:
- 定时器回调函数应尽量短小,不可阻塞(如调用 vTaskDelay() 或等待信号量),否则会影响其他定时器及系统实时性。
- 回调中通常不允许执行可能引起任务切换的系统调用。
- 软件定时器的精度受系统时钟节拍影响(通常为毫秒级),不适合高精度硬件时序需求。
硬件定时器的作用
硬件定时器是芯片内部集成的物理外设,在操作系统中扮演着系统心跳的角色。具体作用如下:
1、 产生系统 Tick 中断
- 时间基准:硬件定时器被配置为以固定频率产生周期性中断,这个中断就是操作系统的“心跳”——Tick 中断。
- 驱动调度:每次 Tick 中断触发时,操作系统都会执行以下操作:
1.更新系统时间计数(如 xTickCount)。
2.检查是否有任务需要从阻塞态切换到就绪态(如延时结束、等待事件超时)。
3.触发任务调度(如果是抢占式调度,可能切换到更高优先级的任务)。
4.递减软件定时器的计数,并在到期时触发软件定时器回调。
2、任务调度的硬件基础
- 时间片轮转:在支持时间片轮转的系统中,硬件定时器的 Tick 中断用于判断当前任务运行的时间片是否耗尽,从而决定是否切换到下一个同等优先级的任务。
- 精确计时:所有依赖时间的操作(vTaskDelay、xTaskGetTickCount、信号量超时等待等)都基于硬件定时器提供的 Tick 计数。
第一个任务是如何开始调度的?
在调用 vTaskStartScheduler() 启动调度器时,第一个任务的启动并非通过常规的 Tick 中断触发,而是通过直接上下文恢复实现的。整个过程分为以下几个关键步骤:
1、任务创建时的准备工作
每个任务在创建时(通过 xTaskCreate()),系统会为其分配一个独立的任务堆栈,并在堆栈中预填充一个初始的上下文结构,看起来就像该任务刚被中断打断一样。这个上下文包含:
- 程序计数器(PC):指向任务函数的入口地址。
- 程序状态寄存器(xPSR):设置为默认值(通常使能 Thumb 状态)。
- 通用寄存器(R0-R12, R14/LR):初始化为 0 或特定值。
- 返回地址(LR):通常设置为一个特殊的退出函数(如 prvTaskExitError),以防任务意外返回。
这样,当这个“虚假”的中断返回时,CPU 就会自动跳转到任务函数开始执行。
2、调度器启动时的任务选择
在 vTaskStartScheduler() 内部,创建完空闲任务(及可选的定时器服务任务)后,调度器会调用一个架构相关的函数(如 xPortStartScheduler())来启动第一个任务。在此之前,所有用户创建的任务都已存在于就绪列表中。
调度器通过就绪任务优先级位图或直接遍历就绪列表,找到当前优先级最高的就绪任务。如果存在多个同优先级任务,通常选择列表中的第一个任务。
3、启动第一个任务(关键汇编实现)
为了让第一个任务开始运行,调度器需要手动恢复该任务的上下文。这部分工作通常由汇编函数完成。典型流程如下:
- 设置 PSP(进程栈指针):将当前要运行的任务的堆栈顶部地址加载到 PSP 寄存器。
- 恢复任务上下文:从任务堆栈中依次弹出 R4-R11(如果使用了浮点单元,还需恢复浮点寄存器)。
- 触发 PendSV 异常:或者直接通过修改 LR 和 PC 实现。在 FreeRTOS 中,通常是通过触发 PendSV 异常,在 PendSV 处理函数中完成上下文恢复。但对于第一个任务,由于系统尚未进入 PendSV 流程,会直接模拟 PendSV 的行为。
① 设置 PSP。
② 设置 CONTROL 寄存器,指示使用 PSP 作为堆栈指针。
③ 执行一个异常返回指令(如 bx r14),该指令会从堆栈中自动弹出剩余的寄存器(R0-R3, R12, LR, PC, xPSR),从而跳转到任务函数。 - 进入任务代码:当异常返回完成后,CPU 开始执行任务函数的第一条指令。
总结
第一个任务的调度成功依赖于两个核心设计:
- 任务创建时预置的虚假上下文,使任务具备“可恢复”的初始状态。
- 汇编级别的直接上下文切换,在无 Tick 中断的情况下手动加载任务上下文并跳转。
这种机制保证了操作系统从单线程(启动代码)到多线程(任务调度)的平滑过渡。
任务调度策略
策略流程图:
优先级反转
优先级反转是指,一个高优先级任务在等待一个被低优先级占用的共享资源时,由于中间优先级任务的干扰,导致高优先级任务被阻塞的时间超出预期,甚至无限期延迟。简单来说就是,高优先级任务无法运行,反而低优先级任务得以执行的现象。
经典场景分析
考虑三个任务:H(高优先级)、M(中优先级)、L(低优先级)。它们共享一个资源(例如一个互斥量保护的临界区)。
1、L 获得资源(例如获取了互斥量),进入临界区。
2、H 被唤醒,它也需要该资源,但资源被 L 占用,因此 H 被阻塞,进入等待状态(挂起)。
3、M 就绪,因为 M 的优先级介于 H 和 L 之间,且不占用该资源,所以 M 抢占了 L 的 CPU(因为 M 优先级高于 L)。
4、M 长时间运行(比如执行大量计算或延迟),而 L 因为被抢占,无法释放资源。
结果:H(最高优先级)一直在等待 L 释放资源,而 L 却被 M 抢占,无法继续执行。H 的等待时间取决于 M 的运行时间,可能无限长。
这个过程中,优先级关系被反转了:低优先级的 L 反而阻止了高优先级 H 的执行。
解决方案
解决优先级反转的常见方法有三种:
1、优先级继承(Priority Inheritance)
当低优先级任务 L 持有高优先级任务 H 所需的资源时,L 临时继承 H 的优先级,直到释放资源。这样:
- L 不会被中间优先级任务 M 抢占,能尽快完成临界区并释放资源。
- H 得到资源后,L 的优先级恢复原状。
优点:实现相对简单,只在必要时提升优先级,动态调整。
缺点:可能引入死锁(若多个资源嵌套),且不能完全避免所有反转情况(但大大缩短了阻塞时间)。
2、优先级天花板(Priority Ceiling)
每个资源(互斥量)被赋予一个 优先级天花板,其值等于可能使用该资源的所有任务的最高优先级。当任务持有该资源时,其优先级被临时提升到天花板值。这样可以防止任何中间优先级任务抢占(因为中间优先级低于天花板),从而避免反转。
优点:更简单,无需跟踪继承链,能预防死锁(如果系统设计得当)。
缺点:需要静态分析所有任务,天花板可能设置过高,导致不必要的优先级提升。
3、禁止抢占 / 关中断
在临界区内禁止任务调度或关中断,但会严重影响系统实时性,通常只用于极短临界区。
Freertos中如何避免优先级反转
- 使用互斥量保护共享资源,而不是二进制信号量。
- 确保互斥量只用于较短临界区,避免长时间持有。
- 避免嵌套互斥量,否则可能引发死锁(优先级继承也可能死锁,需注意资源获取顺序)。
- 对于复杂的资源依赖,可考虑使用优先级天花板方案(需自行实现,FreeRTOS 未直接提供)。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐
所有评论(0)