FreeRTOS-任务切换源码分析

前面分析了启动任务调度器的源码,在创建好空闲任务、启动滴答定时器(systick)后就调用SVC中断跳转到任务去执行,但作为OS我们一般不会只创建一个任务,都是有多任务需求才使用OS,后续任务一旦多了,就涉及了到了任务切换,这也是OS的核心,如何根据任务的优先级的和当前状态来切换任务,怎么保证每个任务都有执行的机会,这就是调度器做的工作,而调度器的核心就是任务切换,任务切换工作都在是PendSV中断中进行的,在执行一次系统调用或systick定时器中断里面定时触发一次PendSV中断就会产生一次任务上下文切换。为什么使用PendSV中断来进行任务切换,在Cortex-M3权威手册中的解释非常清晰:

PendSV(可悬起的系统调用),它和 SVC 协同使用。一方面, SVC异常是必须立即得到响应的(若因优先级不比当前正处理的高, 或是其它原因使之无法立即响应, 将上访成硬 fault——译者注), 应用程序执行 SVC 时都是希望所需的请求立即得到响应。另一方面, PendSV 则不同,它是可以像普通的中断一样被悬起的(不像 SVC 那样会上访)。 OS 可以利用它“缓期执行” 一个异常——直到其它重要的任务完成后才执行动作。 悬起 PendSV 的方法是: 手工往 NVIC 的PendSV 悬起寄存器中写 1。 悬起后, 如果优先级不够高,则将缓期等待执行。PendSV 的典型使用场合是在上下文切换时(在不同任务之间切换)。 例如, 一个系统中有两个就绪的任务,上下文切换被触发的场合可以是:
1、执行一个系统调用
2、系统滴答定时器( SYSTICK)中断,(轮转调度中需要)
让我们举个简单的例子来辅助理解。假设有这么一个系统,里面有两个就绪的任务,并且通过 SysTick 异常启动上下文切换。如下图所示。
FreeRTOS-任务切换源码分析_第1张图片
上图是两个任务轮转调度的示意图。但若在产生 SysTick 异常时正在响应一个中断,则SysTick 异常会抢占其 ISR。在这种情况下, OS 不得执行上下文切换,否则将使中断请求被延迟,而且在真实系统中延迟时间还往往不可预知——任何有一丁点实时要求的系统都决不能容忍这种事。因此,在 CM3 中也是严禁没商量——如果 OS 在某中断活跃时尝试切入线程模式,将触犯用法 fault 异常。
FreeRTOS-任务切换源码分析_第2张图片
为解决此问题,早期的 OS 大多会检测当前是否有中断在活跃中,只有没有任何中断需要响应时,才执行上下文切换(切换期间无法响应中断)。然而,这种方法的弊端在于,它可以把任务切换动作拖延很久(因为如果抢占了 IRQ,则本次 SysTick 在执行后不得作上下文切换,只能等待下一次 SysTick 异常),尤其是当某中断源的频率和 SysTick 异常的频率比较接近时,会发生“共振”。现在好了,PendSV 来完美解决这个问题了。PendSV 异常会自动延迟上下文切换的请求,直到其它的 ISR 都完成了处理后才放行。为实现这个机制,需要把 PendSV 编程为最低优先级的异常。如果 OS 检测到某 IRQ 正在活动并且被SysTick 抢占,它将悬起一个 PendSV 异常,以便缓期执行上下文切换。如下图所示
FreeRTOS-任务切换源码分析_第3张图片
其中事件的流水账记录如下:
1、任务 A 呼叫 SVC 来请求任务切换(例如,等待某些工作完成)
2、OS 接收到请求,做好上下文切换的准备,并且 pend 一个 PendSV 异常。
3、当 CPU 退出 SVC 后,它立即进入 PendSV,从而执行上下文切换。
4、当 PendSV 执行完毕后,将返回到任务 B,同时进入线程模式。
5、发生了一个中断,并且中断服务程序开始执行
6、在 ISR 执行过程中,发生 SysTick 异常,并且抢占了该 ISR。
7、OS 执行必要的操作,然后 pend 起 PendSV 异常以作好上下文切换的准备。
8、当 SysTick 退出后,回到先前被抢占的 ISR 中, ISR 继续执行
9、ISR 执行完毕并退出后, PendSV 服务例程开始执行,并且在里面执行上下文切换
10、当 PendSV 执行完毕后,回到任务 A,同时系统再次进入线程模式。

