【freertos】006-任务切换实现细节

前言 任务调度实现的两个核心:

  • 调度器实现;(上一章节已描述调度基础)
  • 任务切换实现。
    • 接口层实现。
原文:李柱明博客:https://www.cnblogs.com/lizhuming/p/16080202.html
6.1 任务切换基础 任务切换就是在就绪列表中寻找优先级最高的就绪任务,然后去执行该任务。
任务切换有两种方法:
  1. 手动:taskYIELD(),调用该API,强制触发任务切换。在中断中强制任务切换调用portYIELD_FROM_ISR()
  2. 系统:系统节拍时钟中断,在该中断回调里会检查是否触发任务切换。
任务切换的大概内容:
  1. 保存上文。
  2. 恢复下文。
重点:上述中不管是系统还是手动触发切换任务,都只是触发而已,最终还是根据就绪表中最高优先级任务更新到pxCurrentTCB变量,然后切换到pxCurrentTCB指向的任务。
任务切换设计接口层,会分两条主线分析:posix和cortex m
6.2 posix任务切换 任务切换原理都一样,都是暂停当前在跑的任务(保存上文),去跑下一个需要跑的任务(恢复下文)。
只是接口层不一样,实现的方式也不一样。
posix模拟器实现任务切换比较简单,任务切换接口层相关的都是基于posix线程实现,利用信号实现任务启停。
posix标准下,任务切换实现如下:
  1. 进出临界,通过pthread_sigmask()这个API实现屏蔽和解除屏蔽线程部分信号。
  2. 找出当前任务,即当前运行态的任务的线程句柄。
  3. 通过vTaskSwitchContext()找出下一个需要跑的任务。该API内部实现最主要的目的是按照调度器逻辑找出下一个需要执行的任务更新到pxCurrentTCB值。
  4. 调用prvSwitchThread()切换线程,发信号恢复需要跑的线程,让其解除阻塞。如果需要挂起的线程还没有标记结束,就进入阻塞,等待线程信号来解除阻塞。如果需要挂起的信号已经标记消亡,则直接调用pthread_exit()结束该线程。
void vPortYield( void ) { /* 进入临界 */ vPortEnterCritical(); /* 切换任务 */ vPortYieldFromISR(); /* 退出临界 */ vPortExitCritical(); }

void vPortYieldFromISR( void ) { Thread_t *xThreadToSuspend; Thread_t *xThreadToResume; /* 获取当前线程句柄 */ xThreadToSuspend = prvGetThreadFromTask( xTaskGetCurrentTaskHandle() ); /* 任务切换处理,更新pxCurrentTCB值 */ vTaskSwitchContext(); /* 获取下一个需要跑的线程句柄 */ xThreadToResume = prvGetThreadFromTask( xTaskGetCurrentTaskHandle() ); /* 切换进去 */ prvSwitchThread( xThreadToResume, xThreadToSuspend ); }

6.3 cortex m3任务切换 不管是手动还是系统触发任务切换,其任务切换都是在PendSV异常回调中实现。
切换任务过程:
  1. 触发任务切换异常后,部分CPU寄存器硬件使用PSP压栈:xPSR、PC、LR、R12、R3-R0。
  2. 进入异常后,CPU使用MSP。
  3. 把剩余部分寄存器R11-R4,通过软件使用PSP压栈。
  4. 进入临界区。
  5. 调用vTaskSwitchContext()函数找出下一个要执行的任务更新到pxCurrentTCB
  6. 退出临界。
  7. 通过pxCurrentTCB获取到新的任务栈顶。
  8. 使用新的任务栈顶指针出栈R11-R4。
  9. 更新当前任务栈顶指针到PSP。
  10. 退出异常,硬件使用PSP出栈xPSR、PC、LR、R12、R3-R0。
  11. 进入新的任务了。
