05. 支持多优先级

目录

 

1. 支持多优先级的方法

1.1 任务优先级

1.2 基于优先级的就绪列表

 1.3 实现基于优先级的调度

2. 查找最高优先级就绪任务的方法

2.1 通用方法

2.2 体系结构优化方法

3. 修改代码支持多优先级

3.1 创建任务相关

3.1.1 修改任务控制块

3.1.2 增加prvAddNewTaskToReadyList函数

3.2 修改vTaskSwitchContext函数

 3.3 修改xTaskIncrementTick函数

 3.4 修改vTaskDelay函数

4. 实验现象


1. 支持多优先级的方法

1.1 任务优先级

在FreeRTOS中,使用数字表示任务优先级,数字越小优先级越小(这点和uC/OS & RT-Thread正好相反)

 

说明:后文将看到,FreeRTOS中这种优先级设置方式是和查找优先级最高的就绪任务的方式相匹配的

1.2 基于优先级的就绪列表

/* 任务就绪列表 */
List_t pxReadyTasksLists[configMAX_PRIORITIES];

 ① 就绪列表是一个存储就绪任务TCB的列表数组,元素个数就是系统支持的优先级个数

② 数组的下标对应任务的优先级,优先级越低,对应的数组下标越小。其中空闲任务优先级最低,对应下标为0的列表

③ 任务在创建时,会根据任务的优先级将任务插入到就绪列表不同的位置;相同优先级的任务插入到就绪列表的同一个列表中

 1.3 实现基于优先级的调度

pxCurrentTCB是一个全局的TCB指针,用于指向当前正在运行任务的TCB。要想让任务支持优先级,只要解决在任务切换时让pxCurrentTCB指向最高优先级的就绪任务的TCB即可

下面就说明查找最高优先级就绪任务的方法

2. 查找最高优先级就绪任务的方法

在FreeRTOS中提供2种查找最高优先级就绪任务的方法,

① 通用方法

② 根据特定处理器优化的方法

在编译时由configUSE_PORT_OPTIMISED_TASK_SELECTION宏加以控制

2.1 通用方法

/*
* 系统当前就绪任务最高优先级
* 初始值为0,对应最低优先级的空闲任务
*/
// file: tasks.c
static volatile BaseType_t uxTopReadyPriority = tskIDLE_PRIORITY;

// file: tasks.c
/* 在系统中注册就绪任务优先级 */
#define taskRECORD_READY_PRIORITY(uxPriority) \
{ \
    if ((uxPriority > uxTopReadyPriority)) \
    { \
        uxTopReadyPriority = (uxPriority); \
    } \
}
	
/* 查找优先级最高的就绪任务 */
#define taskSELECT_HIGHEST_PRIORITY_TASK() \
{ \
    UBaseType_t uxTopPriority = uxTopReadyPriority; \
    while (listLIST_IS_EMPTY(&(pxReadyTasksLists[uxTopPriority]))) \
    { \
        --uxTopPriority; \
    } \
    listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB, \
            &(pxReadyTasksLists[uxTopPriority])); \
    uxTopReadyPriority = uxTopPriority; \
}
	
/* 在系统中注销任务优先级(通用方法为空) */
#define taskRESET_READY_PRIORITY(uxPriority)
#define portRESET_READY_PRIORITY(uxPriority, uxTopReadyPriority)

说明1:uxTopReadyPriority的含义

在通用方法中,uxTopReadyPriority用于记录当前系统中就绪任务的最高优先级,可见该方法无法体现当前系统中哪些优先级是有任务的,这种缺点就限制了通用方法的实现

 

说明2:注册就绪任务优先级

在通用方法中,注册就绪任务优先级就是比较就绪任务优先级与uxTopReadyPriority的大小,使得uxTopReadyPriority中始终记录当前系统中就绪任务的最高优先级

 

说明3:无法注销优先级

在某些情况下,任务是需要退出就绪状态的(e.g. 比如任务进入睡眠或等待资源),此时应该注销该优先级

但是在通用模式下,退出就绪状态的任务并不知道自己是否是最高优先级,所以无法更新uxTopReadyPriority变量

如果要实现更新,就必须在有任务退出就绪状态时从尾部遍历就绪列表,以便更新uxTopReadyPriority变量,但是这个代价是很大的

补充:无法注销优先级还因为要考虑支持同优先级多任务的场景,当从就绪列表注销一个任务时,可能有与该任务同优先级的任务,所以仍需要遍历就绪列表才能正确更新uxTopReadyPriority变量

 

说明4:遍历查找最高优先级任务