前面提到了任务切换的触发场合:
1、执行一个系统调用
2、系统滴答定时器( SYSTICK)中断,(轮转调度中需要)
系统调用和systick中断服务函数都是通过写寄存器来实现挂起PendSV中断来切换任务上下文的,先看一下systick中断服务函数的实现:

void SysTick_Handler(void)
{	
	/*  xTaskGetSchedulerState 解析在下面*/
    if(xTaskGetSchedulerState()!=taskSCHEDULER_NOT_STARTED)
    {
    	/* 调度器已经开启,调用处理函数 */
        xPortSysTickHandler();	
    }
}

BaseType_t xTaskGetSchedulerState( void )
	{
	BaseType_t xReturn;

		if( xSchedulerRunning == pdFALSE )
		{
			/* 如果调度器还没开启,返回调度器未启动状态 */
			xReturn = taskSCHEDULER_NOT_STARTED;
		}
		else
		{
			/* 调度器已经开启 */
			if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
			{
				/* 调度器未处于休眠状态(默认为pdFALSE),返回调度器已启动状态 */
				xReturn = taskSCHEDULER_RUNNING;
			}
			else
			{
				/* 调度器处于休眠状态,返回调度器休眠状态 */
				xReturn = taskSCHEDULER_SUSPENDED;
			}
		}
		/* 返回状态值 */
		return xReturn;
	}

上面代码在进入systick中断服务函数后先判断调度器当前的状态,在运行状态下的话会去调用处理函数,如下:

void xPortSysTickHandler( void )
{
	/* 关中断 */
	vPortRaiseBASEPRI();
	{
		/* 计数值递增,这个函数在时间管理章节再分析 */
		if( xTaskIncrementTick() != pdFALSE )
		{
			/* 往中断控制及状态寄存器ICSR(地址:0xE000_ED04)的bit28写1挂起一次PendSV中断 */
			portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
		}
	}
	/* 开中断 */
	vPortClearBASEPRIFromISR();
}

在判断到需要进行一次任务调度,就会挂起一次PendSV中断,由于PendSV的中断优先级最低,所以在执行完所有中断操作后就会进入PendSV的中断服务函数:

__asm void xPortPendSVHandler( void )
{
	extern uxCriticalNesting;
	extern pxCurrentTCB;
	extern vTaskSwitchContext;

	PRESERVE8

	/* 获取当前psp(进程堆栈)地址到r0 */
	mrs r0, psp
	isb

	/* 获取当前任务控制块指针到r3*/
	ldr	r3, =pxCurrentTCB		
	ldr	r2, [r3]

	/* 将当前堆栈的r4-r11手动压栈,其它寄存器在进入中断时自动压栈了 */
	stmdb r0!, {r4-r11}		
	/* *r2 = r0 把当前psp的地址保存到当前任务控制块的堆栈指针中*/
	str r0, [r2]			

	/* 下面的操作可能修改r3,r14,所以将r3和r14先压栈 */
	stmdb sp!, {r3, r14}
	/* 关中断 */
	mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
	msr basepri, r0
	dsb
	isb
	/* 进行任务切换,经过这步, pxCurrentTCB(由r3保存着)这个变量变为将要切换的任务控制块指针了*/
	bl vTaskSwitchContext
	/* 开中断 */
	mov r0, #0
	msr basepri, r0
	/* 前面将r3和14压栈,这里出栈返回 */
	ldmia sp!, {r3, r14}

	/* 从新的任务控制块中获取出新的栈psp */
	ldr r1, [r3]
	ldr r0, [r1]			
	/* 从新的任务堆栈中将r4-r11手动出栈,其它寄存器会在退出中断时自动出栈  */
	ldmia r0!, {r4-r11}			
	/* 更新进程栈psp为切换后的任务堆栈 */
	msr psp, r0
	isb
	/* 跳转到新的任务去执行 */
	bx r14
	nop
}

void vTaskSwitchContext( void )
{
	if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE )
	{
		/* 调度器处于休眠状态不允许任务切换 */
		xYieldPending = pdTRUE;
	}
	else
	{
		xYieldPending = pdFALSE;
		traceTASK_SWITCHED_OUT();

		/* 开启了栈溢出检查 */
		taskCHECK_FOR_STACK_OVERFLOW();
		
		/* 找出最高优先级任务,找出后赋值给pxCurrentTCB和更新uxTopReadyPriority */
		taskSELECT_HIGHEST_PRIORITY_TASK();
		traceTASK_SWITCHED_IN();
	}
}

