FreeRTOS任务切换分析

2023-12-16 14:10:33

目录

PendSV异常

FreeRTOS确定要运行的下一个任务

PendSV异常何时触发

个人总结


任务切换的本质:任务切换的本质就是CPU寄存器的切换(作为写过NEMU模拟器的人应该更有体会)🤣

FreeRTOS任务相关的代码占总代码的一半左右,这些代码都在为一件事情而努力,即找到优先级最高的就绪任务,并使之获得CPU运行权。任务切换是这一过程的直接实施者,为了更快找到优先级最高的就绪任务,任务切换的代码通常都是精心设计的,甚至会用到汇编指令或者硬件相关的特性,比如Cortex-M架构的指令,因此任务切换的大部分代码都是移植层提供的,不同的硬件平台,实现方法也可能不同。

FreeRTOS有两种方法触发任务切换

  • 执行系统调用,比如普通任务可以使用taskYIELD()强制任务切换,中断服务程序中使用portYIELD_FROM_ISR()强制任务切换;
  • 系统时钟节拍中断

对于Cortex-M架构,这两种方法的实质是一样的,都会使能一个PendSV中断,在PendSV中断服务程序中,找到优先级最高的就绪任务,然后让这个任务获得CPU运行权,从而完成任务切换

对于第一种任务切换方法,不管是使用taskYIELD()还是portYIELD_FROM_ISR(),最终都会执行宏portYIELD(),这个宏的定义如下:

#define portYIELD()						\
{								\
	/*产生PendSV中断*/		                        \
	portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;		\
}

这个具体的细节可以参考一代神书:Cortex-M3和M4权威指南😄PendSV 的中断由将中

断控制状态寄存器( ICSR )中 PENDSVSET 为置一触发

对于第二种任务切换方法,在系统时钟节拍中断服务函数中,首先会更新Tick计数器的值,查看是否有任务需要解除阻塞,如果有任务解除阻塞的话,则使能PendSV中断,代码如下:

void xPortSysTickHandler( void )
{
	/* 设置中断掩码 */
	vPortRaiseBASEPRI();
	{
		/* 增加tick计数器值,并检查是否有任务解除阻塞 */
		if( xTaskIncrementTick() != pdFALSE )
		{
			/* 需要任务切换。产生PendSV中断 */
			portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
		}
	}
	vPortClearBASEPRIFromISR();
}

从上面的代码中可以看出,PendSV中断的产生是通过代码:portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT实现的,它向中断状态寄存器bit28位写入1,将PendSV中断设置为挂起状态,等到优先级高于PendSV的中断执行完成后,PendSV中断服务程序将被执行,进行任务切换工作。

PendSV异常

PendSV(Pended Service Call,可挂起系统调用服务),是一个对于RTOS非常重要的异常。PendSV的中断优先级是可以编程的,用户可以根据实际的需求,对其进行配置。PendSV的中断由将中断控制状态寄存器(ICSR)中的PENDSVSET为置一触发(中断控制状态寄存器的有关内容可以参考Cortex-M3和M4权威指南这一神书)。PendSV与SVC不同,PendSV的中断是非实时的,即PendSV的中断可以在更高优先级的中断中触发,但是在更高优先级中断结束后才执行。

利用PendSV的这个可挂起特性,在设计RTOS时,可以将PendSV的中断优先级设置为最低优先级(FreeRTOS就是这么干的),这么一来,PendSV的中断服务函数就会在其它所有中断处理完之后才执行。任务切换时,就需要用到PendSV这个特性。

PendSV中断服务函数

FreeRTOS在PendSV的中断中,完成任务切换,PendSV的中断服务函数由FreeRTOS编写,将PendSV的中断服务函数定义成xPortPendSVHandler().

针对Cortex-M3和M4的函数xPortPendSVHandler()稍有不同,其主要原因在于M4单元有浮点单元,因此在进行任务切换的时候,还需要考虑是否保护和恢复浮点寄存器的值