代码实现参考:
__asm void xPortPendSVHandler(void) { extern uxCriticalNesting; extern pxCurrentTCB; /* 指向当前激活的任务 */ extern vTaskSwitchContext; PRESERVE8mrs r0, psp/* PSP内容存入R0 */ isb /* 指令同步隔离,清流水线 */ldr r3, = pxCurrentTCB /* 当前激活的任务TCB指针存入R2 */ ldr r2,[r3]stmdb r0 !,{r4 - r11} /* 保存剩余的寄存器,异常处理程序执行前,硬件自动将xPSR、PC、LR、R12、R0-R3入栈 */ str r0,[r2] /* 将新的栈顶保存到任务TCB的第一个成员中 */stmdb sp !,{r3, r14} /* 将R3和R14临时压入堆栈,因为即将调用函数vTaskSwitchContext,调用函数时,返回地址自动保存到R14中,所以一旦调用发生,R14的值会被覆盖,因此需要入栈保护; R3保存的当前激活的任务TCB指针(pxCurrentTCB)地址,函数调用后会用到,因此也要入栈保护*/ mov r0,#configMAX_SYSCALL_INTERRUPT_PRIORITY /* 进入临界区 */ msr basepri,r0 dsb /* 数据和指令同步隔离 */ isb bl vTaskSwitchContext /* 调用函数,寻找新的任务运行,通过使变量pxCurrentTCB指向新的任务来实现任务切换 */ mov r0,#0 /* 退出临界区*/ msr basepri,r0 ldmia sp !, {r3, r14} /* 恢复R3和R14*/ldr r1,[r3] ldr r0, [r1] /* 当前激活的任务TCB第一项保存了任务堆栈的栈顶,现在栈顶值存入R0*/ ldmia r0 !,{r4 - r11} /* 出栈*/ msr psp,r0 isb bx r14 /* 异常发生时,R14中保存异常返回标志,包括返回后进入线程模式还是处理器模式、使用PSP堆栈指针还是MSP堆栈指针,当调用 bx r14指令后,硬件会知道要从异常返回,然后出栈,这个时候堆栈指针PSP已经指向了新任务堆栈的正确位置,当新任务的运行地址被出栈到PC寄存器后,新的任务也会被执行。*/ nop }

6.4 任务切换:vTaskSwitchContext() 不同的接口层实现任务切换,都需要调用内核层vTaskSwitchContext()检索出新的的pxCurrentTCB值,并在接口层切进去。
6.4.1 检查调度器状态
切换任务时,需要检查调度器是否正常,正常才会检索出新的任务到pxCurrentTCB
如果调度器被挂起,标记下xYieldPendingpdTRUE
xYieldPending这个标记表示,在恢复调度器或下次系统节拍时(调度器已恢复正常)情况下,触发一次上下文切换。
if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE ) /* 挂起调度器就不允许任务切换. */ { /* 带中断保护的API函数的都会有一个参数"xHigherPriorityTaskWoken",若是用户没有使用这个参数,这里设置任务切换标志。在下个系统中断服务例程中,会检查xYieldPending的值,若是为pdTRUE则会触发一次上下文切换。*/ xYieldPending = pdTRUE; }

如果调度器正常,便需要标记xYieldPendingpdFALSE,表示下次触发任务切换不需要检查该值进行强制切换。
6.4.2 任务运行时间统计处理
如果开启了configGENERATE_RUN_TIME_STATS宏,表示开启了任务运行时间统计。
任务运行的时间统计在任务切换时处理,其简要原理是在任务切入时开始计时,任务切出时结束本次任务运行计时,把运行时长累加到pxCurrentTCB->ulRunTimeCounter记录下来。
注意,这里的时间值不要和系统节拍混淆,这两个时间值在两个独立的时间域里各自维护的。
获取当前时间值的函数由用户实现(因为这个时间域提供的时间系统是由用户指定实现的),通过下面两个宏函数之一实现获取当前时间值:
  1. portALT_GET_RUN_TIME_COUNTER_VALUE()
  2. portGET_RUN_TIME_COUNTER_VALUE()
切出旧任务时,把旧任务本次跑的时间累加到pxCurrentTCB->ulRunTimeCounter
同时,切入新的任务时,保存下切入任务时的时间点到ulTaskSwitchedInTime,用于切出统计时间。
综上可得:
/* 任务运行时间统计功能 */ #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/* 将任务运行的时间添加到到目前为止的累计时间中。 任务开始运行的时间存储在ulTaskSwitchedInTime中。 注意,这里没有溢出保护,所以计数值只有在计时器溢出之前才有效。 对负值的防范是为了防止可疑的运行时统计计数器实现——这些实现是由应用程序而不是内核提供的。*/ */ if( ulTotalRunTime > ulTaskSwitchedInTime ) { pxCurrentTCB->ulRunTimeCounter += ( ulTotalRunTime - ulTaskSwitchedInTime ); } else { mtCOVERAGE_TEST_MARKER(); } /* 保存当前时间 */ ulTaskSwitchedInTime = ulTotalRunTime; } #endif /* configGENERATE_RUN_TIME_STATS */

6.4.3 栈溢出检查
任务切换时会对任务栈进行检查,是否溢出或者是否被踩。
/* 栈溢出检查 */ taskCHECK_FOR_STACK_OVERFLOW();

有两种方案可检查栈溢出,可同时使用:(以堆栈向下生长为例)
  1. 方案1:检查任务栈顶指针。如果任务上文压栈后,任务栈顶pxCurrentTCB->pxTopOfStack比栈起始pxCurrentTCB->pxStack还小,说明已经栈溢出了。
  2. 方案2:栈起始内容检查。初始化时,把任务栈其实pxCurrentTCB->pxStack一部分栈内存初始化为特定的值。在每次任务切换时,检查下这几个值是否为原有值,如果不是,说明被踩栈了;如果不是,可初步判断任务战安全(不能绝对判断当前任务栈安全)。
    • 这部分内容需要用户在vApplicationStackOverflowHook()内实现。