上面找出最高优先级任务有两种方式,一种通用方式(使用软件来实现的话所有处理器都适用),一种硬件方式(需要CPU支持),当宏 configUSE_PORT_OPTIMISED_TASK_SELECTION 为0时使用通用方式,当为1时使用硬件方式。
通用方式的定义如下:

/* 从就绪列表中找到最高优先级任务 */
	#define taskSELECT_HIGHEST_PRIORITY_TASK()															\
	{																									\
	/* uxTopReadyPriority 在每次把任务添加到就绪列表的时候会更新 */           								\
	UBaseType_t uxTopPriority = uxTopReadyPriority;														\		
																										\																									
		/* 一个优先级一个列表,查看当前最高优先级就绪列表下是否有任务 */	                                	\
		while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) )							\
		{																								\
			configASSERT( uxTopPriority );																\
			/* 如果当前最高优先级就绪列表没任务就查看下一个优先级列表 */      								    \
			--uxTopPriority;																			\
		}																								\
																										\
		/* 获取下一个优先级最高任务的任务控制块 */															\
		listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );			\
		uxTopReadyPriority = uxTopPriority;																\
	} 

硬件方式:

#define taskSELECT_HIGHEST_PRIORITY_TASK()														\
	{																								\
	UBaseType_t uxTopPriority;																		\
																									\
		portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );								\
		configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 );		\
		listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );		\
	}
/* cortex处理器带有计算前导0个数指令:CLZ */
#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )

硬件方式使用前导0个数CLZ指令来找出最高优先级任务,使用硬件方法的时候 uxTopPriority 就不代表处于就绪态的最高优先级了,而是使用每个bit代表一个优先级,bit0代表优先级0,bit31代表优先级31,当某个优先级有就绪任务的话就将其对应的bit置1,那么使用硬件方法的话最多只能有32个优先级。__clz( ( uxReadyPriorities ) 就是计算 uxReadyPriorities 的前导零个数,前导零个数就是指从最高为开始(bit31)到第一个为1的bit,其间0的个数,如下:
二进制 1000 0000 0000 0000 的前导零个数为0
二进制 0001 0001 0000 0000 的前导零个数为3
得到 uxReadyPriorities 的前导零个数以后再用31减去这个前导零个数得到的就是处于就绪态的最高优先级了,比如优先级30为此时处于就绪态的最高优先级,30的前导零个数为1,即 31-1=30,得到就绪态的最高优先级为30。
得到就绪态的最高优先级后,使用下面这个函数(listGET_OWNER_OF_NEXT_ENTRY)来找到这个就绪优先级列表中下一个要执行的任务:

#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList )										\
{																							\
List_t * const pxConstList = ( pxList );													\
	/* pxIndex 为上一个任务索引,下一个要执行的即pxNext */										\
	( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;							\
	if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) )	\
	{																						\
	/* 由于是环形列表,切默认有一个结束项(xListEnd),如果pxIndex刚好为最后一项,则再指向后面一项, */\
		( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;						\
	}																						\
	( pxTCB ) = ( pxConstList )->pxIndex->pvOwner;											\
}

找到要执行的任务后获取它的任务控制块赋值给 pxCurrentTCB ,这样 xPortPendSVHandler 中的 r3 就会更新为最新的任务控制块,从任务控制块中获取出任务堆栈后跳到相应的任务去执行。上面的systick中断服务函数中只有 xTaskIncrementTick 这个函数返回 pdTRUE 才会触发一次 PendSV 中断,这个函数留到时间管理章节再分析。另外执行系统调用也会触发PendSV 中断,实际上就是某个接口函数对寄存器操作的封装,如下:

#define portYIELD()																\
{																				\
	portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;								\
	__dsb( portSY_FULL_READ_WRITE );											\
	__isb( portSY_FULL_READ_WRITE );											\
}

很多情况下都会调用这个接口,比如在任务创建完后,如果调度器是处于运行状态下的,就会在创建完任务后强制执行一次任务调度,还有一些其他其它会调用到,后面遇到再进行分析。

你可能感兴趣的:(FreeRTOS)