__asm void xPortPendSVHandler( void )
{
    /*导入全局变量及函数*/
	extern uxCriticalNesting;
	extern pxCurrentTCB;
	extern vTaskSwitchContext;

	PRESERVE8  //8字节对齐

	mrs r0, psp  //R0为PSP,即当前运行任务的任务栈
	isb
	/* r3为pxCurrentTCB的地址值,即指向当前运行任务控制块的指针 */
    /* r2为pxCurrentTCB的值,即当前运行任务控制块的首地址*/
	ldr	r3, =pxCurrentTCB
	ldr	r2, [r3]

	/* Is the task using the FPU context?  If so, push high vfp registers. */
	tst r14, #0x10
	it eq
	vstmdbeq r0!, {s16-s31}

	/*将R4~R11入栈到当前运行任务的任务栈中 */
	stmdb r0!, {r4-r11, r14}

	/* R2指向的地址为此时的任务栈指针 */
	str r0, [r2]

	stmdb sp!, {r0, r3}
	mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
	msr basepri, r0
	dsb
	isb

    /* 跳转到函数 vTaskSeitchContext
     * 主要用于更新 pxCurrentTCB,
     * 使其指向最高优先级的就绪态任务
     */
	bl vTaskSwitchContext
    /* 使能所有中断 */
	mov r0, #0
	msr basepri, r0
	ldmia sp!, {r0, r3}

	 /* 注意:R3 为 pxCurrentTCB 的地址值,
     * pxCurrentTCB 已经在函数 vTaskSwitchContext 中更新为最高优先级的就绪态任务
     * 因此 R1 为 pxCurrentTCB 的值,即当前最高优先级就绪态任务控制块的首地址 */
	ldr r1, [r3]
/* R0 为最高优先级就绪态任务的任务栈指针 */
	ldr r0, [r1]

	 /* 从最高优先级就绪态任务的任务栈中出栈 R4~R11 */
	ldmia r0!, {r4-r11, r14}

	/* Is the task using the FPU context?  If so, pop the high vfp registers
	too. */
	tst r14, #0x10
	it eq
	vldmiaeq r0!, {s16-s31}

	msr psp, r0
	isb
	#ifdef WORKAROUND_PMU_CM001 /* XMC4000 specific errata */
		#if WORKAROUND_PMU_CM001 == 1
			push { r14 }
			pop { pc }
			nop
		#endif
	#endif
    /* 跳转到切换后的任务运行
 * 执行此指令,CPU 会自动从 PSP 指向的任务栈中,
 * 出栈 R0、R1、R2、R3、R12、LR、PC、xPSR 寄存器,
 * 接着 CPU 就跳转到 PC 指向的代码位置运行,
 * 也就是任务上次切换时运行到的位置
 */
	bx r14
}

从上面的代码可以看出,FreeRTOS在进行任务切换的时候,会将CPU的运行状态,在当前任务在进行任务切换前,进行保存,保存到任务栈中,然后从切换后运行任务的任务栈中恢复切换后运行任务在上一次被切换时保存的CPU信息。

但是从PendSV的中断回调函数代码中,只看到程序保存和恢复的CPU信息中的部分寄存器(R4~R11),这是因为硬件会自动将一部分其他的寄存器入栈和出栈。

在任务运行的时候, CPU 使用 PSP 作为栈空间使用,也就是使用运行任务的任务栈。当
SysTick 中断( SysTick 的中断服务函数会判断是否需要进行任务切换)发生时,在跳转到 SysTick 中断服务函数运行前,硬件会自动将除 R4~R11 寄存器的 其他 CPU 寄存器入栈,因此就将任务切换前 CPU 的部分信息保存到对应任务的任务栈中。当 退出 PendSV 时,会自动从栈空间中恢复这部分 CPU 信息,以共任务正常运行。

因此在PendSV中断服务函数中,主要要做的事情就是,保存硬件不会自动入栈的CPU信息(需要手动入栈一部分🤣) ,已经确定下一个要运行的任务,并将pxCurrentTCB指向该任务的任务控制块,然后更新PSP指针为该任务的任务堆栈指针。

这里用图说话吧,简单明了:

先强调图中的几个术语,首先是“主堆栈指针MSP”和进程堆栈指针PSP,对于Cortex-M架构,当系统复位后,默认使用MSP指针。MSP指针用于操作系统内核已经处理异常(也就是说中断服务程序中也默认强制使用MSP指针,这是硬件自动设置的)。任务(进程)使用PSP指针,操作系统负责从MSP指针切换到PSP指针。

