ESP32使用esp_http_server异步处理日志流时随机崩溃求助
希望ESP32通过Wi-Fi发送日志消息,为此注册了一个启动异步响应的URI处理器。该功能原理上可行,但会随机崩溃:有时连接后立即崩溃,有时数分钟后才崩溃。核心转储信息多样,多数为LoadProhibited,偶尔出现LoadStoreAlignment,甚至ESP32直接冻结。使用同一框架向UART输出日志正常,且只要不连接/api/log接口,HTTP服务就稳定。
相关代码如下:
/** Handler that will be registered on the httpd. */ esp_err_t uri_handler(httpd_req_t *r) { addResponseHeaders(r); DEBUG << "Sending log stream"; auto out = std::make_shared<httpd_req_t*>(nullptr); const auto err = httpd_req_async_handler_begin(r, out.get()); if (err != ESP_OK || *out == nullptr) { ERROR << "Failed to start async handler: " << esp_err_to_name(err); return err; } auto receiverId = std::make_shared<int>(0); /* Handler that will be registeres on the logging framework */ auto logReceiver = [out, receiverId] (Log::Level level, const char* tag, const char* file, int line, const std::string_view& message) { if (*out) { using namespace std; string json = format( R"({{"uptime":{},"level":{},"tag":"{}","file":"{}","line":{},"message":"{}"}})", xTaskGetTickCount(), Log::levelName(level), tag, file, line, message ); if (httpd_resp_send_chunk(*out, json.data(), json.size()) != ESP_OK) { Log::unregisterLogReceiver(*receiverId); if (*out) { httpd_req_async_handler_complete(*out); } *out = nullptr; DEBUG << "Log stream closed"; } } else { Log::unregisterLogReceiver(*receiverId); DEBUG << "Log stream closed unexpectedly"; } }; *receiverId = Log::registerLogReceiver(logReceiver); return ESP_OK; } /** Will be called at program start. */ void registerLogHandler() { httpd_uri_t httpd_uri = { .uri = "/api/log", .method = HTTP_GET, .handler = &uri_handler, .user_ctx = nullptr, .is_websocket = false}; auto result = httpd_register_uri_handler(HttpServer::handle, &httpd_uri); }
已查看Espressif官方示例,但除应用架构不同外未发现差异,需要排查解决该问题。
你的代码核心问题集中在异步请求生命周期管理、线程安全以及资源释放竞态条件上,以下是具体问题和修复方案:
1. 异步请求指针的野指针风险
httpd_req_async_handler_begin返回的httpd_req_t*由HTTP服务器管理生命周期,当客户端断开连接或异步处理完成后,服务器会自动释放该指针。你直接用std::shared_ptr<httpd_req_t*>保存指针,无法跟踪服务器的释放动作,后续访问*out时会触发LoadProhibited(访问已释放内存)。
修复方案:
- 不要直接保存
httpd_req_t*,改用原子变量标记请求的活跃状态; - 注册HTTP连接断开回调,在客户端断开时及时清理资源。
2. 线程安全与竞态条件
日志接收器回调大概率运行在非HTTP服务器线程(比如日志输出线程),而out、receiverId的读写没有同步机制,会出现竞态:比如刚检查完*out != nullptr,紧接着*out被其他线程置空,后续调用httpd_resp_send_chunk时就会访问无效内存。
修复方案:
- 添加互斥锁保护对请求状态和接收器ID的所有操作;
- 使用
std::atomic原子变量标记请求活跃状态,替代直接指针判断。
3. 资源重复释放问题
当多个日志事件同时触发时,可能多次调用Log::unregisterLogReceiver(*receiverId),或者在httpd_req_async_handler_complete后仍尝试访问指针,导致未定义行为。
修复方案:
- 一旦触发资源释放流程,立即标记接收器为已注销,避免重复操作;
- 在注销接收器后,确保不再执行任何与HTTP请求相关的操作。
修复后的示例代码
#include <mutex> #include <atomic> /** Handler that will be registered on the httpd. */ esp_err_t uri_handler(httpd_req_t *r) { addResponseHeaders(r); DEBUG << "Sending log stream"; // 原子变量标记请求活跃状态 auto is_active = std::make_shared<std::atomic<bool>>(true); auto receiver_id = std::make_shared<int>(0); std::mutex req_mutex; httpd_req_t* async_req = nullptr; esp_err_t err = httpd_req_async_handler_begin(r, &async_req); if (err != ESP_OK || async_req == nullptr) { ERROR << "Failed to start async handler: " << esp_err_to_name(err); return err; } /* Handler that will be registered on the logging framework */ auto logReceiver = [async_req, is_active, receiver_id, &req_mutex] (Log::Level level, const char* tag, const char* file, int line, const std::string_view& message) { std::lock_guard<std::mutex> lock(req_mutex); if (!is_active->load(std::memory_order_acquire)) { return; } // 转义JSON特殊字符,避免格式错误 auto escape_json = [](const std::string_view& s) { std::string res; for (char c : s) { switch(c) { case '"': res += "\\\""; break; case '\\': res += "\\\\"; break; case '\b': res += "\\b"; break; case '\f': res += "\\f"; break; case '\n': res += "\\n"; break; case '\r': res += "\\r"; break; case '\t': res += "\\t"; break; default: res += c; break; } } return res; }; using namespace std; string json = format( R"({{"uptime":{},"level":"{}","tag":"{}","file":"{}","line":{},"message":"{}"}})", xTaskGetTickCount(), Log::levelName(level), escape_json(tag), escape_json(file), line, escape_json(message) ); if (httpd_resp_send_chunk(async_req, json.data(), json.size()) != ESP_OK) { Log::unregisterLogReceiver(*receiver_id); is_active->store(false, std::memory_order_release); httpd_req_async_handler_complete(async_req); DEBUG << "Log stream closed"; } }; *receiver_id = Log::registerLogReceiver(logReceiver); // 注册连接断开回调,及时清理资源 auto on_disconnect = [is_active, receiver_id, &req_mutex](httpd_req_t* r) { std::lock_guard<std::mutex> lock(req_mutex); if (is_active->load(std::memory_order_acquire)) { Log::unregisterLogReceiver(*receiver_id); is_active->store(false, std::memory_order_release); DEBUG << "Log stream disconnected by client"; } return ESP_OK; }; httpd_register_disconnect_cb(async_req, on_disconnect); return ESP_OK; } /** Will be called at program start. */ void registerLogHandler() { httpd_uri_t httpd_uri = { .uri = "/api/log", .method = HTTP_GET, .handler = &uri_handler, .user_ctx = nullptr, .is_websocket = false}; auto result = httpd_register_uri_handler(HttpServer::handle, &httpd_uri); }
额外优化建议
- 避免在日志回调中执行耗时操作(比如复杂JSON格式化),可将日志事件放入队列,由单独线程处理HTTP发送,减少对日志系统的阻塞;
- 限制HTTP chunk发送频率,避免短时间内发送大量数据导致网络栈过载;
- 启用ESP-IDF的
CONFIG_ESP32_DEBUG_OCDAWARE选项,配合核心转储工具更精准定位崩溃点。
内容的提问来源于stack exchange,提问作者Mr. Clear

