You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 02:12:31