• 【freertos】004-任务在内核实现细节


    前言

    后面都是已动态内存任务为例来分析。

    注意:

    • 由于当前学习是在linux上跑的freertos,对于freertos底层相关接口,从demo工程来看,都是posix标准相关。

    • 鉴于freertos多用于ARM架构,本教程涉及到硬件接口,作者会分两条路线讲解:

      • posix标准接口。
      • cortex m3/4架构相关接口。

    参考:

    本文默认按堆栈向下生长方式讲解。

    4.1 任务控制块

    详细说明各成员:

    pxTopOfStack

    • 任务栈顶指针,必须放在任务控制块首位,指向任务当前堆栈的栈顶,且总是指向最后一个压栈的项目。
    • 该值在任务切换时才会更新。

    xMPUSettings

    • 如果使用MPU,xMPUSettings必须位于结构体的第二项,用于MPU设置。

    xStateListItemxEventListItem

    • 状态链表节点和事件链表节点。
    • 这些链表主要被OS调度器使用,用于跟踪、处理任务。
    • 对于链表的学习可以百度搜索下:李柱明 链表

    uxPriority

    • 任务优先级,freertos是0为最低优先级。
    • 一般在创建任务时配置,也可以动态修改。对于动态修改,后面任务控制章节会讲述。

    pxStack

    • 任务栈底指针,指向任务堆栈的起始位置。
    • 在任务创建时就被赋值了。
    • 栈底指针pxStack被赋值后就不会改变的,而栈顶指针pxTopOfStack是会随着出入栈变化的。
    • 对于向下生长的栈,该值可用于任务栈溢出监测。在任务栈初始化时,会初始化为也给固定值,如0xA5,在切换任务时,检查该任务的栈底的几个值是否是0xA5,如果是,则可粗略判断为任务栈未溢出,如果不是,则可肯定任务栈一定异常。被踩,或溢出。

    pcTaskName

    • 任务的描述或名字,任务创建时赋值。
    • 主要用于调试分析。
    • 名字的长度由宏configMAX_TASK_NAME_LEN(位于FreeRTOSConfig.h中)指定,包含字符串结束标志。

    pxEndOfStack

    • 指向任务栈的尾部。
    • 该值在堆栈向上生长portSTACK_GROWTH > 0,或者开启记录堆栈高地址configRECORD_STACK_HIGH_ADDRESS == 1时有效。
    • 也是用任务栈溢出检测。

    uxCriticalNesting

    • 临界区嵌套深度记录值,初始为0。

    uxTCBNumber

    • 标记当前任务控制块序号,由内核决定,每个任务不同。
    • 主要用于调试。

    uxTaskNumber

    • 标记当前任务序号,但不是有内核决定,而是通过API函数vTaskSetTaskNumber()来设置的。
    • 主要用于调试。

    uxBasePriority

    • 保存任务原来的优先级。
    • 主要用于优先级继承机制。如互斥量。

    uxMutexesHeld

    • 当前任务获取到的互斥量个数。
    • 获取到一个互斥量,该值+1;释放一个互斥量,该值-1;为 0 时,优先级恢复基优先级。

    pxTaskTag

    • 任务标签。
    • 内核不使用。
    • 类型是任务钩子函数指针,主要供给用户使用。

    pvThreadLocalStoragePointers

    • 本地内存指针。
    • 其实就是在自己的任务栈里占用部分内存,并通过接口把这部分内存开放出去,让其它任务也可以使用。
    • 参考:官网

    ulRunTimeCounter:

    • 记录任务在运行状态下执行的总时间。
    • 单位:tickle。

    ulNotifiedValue

    • 任务通知值数组。

    ucNotifyState:

    • 任务通知状态数组。

    xNewLib_reent

    • 还没研究这有啥用。

    ucStaticallyAllocated

    • 标记任务是动态内存创建还是静态内存创建。
    • 静态标记为pdTURE。
    • 提供给任务回收时使用。

    ucDelayAborted

    • 打断延时标记。
    • 解除挂起时被标记为 pdTURE。

    iTaskErrno

    • 当前任务的错误码。

    4.2 创建任务源码主要内容

    主要内容:

    1. 初始化任务控制块。
    2. 初始化任务栈。与主控架构有关。就是把重要数据压栈,主要是伪造CPU寄存器组压栈现场。或者说只是伪造上文保护现场,让下次调用时恢复下文使用。
    3. 把当前任务插入就绪链表。

    参考:查看源码附加部分注释

    4.3 内存申请

    一个任务主要由三部分组成:

    1. 任务主体程序。
    2. 任务控制块。
    3. 任务栈。

    任务主体程序一般存在代码区中。

    任务控制块和任务栈需要的空间有两种方式申请:

    1. 静态申请:非freertos内部动态分配方式。
    2. 动态申请:freertos内部动态分配的方式,占用的是对应的系统堆空间。

    本次讲解的函数是动态内存创建任务。

    对于任务控制块和任务栈的空间位置顺序也是有讲究的,建议是按堆栈增长方向顺序,任务控制块在先,任务栈在后。

    这样做的目的是为了栈溢出时不会踩到任务控制块:

    • 如果堆栈向上生长,先申请任务控制块空间,再申请任务栈空间。
    • 如果堆栈向下生长,先申请任务栈空间,再申请任务控制块空间。

    申请任务控制块空间:

    申请任务栈空间:

    需要注意,如果申请失败,已申请部分需要释放空间再退出。

    4.4 任务控制块初始化

    任务控制块和任务栈都获得了合法空间,即可开始初始化。

    初始化任务控制块,按照任务控制块成员进行初始化即可。

    主要是调用prvInitialiseNewTask()API来完成任务初始化。

    4.4.1 任务栈地址保存

    任务栈地址保存到任务控制块:(这个在申请空间时实现)

    4.4.2 栈顶对齐纠正

    先获取对齐前的栈顶地址。

    再纠正栈顶地址pxTopOfStack,等等初始化任务栈伪造任务上文现场时就从这个栈顶变量pxTopOfStack指向的地址开始。

    4.4.3 保存任务名称

    保存任务名称到任务控制块,长度受限于宏configMAX_TASK_NAME_LEN

    保存时遇到0x00结束符结束或受限长度结束,并且会在受限长度末强制加上0x00结束符。

    4.4.4 任务优先级保存

    任务优先级会实现断言式校验,不能大于等于系统配置的优先级限定值configMAX_PRIORITIES

    如果优先级超出配置范围,且没有开启断言式校验,便会纠正任务优先级值,因为不纠正会存在越界访问。(就绪表是二级线性表,用数组记录各个优先级就绪链表,优先级会作为数组下标访问对应就绪链表,所以不能让优先级越界。)

    优先级校验正确,纠正后,保存到任务控制块,如果开启了互斥量功能,即是系统当前配置支持了优先级继承机制,为了实现该机制,任务控制块会有两个优先级相关的变量:

    1. pxNewTCB->uxBasePriority:任务基优先级,优先级继承机制使用。在优先级继承状态时,该值用于保存任务原有优先级。
    2. pxNewTCB->uxPriority:任务在用优先级,实时使用。这个就是任务当前状态的优先级,是根据这个优先级插入对应就绪链表进行抢占调度的。

    4.4.5 任务状态节点

    先初始化任务状态节点。后面完成任务初始前,会把当前任务,即任务状态节点插入就绪链表。

    需要设置节点归属,这样才能通过状态节点找到任务控制块。

    还需要设置任务状态节点值,就是按这个值排序的,参考任务优先级来配置该值。

    • 使用倒叙onfigMAX_PRIORITIES - uxPriority是因为链表排序采用小在前,而任务优先级采用大优先。

    任务状态节点在系统中被插入到不同链表而呈现不同的任务状态:

    1. 就绪链表。就绪态。(在跑就是运行态)
    2. 延时链表。阻塞态。
    3. 挂起链表。阻塞态或者挂起态。

    4.4.6 任务事件节点

    初始化任务状态节点,就只是初始化节点而已。还需要设置节点归属,这样才能通过事件节点找到任务控制块。

    事件节点用于把任务记录到各种事件链表中,消息队列阻塞、事件组等等。

    4.4.7 任务本地开放内存配置

    任务本地开放内存,其实就是在任务栈中取一部分空间出来,通过接口vTaskSetThreadLocalStoragePointer()pvTaskGetThreadLocalStoragePointer()开放给外部使用。

    4.4.8 其它值初始化

    参考下源码即可:

    4.5 任务栈初始化

    任务栈初始化主要有以下内容:

    1. 主要的就是未在上文现场。在调用时能正常恢复出来执行。
    2. 个人习惯配置。如有些系统喜欢在栈前标记特殊的值,用于dump时判断任务栈是否正常。

    任务栈初始化主要是伪造上文现场,与主控硬件架构有关,调用pxPortInitialiseStack()来实现,该函数返回初始化后的栈顶地址。

    先把整个任务栈初始化为固定的tskSTACK_FILL_BYTE值,方便调试和任务栈溢出和踩栈检查。

    后面读者自选posix或cortex m3其一学习即可。

    4.5.1 posix标准任务栈初始化

    因为posix标准下的freertos任务实质是线程,通过posix标准接口实现任务切换。

    所以任务栈大概内容就是创建线程,初始化线程管理数据块,指定任务栈等等。

    把线程管理数据结构Thread_t *thread;固定到栈顶,用于管理实现线程启停从而实现上层任务切换使用:

    初始化线程管理数据结构:

    初始化线程,指定线程栈:

    按照前面配置,创建线程:

    返回当前栈顶地址:

    4.5.2 cortex m3任务栈现场伪造

    前面章节已经了解了cortex m3内核架构进出异常的知识了,所以伪造现场前段按照异常压栈部分伪造,当然,对于系统任务切换来说,异常压栈的那些CPU寄存器组还不完整,需要手动完成其余CPU寄存器组压栈。

    前段栈使用:

    在伪造现场前,先安排好前面栈的用途,然后再开始伪造。

    比如我把当前栈顶的前10个字节初始化为0x55,在调试时就可以方便看到自己的任务栈尾位置;

    又比如,像posix标准一样,把前段栈用于数据管理。

    伪造现场,顺序不能随意,需要参考cortex m异常时压栈处理:

    • 硬件压栈部分:xPSR、PC、LR、R12、R3、R2、R1、R0
    • 软件压栈部分:R11、R10、R9、R8、R7、R6、R5、R4

    初始化后的栈情况参考图(图片源自野火):

    4.6 新任务插入就绪表处理

    初始化任务控制块和任务栈后,便可插入就绪链表,待调度器调度运行。

    调用prvAddNewTaskToReadyList()实现插入就绪链表。

    4.6.1 就绪表

    freertos就绪表是一个二级线性表,由数组+链表组成。

    如图:

    各级就绪链表都寄存在pxReadyTasksLists数组中,调度器检索就绪任务就是从pxReadyTasksLists数组中,从高优先级开始检索就绪任务。

    另外还有一个变量可以辅助快速检索就绪任务,uxTopReadyPriority,就是就绪任务优先级位图表。

    当某个优先级下存在任务就绪,这个值对应bit就会值一,开启该功能需要限制优先级最大值。cortex m架构的可以了解下前导零指令。

    为啥要使用数组+链表的方式?本人的认为

    • 数组寻址时间复杂度可以达到O(1),但是会浪费空间,但是对于优先级个数,占用的不多,有效控制好最大优先级即可。
    • 而二级使用链表是因为,任务数量不定,想像管理优先级一样管理任务,非常浪费空间,所以链表更加适合。

    下面处理都进入临界


    4.6.2 就绪表初始化

    如果当前新建的任务时第一个,需要初始化就绪表和赋值当前在跑任务全局变量pxCurrentTCB

    4.6.3 切换在跑任务

    新建的任务如果优先级比当前标记任务更高,而且调度器没有启动,可以立即更新该值:

    如果调度器已经启动了,那切换在跑任务的处理就应该交给调度器处理,所以先插入就绪表,退出临界再触发任务调度,触发任务调度实现如下:

    4.6.4 插入就绪表

    插入就绪链表:

    更新就绪链表最高优先级图位:

    插入对应就绪链表尾:

    • 这个函数只是一个简单的插入链表的API,数据结构的基础。但是这里的重点不是这个API,而是这个API的参数。

    • listINSERT_END( &( pxReadyTasksLists[ ( pxTCB )->uxPriority ] ), &( ( pxTCB )->xStateListItem ) );

      • 就绪链表不是一个循环双向链表,freertos的就绪链表是一个二级线性表。由数组+链表组成。
      • 由一个数组管理各级就绪链表。

    4.7 删除任务源码

    主要是释放资源。

    如果是删除自己的话,就插入到结束链表xTasksWaitingTermination

    • 因为任务调度时需要上下文切换,所以为了保证调度器能顺利切换到下一个任务,便把释放资源,删除任务的内容交给空闲任务处理。

    如果不是删除本身,立即就删除,无需经过空闲任务处理。

    处理需要进入临界处理。

    4.7.1 相关变量

    uxDeletedTasksWaitingCleanUp:这个值表示当前有多少人任务需要释放。空闲任务会检查这个值。

    xTasksWaitingTermination:结束链表。空闲任务调用 prvCheckTasksWaitingTermination()函数来检查该链表并释放资源。

    4.7.2 解除任务所有状态

    通过任务句柄获取任务控制块:

    解除任务所有状态,即是从相关状态链表中移除当前任务:

    解除事件阻塞:

    4.7.3 删除本身

    传入任务句柄为NULL,表示删除本身,但是任务调度时需要上下文切换,所以为了保证调度器能顺利切换到下一个任务,便把释放资源,删除任务的内容交给空闲任务处理。

    先把当前任务插入到结束链表xTasksWaitingTermination,更新uxDeletedTasksWaitingCleanUp,让空闲任务知道有多少个已删除的任务需要进行内存释放:

    4.7.4 删除其它任务

    删除的任务非当前在跑任务。可以在这里就做删除处理,释放资源。

    当前有效任务统计uxCurrentNumberOfTasks减一,还要重置下一个预期的解锁时间,以防它被引用被删除的任务:

    • prvResetNextTaskUnblockTime()需要在临界内处理,因为内部涉及到延时机制组件的处理,如延时链表pxDelayedTaskList、未来最近唤醒时间变量xNextTaskUnblockTime的处理,这些变量在系统节拍中断回调中用到。

    然后调用prvDeleteTCB()释放资源

    4.7.5 触发任务调度

    如果调度器没有关闭,且删除了本身,那需要触发任务调度,切换到其它有效任务:

    4.7.6 空闲任务释放被删除任务资源

    在空闲任务中调用prvCheckTasksWaitingTermination()来处理在结束链表xTasksWaitingTermination中的任务。

    需要注意的是,在系统中,需要留点CPU时间给空闲任务,要不然删除本身的任务资源久久得不到释放。

    4.7.7 释放任务空间函数prvDeleteTCB()

    不管在哪里释放资源,最终都是调用prvDeleteTCB()API来实现。

    释放资源主要是任务控制块空间和任务栈空间,前期需要先判断是否是动态分配,动态分配才能动态释放。

    先了解几个参数或宏:

    • configSUPPORT_DYNAMIC_ALLOCATION:动态分配内存宏

      • 定义为 1 :在创建 FreeRTOS的内核对象时候 所需要的 RAM 就会从 FreeRTOS 的堆中动态的获取内存。
      • 定义为 0:需要用户自行提供。
      • 默认为1。
    • configSUPPORT_STATIC_ALLOCATION:静态分配内存宏

      • 定义为1:允许静态创建任务。
      • 定义为0:不允许静态创建任务。
    • pxTCB->ucStaticallyAllocated:任务分配内存记录

      • tskDYNAMICALLY_ALLOCATED_STACK_AND_TCB:动态分配任务控制块和任务栈。
      • tskSTATICALLY_ALLOCATED_STACK_ONLY:只是静态分配了任务栈。任务控制块是动态分配的。
      • tskSTATICALLY_ALLOCATED_STACK_AND_TCB:静态分配任务控制块和任务栈。

    根据上述参数可以了解到当前任务的任务栈和任务控制块是如何分配的,把动态分配的动态释放即可。

    附件

    xTaskCreate():创建任务源码

    prvInitialiseNewTask():任务初始化函数

    pxPortInitialiseStack():POSIX标准任务栈初始化函数

    pxPortInitialiseStack():cortex m3/m4任务栈现场伪造

    prvAddNewTaskToReadyList():插入任务就绪链表函数

    vTaskDelete():删除任务源码

    prvCheckTasksWaitingTermination():空闲任务检索结束链表释放资源

    prvDeleteTCB():删除任务控制块和任务堆栈


    __EOF__

  • 本文作者: 李柱明
  • 本文链接: https://www.cnblogs.com/lizhuming/p/16072375.html
  • 关于博主: 嵌入式从业者。RTOS、Linux ...
  • 版权声明: 版权归博主所有
  • 声援博主: 学习笔记分享
  • 相关阅读:
    灰色关联度分析-详细代码和说明
    鉴源论坛 · 观辙丨汽车CAN总线渗透测试
    【Linux/Ubuntu】 部署docker时遇到的问题
    Jetson Orin平台多路 FPDlink Ⅲ相机采集套装推荐
    sklearn【F1 Scoree】F1分数原理及实战代码!
    9月《中国数据库行业分析报告》已发布,47页干货带你详览 MySQL 崛起之路!
    网络编程、socket编程、多进程并发服务器
    流行的前端开源报表工具有哪些?适合在企业级应用的
    【编译原理实验】 -- 词法分析程序设计原理与实现(C语言实现)
    【云享·人物】华为云AI高级专家白小龙:AI如何释放应用生产力,向AI工程化前行?
  • 原文地址:https://www.cnblogs.com/lizhuming/p/16072375.html