• 【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()结束该线程。

    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. 进入新的任务了。

    代码实现参考:

    6.4 任务切换:vTaskSwitchContext()

    不同的接口层实现任务切换,都需要调用内核层vTaskSwitchContext()检索出新的的pxCurrentTCB值,并在接口层切进去。

    6.4.1 检查调度器状态

    切换任务时,需要检查调度器是否正常,正常才会检索出新的任务到pxCurrentTCB

    如果调度器被挂起,标记下xYieldPendingpdTRUE

    xYieldPending这个标记表示,在恢复调度器或下次系统节拍时(调度器已恢复正常)情况下,触发一次上下文切换。

    如果调度器正常,便需要标记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,用于切出统计时间。

    综上可得:

    6.4.3 栈溢出检查

    任务切换时会对任务栈进行检查,是否溢出或者是否被踩。

    有两种方案可检查栈溢出,可同时使用:(以堆栈向下生长为例)

    1. 方案1:检查任务栈顶指针。如果任务上文压栈后,任务栈顶pxCurrentTCB->pxTopOfStack比栈起始pxCurrentTCB->pxStack还小,说明已经栈溢出了。

    2. 方案2:栈起始内容检查。初始化时,把任务栈其实pxCurrentTCB->pxStack一部分栈内存初始化为特定的值。在每次任务切换时,检查下这几个值是否为原有值,如果不是,说明被踩栈了;如果不是,可初步判断任务战安全(不能绝对判断当前任务栈安全)。

      • 这部分内容需要用户在vApplicationStackOverflowHook()内实现。

    参考代码:(例子方案的条件可以结合使用)

    • portSTACK_LIMIT_PADDING值用于偏移,缩少任务栈安全范围。
    • 方案1:检查任务栈顶指针。
    • 方案2:栈起始内容检查。

    6.4.4 检索就绪表发掘新任务

    freertos就绪表是一个二级线性表,由数组+链表组成。
    各级就绪链表都寄存在pxReadyTasksLists数组中,调度器检索就绪任务就是从pxReadyTasksLists数组中,从最高优先级就绪链表开始检索就绪任务。

    从最高优先级的就绪链表开始检索,找到所有就绪任务中最高优先级的就绪链表。

    然后检索这个优先级的就绪链表:

    • 如果这个优先级只有一个就绪任务,就把这个就绪任务更新到pxCurrentTCB

    • 如果这个优先级不止一个就绪任务,就把这个链表索引指向的任务的下一个任务更新到pxCurrentTCB

      • 这点就是freertos时间片的机制,伪时间片,因为这样的实现导致freertos默认每个同级任务只有一人时间片。

    这样,就完成了更新pxCurrentTCB值,这个值就是需要切入的新任务的任务句柄值。

    附件

    任务切换内核层:vTaskSwitchContext()


    __EOF__

  • 本文作者: 李柱明
  • 本文链接: https://www.cnblogs.com/lizhuming/p/16080202.html
  • 关于博主: 嵌入式从业者。RTOS、Linux ...
  • 版权声明: 版权归博主所有
  • 声援博主: 学习笔记分享
  • 相关阅读:
    一文带你搞懂环绕通知@Around与最终通知@After的实现
    设计模式概述
    6月抖音和小红书达人涨粉榜单,这两个平台的粉丝喜欢看这些内容
    C++--STL总结
    C# Kafka重置到最新的偏移量,即从指定的Partition订阅消息使用Assign方法
    代理IP与Socks5代理:网络工程师的神奇魔法棒
    Java基础复习巩固(二)
    javaweb--jsp
    【Linux】7.0 信号
    vite下,修改node_modules源码后,在浏览器源码中看不到改动的内容
  • 原文地址:https://www.cnblogs.com/lizhuming/p/16080202.html