正是由于上面提到的不能实时更新uxTopReadyPriority变量,所以在调用taskSELECT_HIGHEST_PRIORITY_TASK时,可能uxTopReadyPriority对应的优先级并没有任务,所以需要从尾部遍历

2.2 体系结构优化方法

/*
* 系统当前就绪任务最高优先级位图
* 初始值为0,对应没有任务
*/
static volatile BaseType_t uxTopReadyPriority = tskIDLE_PRIORITY;

/* 在系统中注册就绪任务优先级 */
// file: portmacro.h
#define portRECORD_READY_PRIORITY(uxPriority, uxTopReadyPriority) \
		(uxTopReadyPriority) |= (1UL << (uxPriority));

// file: tasks.c
#define taskRECORD_READY_PRIORITY(uxPriority) \
		portRECORD_READY_PRIORITY(uxPriority, uxTopReadyPriority)
	
/* 查找优先级最高的就绪任务 */
// file: portmacro.h
#define portGET_HIGHEST_PRIORITY(uxTopPriority, uxTopReadyPriority) \
    uxTopPriority = (31UL - (uint32_t)__clz((uxTopReadyPriority)));

// file: tasks.c
#define taskSELECT_HIGHEST_PRIORITY_TASK() \
{ \
    UBaseType_t uxTopPriority; \
    portGET_HIGHEST_PRIORITY(uxTopPriority, uxTopReadyPriority); \
    listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB, \
            &(pxReadyTasksLists[uxTopPriority])); \
}

/* 在系统中注销任务优先级 */
// file: portmacro.h
#define portRESET_READY_PRIORITY(uxPriority, uxTopReadyPriority) \
		(uxTopReadyPriority) &= ~(1UL << (uxPriority));

// file: tasks.c
#define taskRESET_READY_PRIORITY(uxPriority) \
{ \
    portRESET_READY_PRIORITY(uxPriority, uxTopReadyPriority); \
}

 说明1:uxTopReadyPriority的含义

05. 支持多优先级_第1张图片

在体系结构优化方法中uxTopReadyPriority作为任务就绪位图使用,当某一位为1时,表示对应的优先级有任务就绪

此时很容易通过位操作来注册和注销任务优先级,也就解决了通用方法中的问题

 

说明2:基于clz指令的优化

优化方法的核心是使用clz指令计算uxTopReadyPriority中的先导零个数,进而计算出当前的最高优先级

基于这种优化方式,就很容易理解为什么在FreeRTOS中数字越大优先级越高,因为任务优先级在此处被映射为就绪位图中的位数

 

说明3:再看listGET_OWNER_OF_NEXT_ENTRY宏的使用

#define listGET_OWNER_OF_NEXT_ENTRY(pxTCB, pxList) \
{ \
	List_t *const pxConstList = (pxList); \
	/* 节点索引指向链表第1个节点 */ \
	(pxConstList)->pxIndex = (pxConstList)->pxIndex->pxNext; \
	if ((void *)(pxConstList)->pxIndex == \
        (void *)&((pxConstList)->xListEnd)) \
	{ \
		(pxConstList)->pxIndex = (pxConstList)->pxIndex->pxNext; \
	} \
	/* 获取节点owner */ \
	(pxTCB) = (pxConstList)->pxIndex->pvOwner; \
}

 在列表章节的笔记中曾经谈到过对该宏的使用,对该宏的正确理解应该是用于遍历列表,每调用一次,均会按序返回下一个列表项

当列表非空时,该宏会越过尾节点;如果列表为空,则会出错,因为尾节点中没有pvOwner选项,所以该宏只能在确知列表非空的情况下调用

 

说明4:使用体系结构优化方法的代价就是任务优先级一般不能超过32个,否则无法映射到32位的位图之上

 

补充:FreeRTOS中任务就绪列表视图如下图所示,可见如何将就绪列表状态映射到就绪位图中

05. 支持多优先级_第2张图片

3. 修改代码支持多优先级

3.1 创建任务相关

3.1.1 修改任务控制块

typedef struct tskTaskControlBlock
{
    // 其他字段
    UBaseType_t uxPriority; // 任务优先级
} tskTCB;

说明:由于TCB中增加了任务优先级字段,所以用于创建任务的xTaskCreateStatic & prvInitialiseNewTask函数均要增加任务优先级参数

3.1.2 增加prvAddNewTaskToReadyList函数

// file: tasks.c
/* 将pxTCB指向的任务加入就绪列表 */
#define prvAddTaskToReadyList(pxTCB) \
	taskRECORD_READY_PRIORITY((pxTCB)->uxPriority); \
	vListInsertEnd(&(pxReadyTasksLists[(pxTCB)->uxPriority]), \
	               &((pxTCB)->xStateListItem));

