遍历中修改列表元素:xTaskGetNext 在任务增删时陷入无限循环

概述

设备内置调试页面,每 5 秒自动刷新任务列表与内存水位,用于前期研发调试,便于远程观察运行状态;页面数据由设备 Web 服务在运行时遍历任务列表生成。

该功能长期运行稳定。某次自动刷新后,任务列表出现数百行重复条目,内存水位降至 16 字节,设备失去响应并重启。以下记录现象、复现过程与根因判定。

现象

调试接口 /debug.json 由页面每 5 秒自动刷新,返回任务列表和内存水位。某次刷新后,任务列表出现同一批任务的几百行重复条目,堆的"历史最低剩余"掉到 16 字节,设备失去响应并重启。

复现

最小复现工程:一个任务每毫秒创建并删除子任务,另一个任务反复调用 xTaskGetNext() 遍历并统计每个任务句柄的返回次数。

第一轮统计把"句柄重复"当信号,被 FreeRTOS 的句柄复用干扰(子任务删除后新任务复用同一内存地址),数据虚高。改为只统计永不删除的持久任务后,结论稳定:遍历期间任务被增删时,迭代器会卡在同一段列表反复重走,5000 次调用都不返回结束标志;挂起搅动任务后异常消失。

点名的人卡在被划掉的第2人,反复重走,永远到不了名单末尾

官方回复

issue 提交后,Espressif 维护者 igrr 回复:

These private functions are intended to be used in debug contexts: GDB Stub, Core Dump, where iteration over the list of tasks happens while the application isn't running.

这些私有函数只用于调试场景:GDB Stub、Core Dump,且这些场景在应用未运行时遍历任务列表。翻译成大白话:这是调试专用工具,设计前提是遍历时应用不运行、任务列表不动。我们把它用在了运行中的系统里,遍历期间任务增删导致卡住。

官方回应的边界成立,但它不消解缺陷本身:代码显式写了“遇到失效节点”的分支,正确的处理应当跳过并推进迭代器状态,或无法推进时返回 -1 终止;实际实现却只跳过不推进,形成无限循环。错误处理路径本身有 bug,这一点与“调试专用”的定位无关,后者只解释了它为何长期未被触发。

issue:#19022,复现工程:esp-idf-xtaskgetnext-repro

结论

经排查:设备调试页面按 5 秒周期刷新任务列表,在任务频繁创建、删除的运行环境下,xTaskGetNext() 遍历出现不结束现象。迭代器反复重走同一段列表,单次刷新产生数百行重复条目,堆的历史最低水位降至 16 字节,设备失去响应并重启。

经最小复现工程验证,该现象可稳定复现:遍历期间存在任务增删时,迭代器连续 5000 次调用不返回结束标志;任务增删停止后,异常消失。

经分析,根因在 xTaskGetNext() 的错误处理路径:代码为“遇到失效节点”写有处理分支,但该分支仅跳过当前节点、不推进迭代器状态,遍历因此无限循环且不返回结束标志。该缺陷属“遍历中修改容器”一类已知缺陷,触发与否取决于运行场景,缺陷本身不随场景变化。