任务调度简介
调度器保证了总是在所有可运行的任务中选择具有最高优先级的任务,并将其进入运行态
根据 configUSE_PREEMPTION(使用抢占调度器) 和 configUSE_TIME_SLICING(使用时间片轮询) 两个参数的不同,FreeRTOS涉及三种不同的调度方法
- 时间片轮询的抢占式调度方法(configUSE_PREEMPTION=1,configUSE_TIME_SLICING=1)
- 不用时间片轮询的抢占式调度方法(configUSE_PREEMPTION=1,configUSE_TIME_SLICING=0)
- 协作式调度方法(configUSE_PREEMPTION=0)任务不会被强制抢占,只有当任务主动让出执行权时,调度器才会切换到其他任务执行。
什么是时间片?
FreeRTOS基础时钟的一个定时周期称为一个时间片,所以其长度由 configTICK_RATE_HZ 参数决定,默认情况下为1000HZ(也即1ms)
对于时间片轮询的抢占式调度方法,其在任务调度过程中一般满足以下两点要求
- 高优先级的任务可以抢占低优先级的任务
- 同等优先级的任务根据时间片轮流执行
对于不用时间片轮询的抢占式调度方法,其在任务调度过程中一般满足以下两点要求
- 高优先级的任务同样可以抢占低优先级的任务
- 同等优先级的任务不会按照时间片轮流执行,可能出现任务间占用处理器时间相差很大的情况
任务调度主要是由任务调度器 scheduler 负责,其由 FreeRTOS 内核管理,用户一般无需控制任务调度器,但是 FreeRTOS 也给用户提供了启动、停止、挂起和恢复三个常见的控制 scheduler 的 API 函数,具体如下所述
/**
* @brief 启动调度器
* @retval None
*/
void vTaskStartScheduler(void);
/**
* @brief 停止调度器
* @retval None
*/
void vTaskEndScheduler(void);
/**
* @brief 挂起调度器
* @retval None
*/
void vTaskSuspendAll(void);
/**
* @brief 恢复调度器
* @retval 返回是否会导致发生挂起的上下文切换(pdTRUE/pdFALSE)
*/
BaseType_t xTaskResumeAll(void);
任务状态
FreeRTOS 中任务共存在4种状态:
-
运行状态:一个任务正在被处理器执行, 如果运行RTOS 的处理器只有一个内核, 那么在任何给定时间内都只能有一个任务处于运行状态。
-
就绪状态:一个任务处于未运行状态但是既没有阻塞也没有挂起,处于就绪状态的任务当前尚未运行,但随时可以进入运行状态
下图为一个任务在四种不同状态(阻塞状态、挂起状态、就绪状态和运行状态)下完整的状态转移机制图
-
阻塞状态:一个任务正在等待某个事件发生,调用可以进入阻塞状态的API函数可以使任务进入阻塞状态,等待的事件通常为以下两种事件
- 时间相关事件:如 vTaskDelay() 或 vTaskDelayUntil(),处于运行状态的任务调用这两个延时函数就会进入阻塞状态,等待延时时间结束后会进入就绪状态,待任务调度后又会进入运行状态
- 同步相关事件:例如尝试进行读取空队列、尝试写入满队列、尝试获取尚未被释放的二值信号量等等操作都会使任务进入阻塞状态,这些同步事件会在后面的章节详细讲解
-
挂起状态:一个任务暂时脱离调度器的调度,挂起状态的任务对调度器来说不可见
- 让一个任务进入挂起状态的唯一方法是调用 vTaskSuspend() API函数
- 将一个任务从挂起状态唤醒的唯一方法是调用 vTaskResume() API函数(在中断中应调用挂起唤醒的中断安全版本vTaskResumeFromISR() API函数)
/**
* @brief 挂起某个任务
* @param pxTaskToSuspend:被挂起的任务的句柄,通过传入NULL来挂起自身
* @retval None
*/
void vTaskSuspend(TaskHandle_t pxTaskToSuspend);
/**
* @brief 将某个任务从挂起状态恢复
* @param pxTaskToResume:正在恢复的任务的句柄
* @retval None
*/
void vTaskResume(TaskHandle_t pxTaskToResume);
/**
* @brief vTaskResume的中断安全版本
* @param pxTaskToResume:正在恢复的任务的句柄
* @retval 返回退出中断之前是否需要进行上下文切换(pdTRUE/pdFALSE)
*/
BaseType_t xTaskResumeFromISR(TaskHandle_t pxTaskToResume);
四种任务状态之间的转换图:
这四种状态中, 除了运行态, 其他三种任务状态的任务都有其对应的任务状态列表
- 就绪列表:
pxReadyTasksLists[x]
, 数组的下标对应任务的优先级,优先级越低对应的数组下标越小。空闲任务的优先级最低,对应的是下标为0的链表。任务在创建的时候, 会根据任务的优先级将任务插入到就绪列表不同的位置。相同优先级的任务插入到就绪列表里面 的同一条链表中 - 阻塞列表: pxDelayedTaskList
- 挂起列表: xSuspendedTaskList
在程序中可以使用 eTaskGetState() API 函数利用任务的句柄查询任务当前处于什么状态,任务的状态由枚举类型 eTaskState 表示,具体如下所示
/**
* @brief 查询一个任务当前处于什么状态
* @param pxTask:要查询任务状态的任务句柄,NULL查询自己
* @retval 任务状态的枚举类型
*/
eTaskState eTaskGetState(TaskHandle_t pxTask);
/*任务状态枚举类型返回值*/
typedef enum
{
eRunning = 0, /* 任务正在查询自身的状态,因此肯定是运行状态 */
eReady, /* 就绪状态 */
eBlocked, /* 阻塞状态 */
eSuspended, /* 挂起状态 */
eDeleted, /* 正在查询的任务已被删除,但其 TCB 尚未释放 */
eInvalid /* 无效状态 */
} eTaskState;
源码函数命名规律
FreeRTOS源码中函数命名规律:FreeRTOS源码中各个函数并非随机命名,而是有规律的命名,这样方便使用者看到名字就能获得该函数更多的信息,其函数名一般由 ① 函数返回值类型简写,② 函数所在文件 和 ③ 函数作用名称这三部分组成
-
函数返回值类型简写主要有:
- ‘u’表示’unsigned’
- ‘c’表示’char’
- ‘s’表示’int16_t(short)’
- ‘l’表示’int32_t(long)’
- 'p’表示指针类型变量
- 'x’表示’BaseType_t’结构体和其他非标准类型的变量名
- 'uc’表示’UBaseType_t’结构体
- ‘v’表示’void’
- 'prv’表示私有函数无返回值
这些简写可以自由组合在一起,例如 ‘pc’ 表示 ‘char *’ 类型,‘uc’ 表示 ‘unsigned char’ 类型
-
函数所在文件:
- 'CoRoutine’表示该函数定义在’coroutine.c’文件中的
- 'EventGroup’表示该函数定义在’event_groups.c’文件中的
- 'List’表示该函数定义在’list.c’文件中的
- 'Queue’表示该函数定义在’queue.c’文件中的
- 'StreamBuffer’表示该函数定义在’stream_buffer.c’文件中的
- 'Task’表示该函数定义在’tasks.c’文件中的
- 'Timer’表示该函数定义在’timers.c’文件中的
- 'Port’表示该函数定义在’port.c’或’heap_x.c’文件中的
举几个例子:
- xTaskCreate表示函数返回值为 BaseType_t 结构体类型,函数被定义在 ‘tasks.c’ 文件中,函数作用为“创建”
- vTaskSuspend表示函数返回值为 void 类型,函数被定义在 ‘tasks.c’ 文件中,函数作用为“挂起”
- prvTaskIsTaskSuspended表示该函数为私有函数,仅能在 ‘tasks.c’ 文件中使用,函数作用为“判断任务是否被挂起”