获取 FreeRTOS 栈空间大小及其高水位线
概述
如在FreeRTOS 创建任务的博客中所述的那样,RTOS 中的任务需要一块存储空间存储 TaskCode 中的临时变量。在创建任务时,可以通过函数中的 StackDepth 指定该空间的大小:
BaseType_t xTaskCreate(TaskFunction_t pvTaskCode, const char *const pcName, const uint32_t usStackDepth, void *const pvParameters, UBaseType_t uxPriority, TaskHandle_t *const pxCreatedTask)
通常 stack 使用 RAM 上的一块存储空间,考虑到 RAM 空间极为有限,stack size 的配置大小就显得极为重要了。
若 stack size 配置的太小,当 TaskCode 中局部变量过多时容易造成存储空间不足,出现”栈溢出“的风险,导致任务出现异常。
若 stack size 配置的太大,则容易对有限的 RAM 空间造成浪费。
因此,本节对评估 Task 的 stack size 的一些方法进行总结,期望大家通过实践写出即节省 RAM 又稳定可靠的 Task Code.
需求及功能解析
每个任务都有自己的堆栈(简称栈、stack),堆栈的总大小在创建任务的时候就确定了,下述两个函数用于检查任务从创建好到现在的**历史剩余最小值,**这个值越小说明任务堆栈溢出的可能性就越大!FreeRTOS 把这个历史剩余最小值叫做“高水位线(High Water Mark,简称 HWM)”。该部分函数相对来说会多耗费一点时间,所以在代码调试阶段可以使用,产品发布的时候最好不要使用。评估 Task 的 stack size 可以使用下述两个函数:
1)
/*
* 参数:要查询的任务的任务句柄,当这个参数为 NULL 的话说明查询自身任务(即调用函数 uxTaskGetStackHighWaterMark()的任务)的“高水位线”。
*/
UBaseType_t uxTaskGetStackHighWaterMark(TaskHandle_t xTask)
返回值:
在标准的 FreeRTOS 中上述函数返回的值是以字表示的高水位线(例如,在32位机器上,返回值1表示堆栈的4个字节未使用)。如果返回值为零,则任务可能已溢出其堆栈。如果返回值接近于零,则任务已接近溢出其堆栈。*在 ESP-IDF 中注意返回的是 字节 而不是字。*
2)
configSTACK_DEPTH_TYPE uxTaskGetStackHighWaterMark2(TaskHandle_t xTask)
uxTaskGetStackHighWaterMark2()是uxTaskGetStackHighWaterMark()的一个版本,它返回用户可定义的类型,以消除8位体系结构上UBaseType_t类型的数据类型宽度限制。
任务使用的堆栈将随着任务的执行和中断的处理而增长和收缩。uxTaskGetStackHighWaterMark()返回自任务开始执行以来任务可用的最小剩余堆栈空间量,即任务堆栈达到最大(最深)值时未使用的堆栈量。这就是所谓的烟囱“高水位线”。
注意:返回值更新的前提是有新的最小值出现,否则即便初始化新的临时变量,可能也不会刷新这个 HighWaterMark 的值(因为 stack size 减小了,但是没有到刷新最小值的地步)。
注意:Task 本身的 handle、状态管理不消耗这个 stack size(内部用的是malloc),但是 printf() 这个函数是消耗 stack size 的。
感兴趣的老铁可以增加临时变量的 size,观察该函数的变化。
示例解析
log 输出如下:
This is esp32 chip with 2 CPU core(s), WiFi/BT/BLE, Minimum free heap size: 294424 bytes
TASK1: wm1 = 1744
Task2 stk_wm = 1596
TASK1: wm2 = 496
TASK1: wm3 = 496
Task3 stk_wm = 3672
示例给出了使用两个函数查询任务 高水位线的使用方法。随着 Task1 中的局部变量被使用,task1 的高水位线被更新为更小的值。task2、task3 的代码一样,但创建任务时指定的 stack size 不一样,因此可用的栈空间也不一样。
讨论
通过对任务栈空间的理解及测试方法的了解,我们给出下述建议:
1)当某个 task 最小剩余 task 栈空间比较大时,适当减小 xTaskCreate 创建该 task 时给定的第三个参数值,可节约 RAM,以优化系统内存。
2)当某个 task 最小剩余 task 栈空间比较小时,适当增大 xTaskCreate 创建该 task 时给定的第三个参数值,可降低 task 栈溢出风险。
3)如果对 SDK 没有深入了解,不要修改 系统 task 分配的最大可用栈空间。
4)不要在系统 task 的 callback 函数里,添加过多代码,不要添加阻塞操作。
因为系统 task 通常都是经过优化配置的,如果代码深度较大,容易造成 task 栈溢出;
如果有阻塞操作,将导致该系统 task 接下来逻辑无法执行,甚至有死锁的可能。
例如:
sniffer 的 callback 函数: wifi_promiscuous_cb_t cb
WiFi callback 函数: system_event_cb_t cb
5)占用空间较大的变量,尽可能通过 malloc/calloc 等动态申请释放,以提高栈空间利用率。
总结
1)通过 uxTaskGetStackHighWaterMark()
和 uxTaskGetStackHighWaterMark2()
可以评估任务自创建以来的最小剩余栈空间的大小,其返回值以字节为单位。
2)在任务创建时合理的分配栈空间大小对节省RAM 空间、保证任务运行的稳定性非常重要。
3)系统自动创建的任务中,也要注意栈空间大小的限制,在系统 task 中的回调函数中不能添加过多代码,不要添加任何阻塞操作,慎用延时。
4) 占用空间较大的变量,尽可能通过 malloc/calloc 等动态申请释放,以提高栈空间利用率。
资源链接
1)Learning-FreeRTOS-with-esp32 系列博客介绍
2)对应示例的 code 链接 (点击直达代码仓库)
3)下一篇:RTOS任务状态总结及查看 RTOS 任务的状态