参考代码:
  • portSTACK_LIMIT_PADDING值用于偏移,缩少任务栈安全范围。
#if ( ( configCHECK_FOR_STACK_OVERFLOW == 1 ) && ( portSTACK_GROWTH < 0 )/* 向下生长 */ #define taskCHECK_FOR_STACK_OVERFLOW()\ {\ /* 当前保存的堆栈指针是否在堆栈限制内 */\ if( pxCurrentTCB->pxTopOfStack <= pxCurrentTCB->pxStack + portSTACK_LIMIT_PADDING )\ {\ vApplicationStackOverflowHook( ( TaskHandle_t ) pxCurrentTCB, pxCurrentTCB->pcTaskName ); \ }\ } #ednif

6.4.4 检索就绪表发掘新任务
freertos就绪表是一个二级线性表,由数组+链表组成。
各级就绪链表都寄存在pxReadyTasksLists数组中,调度器检索就绪任务就是从pxReadyTasksLists数组中,从最高优先级就绪链表开始检索就绪任务。
从最高优先级的就绪链表开始检索,找到所有就绪任务中最高优先级的就绪链表。
然后检索这个优先级的就绪链表:
  • 如果这个优先级只有一个就绪任务,就把这个就绪任务更新到pxCurrentTCB
  • 如果这个优先级不止一个就绪任务,就把这个链表索引指向的任务的下一个任务更新到pxCurrentTCB
    • 这点就是freertos时间片的机制,伪时间片,因为这样的实现导致freertos默认每个同级任务只有一人时间片。
#define taskSELECT_HIGHEST_PRIORITY_TASK()\ {\ /* 从就绪列表数组中找出最高优先级列表*/\ while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopReadyPriority ] ) ) )\ {\ configASSERT( uxTopReadyPriority ); \ --uxTopReadyPriority; \ }\ \ /* 相同优先级的任务使用时间片共享处理器就是通过这个宏实现*/\ listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB, &( pxReadyTasksLists[ uxTopReadyPriority ] ) ); \ } /* taskSELECT_HIGHEST_PRIORITY_TASK *#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList )\ {\ List_t * const pxConstList = ( pxList ); \ /* 获取所有指向的下一个任务到pxTCB,并更新当前链表索引。*/\ ( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \ if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) ) \ {\ ( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \ }\ ( pxTCB ) = ( pxConstList )->pxIndex->pvOwner; \ }

【【freertos】006-任务切换实现细节】这样,就完成了更新pxCurrentTCB值,这个值就是需要切入的新任务的任务句柄值。
附件 任务切换内核层:vTaskSwitchContext()
void vTaskSwitchContext( void ) { if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE ) /* 挂起调度器就不允许任务切换. */ { /* 带中断保护的API函数的都会有一个参数"xHigherPriorityTaskWoken",若是用户没有使用这个参数,这里设置任务切换标志。在下个系统中断服务例程中,会检查xYieldPending的值,若是为pdTRUE则会触发一次上下文切换。*/ xYieldPending = pdTRUE; } else { 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/* 将任务运行的时间添加到到目前为止的累计时间中。 任务开始运行的时间存储在ulTaskSwitchedInTime中。 注意,这里没有溢出保护,所以计数值只有在计时器溢出之前才有效。 对负值的防范是为了防止可疑的运行时统计计数器实现——这些实现是由应用程序而不是内核提供的。*/ */ if( ulTotalRunTime > ulTaskSwitchedInTime ) { pxCurrentTCB->ulRunTimeCounter += ( ulTotalRunTime - ulTaskSwitchedInTime ); } else { mtCOVERAGE_TEST_MARKER(); } /* 保存当前时间 */ ulTaskSwitchedInTime = ulTotalRunTime; } #endif /* configGENERATE_RUN_TIME_STATS *//* 栈溢出检查 */ taskCHECK_FOR_STACK_OVERFLOW(); /* 在切换当前运行的任务之前,保存其errno*/ #if ( configUSE_POSIX_ERRNO == 1 ) { pxCurrentTCB->iTaskErrno = FreeRTOS_errno; } #endif/* 选出下一个需要跑的任务. */ taskSELECT_HIGHEST_PRIORITY_TASK(); traceTASK_SWITCHED_IN(); /* 切换到新任务后,更新全局errno */ #if ( configUSE_POSIX_ERRNO == 1 ) { FreeRTOS_errno = pxCurrentTCB->iTaskErrno; } #endif#if ( configUSE_NEWLIB_REENTRANT == 1 ) { /* 略 */ _impure_ptr = &( pxCurrentTCB->xNewLib_reent ); } #endif /* configUSE_NEWLIB_REENTRANT */ } }

    推荐阅读