前言

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

  • 调度器实现;(上一章节已描述调度基础)

  • 任务切换实现。

    • 接口层实现。

原文:李柱明博客: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; PRESERVE8 mrs 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; \
}

这样,就完成了更新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 */
}
}

【freertos】006-任务切换实现细节的更多相关文章

  1. 【原创】uC/OS II 任务切换原理

    今天学习了uC/OS II的任务切换,知道要实现任务的切换,要将原先任务的寄存器压入任务堆栈,再将新任务中任务堆栈的寄存器内容弹出到CPU的寄存器,其中的CS.IP寄存器没有出栈和入栈指令,所以只能引 ...

  2. 使用FreeRTOS进行性能和运行时分析

    在MCU on Eclipse网站上看到Erich Styger在2月25日发的博文,一篇关于使用FreeRTOS进行性能和运行分析的文章,本人觉得很有启发,特将其翻译过来以备参考.当然限于个人水平, ...

  3. FreeRTOS 临界段和开关中断

    以下转载自安富莱电子: http://forum.armfly.com/forum.php 临界段代码的临界段也称为临界区,一旦这部分代码开始执行,则不允许任何中断打断.为确保临界段代码的执行不被中断 ...

  4. 不可被忽视的操作系统( FreeRTOS )【1】

    把大多数人每个星期的双休过过成了奢侈的节假日放假,把每天23点后定义为自己的自由时间,应该如何去思考这个问题 ? 双休的两天里,不!是放假的两天里,终于有较长的时间好好的学习一下一直断断续续的Free ...

  5. FreeRTOS学习笔记——FreeRTOS 任务基础知识

    RTOS 系统的核心就是任务管理,FreeRTOS 也不例外,而且大多数学习RTOS 系统的工程师或者学生主要就是为了使用RTOS 的多任务处理功能,初步上手RTOS 系统首先必须掌握的也是任务的创建 ...

  6. HDFS 架构简述

    HDFS 架构简述 Hadoop分布式文件系统(HDFS)是一个分布式的文件系统,运行在廉价的硬件上.它与现有的分布式文件系统有很多相似之处.然而与其他的分布式文件系统的差异也是显着的.HDFS是高容 ...

  7. iOS开源加密相册Agony的实现(七)

    简介 虽然目前市面上有一些不错的加密相册App,但不是内置广告,就是对上传的张数有所限制.本文介绍了一个加密相册的制作过程,该加密相册将包括多密码(输入不同的密码即可访问不同的空间,可掩人耳目).Wi ...

  8. matlab 小波工具箱

    wavemenu --- >wavelet ---->wavelet packet1-D Matlab小波工具箱的使用1 转载▼ http://blog.sina.com.cn/s/blo ...

  9. VSC软件快捷键

    Shift + Alt + F  格式化 Ctrl+Shift+P, F1显示命令面板 Ctrl+P快速打开,进入File… Ctrl + Shift + N新窗口/实例 Ctrl + Shift + ...

随机推荐

  1. .net 底层运行机制

    1.           CLR C#.NET 平台下,代码是怎么运行的 源代码-->托管模块-->程序集-JIT->编程CPU指令 1.1     在.NET框架下,首先将源代码编 ...

  2. linux批量修改root密码脚本

    转至:https://blog.csdn.net/onionm/article/details/100514892?utm_medium=distribute.pc_relevant_download ...

  3. 60天shell脚本计划-7/12-渐入佳境

    --作者:飞翔的小胖猪 --创建时间:2021年2月26日 --修改时间:2021年3月2日 说明 每日上传更新一个shell脚本,周期为60天.如有需求的读者可根据自己实际情况选用合适的脚本,也可在 ...

  4. Android系统编程入门系列之硬件交互——无线通信WLAN

    Android系统的移动设备大多支持无线WLAN技术.利用该技术,不仅能实现互联网通信,还能实现无线定位,热点共享等远程通信功能.针对使用WLAN的不同功能,可能需要分别申请不同的权限声明,同时调用不 ...

  5. selenium+python自动化106 - 滑动 iframe 上的滚动条

    前言 页面嵌套了iframe,这个iframe又是可以滚动的,如何操作iframe上的滚动条? 示例 写一个html页面案例,源码如下 <!DOCTYPE html> <html l ...

  6. 矩池云利用ipykernel为JupyterLab添加kernel以及展示出来

    source activate myconda pip install ipykernel python -m ipykernel install --user --name myconda --di ...

  7. TP5框架中实现多条件登录

    控制器代码 //登录功能 public function login(){ $data = input(); $validate = $this->validate($data,[ 'uname ...

  8. CF772D题解

    什么阴间十进制状压 题意:给定 $ n $ 数字,求定义函数 $ G(x) $ 能够表示 满足"十进制按位与为 $ x $"的集合的平方和之和乘上 \(x\),求 \(\bigop ...

  9. SpringCloud微服务之Ribbon负载均衡(一)

    什么是微服务?什么是SpringCloud? 微服务是一种架构的模式,它提倡将一个应用程序划分成很多个微小的服务,服务与服务之间相互协调.相互配合.每个服务运行都是一个独立的进程,服务与服务之间采用轻 ...

  10. Python knife 一款伪菜刀

    Python knife ​ 一款伪菜刀. ​ 设计之初,本想只写个命令行的就可以了,但又想与众不同,想用python写代码,又不想用c#写前端(c#太卡了),万分无奈之下,找到一个替代品,Pyqt, ...