其次,堆栈和任务堆栈也得强调一下。每个任务都有自己的任务堆栈,在任务创建时会创建指定大小的任务堆栈,这是任务能够独立运行的前提条件之一。在任务中定义的局部变量,会优先使用寄存器,寄存器不够时就使用任务堆栈空间。如果在任务中调用其它函数,则调用前的保存信息也存到任务堆栈中去。根据任务代码来估算任务堆栈的大小是件十分重要的技能。?前面也说了,Cortex-M3硬件有两个堆栈指针,操作系统内核以及异常处理程序中使用MSP指针,所以它们也需要一个堆栈空间,我们称之为“堆栈”,这个堆栈空间和任务堆栈空间在物理上是绝对不可以重叠的,下图展示了一个编译好的程序可能的RAM分配情况(堆栈向下生长)。

?有了上面的基础,接下来我们来分析PendSV中断服务程序。

mrs r0, psp 

?是将任务堆栈指针PSP的值保存到寄存器R0中,因为接下来我们会将寄存器R4~R11也保存到任务堆栈中,但是我们没有哪个汇编指令能直接操作PSP完成入栈,所以只能借助R0。

ldr	r3, =pxCurrentTCB		    /* 当前激活的任务TCB指针存入R2 */
ldr	r2, [r3]

?这两句代码是获取当前激活的任务TCP指针,指针pxCurrentTCB前面文章已经提到过很多次了,它是位于tasks.c文件中定义的唯一一个全局指针型变量,指向当前激活的任务TCB。

stmdb r0!, {r4-r11}

? 这句代码用于将寄存器R4~R11保存到当前激活的程序任务堆栈中,并且同步更新寄存器R0的值?

str r0, [r2]

?寄存器R2中保存当前激活的任务TCB指针,在《FreeRTOS高级篇2---FreeRTOS任务创建分析》中讲任务TCB数据结构时我们知道,任务TCB数据结构第一个成员一定是指向任务当前堆栈栈顶的指针变量pxTopOfStack。这句代码将R0的内容保存到任务TCB数据结构的第一个成员pxTopOfStack中,也就是将最新的任务堆栈指针保存到任务TCB的pxTopOfStack字段中。当任务被激活时,就是从这个字段中获取任务堆栈指针,然后完成数据出栈操作的。
?

stmdb sp!, {r3, r14}

?将R3和R14临时压入堆栈,因为即将调用函数vTaskSwitchContext。调用函数时,返回地址自动保存到R14中,所以一旦调用发生,R14的值会被覆盖,因此需要入栈保护。R3保存的当前激活的任务TCB指针(pxCurrentTCB)地址,函数调用后会用到,因此也要入栈保护。
?

mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY   
msr basepri, r0

? 这两句代码用来进入临界区,中断优先级号大于等于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断都会被屏蔽,屏蔽受 FreeRTOS 管理的所有中断。

bl vTaskSwitchContext

?调用函数,选择下一个要执行的任务,也就是寻找处于就绪态的最高优先级任务。变量pxCurrentTCB指向找到的任务TCB。这个函数是核心中的核心,所有的其它代码都是为了保证这个函数能正确运行。
某些运行FreeRTOS的硬件有两种方法:通用方法和特定于硬件的方法(以下简称“特殊方法”)。

1.对于通用方法:

configUSE_PORT_OPTIMISED_TASK_SELECTION设置为0或者硬件不支持这种特殊方法。
可以用于所有FreeRTOS支持的硬件。
完全用C实现,效率略低于特殊方法。
不强制要求限制最大可用优先级数目
2.对于特殊方法:

并非所有硬件都支持。
必须将configUSE_PORT_OPTIMISED_TASK_SELECTION设置为1。
依赖一个或多个特定架构的汇编指令(一般是类似计算前导零[CLZ]指令)。
比通用方法更高效。
一般强制限定最大可用优先级数目为32(0~31)。
?

FreeRTOS确定要运行的下一个任务

从上面我们可以看出,PendSV的中断服务函数中,调用了函数vTaskSwitchContext()来确定下一个要运行的任务。

函数vTaskSwitchContext()

/*-----------------------------------------------------------*/