static void prvAddNewTaskToReadyList(TCB_t *pxNewTCB)
{
	/* 进入临界段 */
	taskENTER_CRITICAL();
	{
		/* 全局任务计数器加1 */
		uxCurrentNumberOfTasks++;
		
		if (pxCurrentTCB == NULL)
		{
			/* 如果pxCurrentTCB为空,将其指向新创建的任务 */
			pxCurrentTCB = pxNewTCB;
			
			/* 如果是第一次创建任务,需要初始化任务相关列表 */
			if (uxCurrentNumberOfTasks == (UBaseType_t)1)
			{
				prvInitialiseTaskLists();
			}
		}
		else
		{
			/* 如果pxCurrentTCB不为空,则根据任务优先级更新 */
			if (pxCurrentTCB->uxPriority <= pxNewTCB->uxPriority)
			{
				pxCurrentTCB = pxNewTCB;
			}
		}
		
		/* 将任务加入就绪列表 */
		prvAddTaskToReadyList(pxNewTCB);
	}
	/* 退出临界段 */
	taskEXIT_CRITICAL();
}

说明1:在将任务加入就绪列表的同时,修改任务就绪位图,表示该优先级已有任务就绪

 

说明2:pxCurrentTCB的更新

在创建任务的过程中,根据任务优先级更新pxCurrentTCB指针,这样在启动调度器时就可以自动运行当前优先级最高的任务,而不需要手动指定

这里需要注意的是,只有在调度器尚未开始运行时,才可以在prvAddNewTaskToReadyList函数中切换pxCurrentTCB指针的指向。如果调度器已经运行,则只能等待任务调度

3.2 修改vTaskSwitchContext函数

现在的调度变得非常简单,就是查找优先级最高的就绪任务

// file: tasks.c

void vTaskSwitchContext(void)
{
	/* 获取优先级最高的就绪任务的TCB,然后更新到pxCurrentTCB */
	taskSELECT_HIGHEST_PRIORITY_TASK();
}

 3.3 修改xTaskIncrementTick函数

// file: tasks.c

void xTaskIncrementTick(void)
{
	TCB_t *pxTCB = NULL;
	BaseType_t i = 0;
	
	const TickType_t xConstTickCount = xTickCount + 1;
	xTickCount = xConstTickCount;
	
	/* 更新任务延时计数 */
	for (i = 0; i < configMAX_PRIORITIES; i++)
	{
		pxTCB = (TCB_t *)listGET_OWNER_OF_HEAD_ENTRY(
                              &(pxReadyTasksLists[i]));
		if (pxTCB->xTicksToDelay > 0)
		{
			pxTCB->xTicksToDelay--;
			
			/* 延时到达,将任务就绪 */
			if (pxTCB->xTicksToDelay == 0)
			{
				taskRECORD_READY_PRIORITY(pxTCB->uxPriority);
			}
		}
		
		/* 触发任务切换 */
		portYIELD();
	}
}

说明:现在递减任务延时的方式仍然是遍历任务就绪列表,这样效率太低,后续将实现任务延时列表,专门用于维护处于延时状态的任务

 3.4 修改vTaskDelay函数

void vTaskDelay(const TickType_t xTicksToDelay)
{
	TCB_t *pxTCB = NULL;
	
	/* 获取当前任务TCB */
	pxTCB = pxCurrentTCB;
	
	/* 设置延时Tick值 */
	pxTCB->xTicksToDelay = xTicksToDelay;
	
	/* 将任务从就绪列表中删除 */
	taskRESET_READY_PRIORITY(pxTCB->uxPriority);
	
	/* 主动放弃CPU */
	taskYIELD();
}

说明:此处并未真的将任务从就绪列表中删除,因为xTaskIncrementTick函数还需要遍历就绪列表维护延时tick值,此处只是注销了任务优先级

设想一下,如果此时同优先级有多个任务,简单注销任务优先级也是不合理的。这就促使我们一定要实现延时列表,将需要延时的任务移出就绪列表

4. 实验现象

05. 支持多优先级_第3张图片

说明1:实验现象和上一章是相同的,差别在于现在是根据任务优先级调度,而之前是人为在Task1和Task2之间切换

 

说明2:在当前代码环境下,通用模式是不能工作的,因为我们没有将退出就绪状态的任务移出就绪列表,在调用taskSELECT_HIGHEST_PRIORITY_TASK宏从尾部遍历就绪列表时,优先级永远不会改变,导致只有一个高优先级任务被调度

在下个章节完成任务延时列表后将再次验证

你可能感兴趣的:(FreeRTOS内核实现)