void vTaskSwitchContext( void )
{
    /*判断任务调度器是否运行*/
	if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE )
	{
	/* 此全局变量用于在系统运行的任意时刻标记需要进行任务切换
     * 会在 SysTick 的中断服务函数中统一处理
     * 任务任务调度器没有运行,不允许任务切换,
     * 因此将 xYieldPending 设置为 pdTRUE
     * 那么系统会在 SysTick 的中断服务函数中持续发起任务切换
     * 直到任务调度器运行
     */
		xYieldPending = pdTRUE;
	}
	else
	{
        /* 可以执行任务切换,因此将 xYieldPending 设置为 pdFALSE */
		xYieldPending = pdFALSE;
        /* 用于调试,不用理会 */
		traceTASK_SWITCHED_OUT();
        /* 此宏用于使能任务运行时间统计功能,不用理会 */
		#if ( configGENERATE_RUN_TIME_STATS == 1 )
		{
			#ifdef portALT_GET_RUN_TIME_COUNTER_VALUE
				portALT_GET_RUN_TIME_COUNTER_VALUE( ulTotalRunTime );
			#else
				ulTotalRunTime = portGET_RUN_TIME_COUNTER_VALUE();
			#endif

			if( ulTotalRunTime > ulTaskSwitchedInTime )
			{
				pxCurrentTCB->ulRunTimeCounter += ( ulTotalRunTime - ulTaskSwitchedInTime );
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
			ulTaskSwitchedInTime = ulTotalRunTime;
		}
		#endif /* configGENERATE_RUN_TIME_STATS */

		/* 检查任务栈是否溢出,
         * 未定义,不用理会
         */
		taskCHECK_FOR_STACK_OVERFLOW();

		/* 此宏为 POSIX 相关配置,不用理会 */
		#if( configUSE_POSIX_ERRNO == 1 )
		{
			pxCurrentTCB->iTaskErrno = FreeRTOS_errno;
		}
		#endif

	/* 将 pxCurrentTCB 指向优先级最高的就绪态任务
     * 有两种方法,由 FreeRTOSConfig.h 文件配置决定
     */
		taskSELECT_HIGHEST_PRIORITY_TASK(); 
    /* 用于调试,不用理会 */
		traceTASK_SWITCHED_IN();

		/* 此宏为 POSIX 相关配置,不用理会 */
		#if( configUSE_POSIX_ERRNO == 1 )
		{
			FreeRTOS_errno = pxCurrentTCB->iTaskErrno;
		}
		#endif
        /* 此宏为 Newlib 相关配置,不用理会 */
		#if ( configUSE_NEWLIB_REENTRANT == 1 )
		{
			/* Switch Newlib's _impure_ptr variable to point to the _reent
			structure specific to this task.
			See the third party link http://www.nadler.com/embedded/newlibAndFreeRTOS.html
			for additional information. */
			_impure_ptr = &( pxCurrentTCB->xNewLib_reent );
		}
		#endif /* configUSE_NEWLIB_REENTRANT */
	}
}

函数vTaskSwitchContext()调用了函数taskSELECT_HIGHEST_PRIORITY_TASK(),来将pxCurrentTCB设置为指向优先级最高的就绪任务。

函数 taskSELECT_HIGHEST_PRIORITY_TASK()
函数taskSELECT_HIGHEST_PRIORITY_TASK()用于将pxCurrentTCB设置为优先级最高的就绪任务,因此该函数会使用位图的方式在任务优先级记录中查找优先级最高的任务优先等级,然后根据优先等级,到对应的就绪态任务列表中取任务运行。
FreeRTOS提供了两种从任务优先级记录中查找优先级最高任务优先等级的方式,一种是由纯C代码实现的,这种方式适用于所有运行FreeRTOS的MCU;另一种方式是使用了硬件计算前导零的指令,因此这种方式并不适用于所有运行FreeRTOS的MCU,而仅适用于具有相应硬件指令的MCU。
软件方式实现的函数taskSELECT_HIGHEST_PRIORITY_TASK()是一个宏定义,在task.c文件中由定义,具体代码如下所示:
#define taskSELECT_HIGHEST_PRIORITY_TASK()															\
	{																									\
    /*全局变量uxTopReadPriority以位图的方式记录了系统中存在任务的优先级
    *将遍历的起始优先级设置为这个全局变量
    *而无需从系统支持优先级的最大值开始遍历
    *可以节约一定的遍历时间
    */
	UBaseType_t uxTopPriority = uxTopReadyPriority;														\
																										\
		/* Find the highest priority queue that contains ready tasks. */								\       /*按照优先级从高到低,判断对应的就绪态任务列表中是否有任务
        *找到存在任务的最高优先级就绪态任务列表后,退出遍历
        */ 
		while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) )							\
		{																								\
			configASSERT( uxTopPriority );																\
			--uxTopPriority;																			\
		}																								\
																										\
		/* 从找到了就绪态任务列表中取下一个任务

        *让pxCurrentTCB指向这个任务的任务控制块
         */									\
		listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );			\
        /*更新优先级记录*/
		uxTopReadyPriority = uxTopPriority;																\
	} /* taskSELECT_HIGHEST_PRIORITY_TASK */

硬件方式实现:


	/*-----------------------------------------------------------*/

	#define taskSELECT_HIGHEST_PRIORITY_TASK()														\
	{																								\
	UBaseType_t uxTopPriority;																		\
																									\
		/* 使用硬件方式从任务优先级记录中获取最高的任务优先等级 */								\
		portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );								\
		configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 );		\
        /*从获取的任务优先级对应的就绪态任务列表中取出下一个任务
         *让pxCurrentTCB指向这个任务的任务控制块
        */
		listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );		\
	} /* taskSELECT_HIGHEST_PRIORITY_TASK() */

在使用硬件方式实现的函数taskSELECT_HIGHEST_PRIORITY_TASK()中调用了函数portGET_HIGHEST_PRIORITY()来计算任务优先级记录中的最高任务优先级,函数portGET_HIGHEST_PRIORITY()实际上是一个宏定义,具体代码如下:

	#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )
可以看到,宏 portGET_HIGHEST_PRIORITY() 使用了 __clz 这 个 硬 件 指 定 来 计 算
uxReadyPriorities 的前导零,然后使用 31 (变量 uxReadyPriorities 的最大比特位)减去得到的前
导零,那么就得到了变量 uxReadyPriorities 中,最高位 1 的比特位。使用此方法就限制了系统
最大的优先级数量不能超多 32 ,即最高优先等级位 31 ,不过对于绝大多数的应用场合, 32
任务优先级等级已经足够使用了。

PendSV异常何时触发

PendSV异常用于进行任务切换,当需要进行任务切换的时候,FreeRTOS 就会触发PendSV异常,以进行任务切换。
FreeRTOS提供了多个用于触发任务切换的宏,如下所示:
#if( configUSE_PREEMPTION == 0 )
	/* If the cooperative scheduler is being used then a yield should not be
	performed just because a higher priority task has been woken. */
	#define taskYIELD_IF_USING_PREEMPTION()
#else
	#define taskYIELD_IF_USING_PREEMPTION() portYIELD_WITHIN_API()
#endif
从上面的代码中可以看到,这些后实际上最终都是调用了函数 portYIELD() ,函数实际上是
一个宏定义,在 portmacro.h 文件中有定于,具体的代码如下所示:
#define portYIELD()																\
{																				\
	/* 设置中断控制状态寄存器,以触发PendSV异常 */								\
	portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;								\
																				\
	/* Barriers are normally not required but do ensure the code is completely	\
	within the specified behaviour for the architecture. */						\
	__dsb( portSY_FULL_READ_WRITE );											\
	__isb( portSY_FULL_READ_WRITE );											\
}

上面宏portNVIC_INT_CTRL_REG和宏portNVIC_PENDSVSET_BIT在portmacro.h文件中有定义:

#define portNVIC_INT_CTRL_REG		( * ( ( volatile uint32_t * ) 0xe000ed04 ) )
#define portNVIC_PENDSVSET_BIT		( 1UL << 28UL )

个人总结

任务切换的本质:就是CPU寄存器的切换(从NEMU学习更有体会,计算机系统基础)

假设当任务A切换到任务B时,主要分为两步:
第一步:需暂停任务A的执行,并将此时任务A的寄存器保存到任务堆栈,这个过程叫做 保存现场
第二步:将任务B的各个寄存器值(被存于任务堆栈中)恢复到CPU寄存器中,这个过程叫做 恢复现场。
对任务A保存现场,对任务B恢复现场,这个整体的过程称之为: 上下文切换。

任务切换我认为就三个工作:

保存当前任务的现场、找到要切换的任务,恢复这个要切换任务的现场?

文章来源:https://blog.csdn.net/qq_43460068/article/details/134818755
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。