[ESP32] 7. Quản lý bộ nhớ trong lập trình đa nhân

Arduino 25 Th07 2024

Quản lý bộ nhớ hiệu quả là một khía cạnh quan trọng trong lập trình đa nhân, đặc biệt trên các hệ thống nhúng như ESP32 với tài nguyên bộ nhớ hạn chế. Quản lý bộ nhớ hiệu quả trong môi trường đa nhân đòi hỏi sự cẩn thận và kiểm soát chặt chẽ. Việc hiểu rõ cách hoạt động của heap và stack, cùng với việc sử dụng đúng các API của FreeRTOS, sẽ giúp bạn xây dựng các ứng dụng ổn định và hiệu quả trên ESP32.

7.1. Heap và Stack

Giải thích chi tiết:

  1. Heap:

    • Là vùng bộ nhớ động, được sử dụng cho việc cấp phát bộ nhớ trong thời gian chạy.
    • Được quản lý bởi bộ cấp phát bộ nhớ của hệ điều hành (trong trường hợp này là FreeRTOS).
    • Thường được sử dụng cho các đối tượng có kích thước không xác định tại thời điểm biên dịch.
  2. Stack:

    • Là vùng bộ nhớ tĩnh, được cấp phát cho mỗi tác vụ khi nó được tạo.
    • Lưu trữ biến cục bộ, tham số hàm, và địa chỉ trở về của hàm.
    • Mỗi tác vụ trong FreeRTOS có stack riêng.

Ví dụ:

void taskFunction(void *parameter) {
    // Biến này được lưu trên stack
    int stackVar = 10;

    // Cấp phát bộ nhớ động từ heap
    int *heapVar = (int*)pvPortMalloc(sizeof(int));
    *heapVar = 20;

    // Sử dụng biến
    Serial.printf("Stack var: %d, Heap var: %d\n", stackVar, *heapVar);

    // Giải phóng bộ nhớ heap
    vPortFree(heapVar);

    vTaskDelete(NULL);
}

void setup() {
    Serial.begin(115200);
    xTaskCreate(taskFunction, "Task", 2000, NULL, 1, NULL);
}

7.2. Phân bổ và giải phóng bộ nhớ động

Giải thích chi tiết:

  • FreeRTOS cung cấp các hàm riêng để cấp phát và giải phóng bộ nhớ động:
    • pvPortMalloc(): Cấp phát bộ nhớ
    • vPortFree(): Giải phóng bộ nhớ
  • Sử dụng các hàm này thay vì malloc() và free() tiêu chuẩn của C để đảm bảo thread-safety.

Ví dụ:

void memoryTask(void *parameter) {
    while(1) {
        // Cấp phát bộ nhớ
        char *buffer = (char*)pvPortMalloc(100 * sizeof(char));
        if (buffer != NULL) {
            snprintf(buffer, 100, "Hello from task");
            Serial.println(buffer);
            // Giải phóng bộ nhớ
            vPortFree(buffer);
        } else {
            Serial.println("Memory allocation failed");
        }
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
}

void setup() {
    Serial.begin(115200);
    xTaskCreate(memoryTask, "Memory Task", 2000, NULL, 1, NULL);
}

7.3. Xử lý lỗi bộ nhớ

Giải thích chi tiết:
Xử lý lỗi bộ nhớ là rất quan trọng trong lập trình đa nhân để đảm bảo tính ổn định của hệ thống. Các lỗi bộ nhớ phổ biến bao gồm:

  1. Stack Overflow: Xảy ra khi một tác vụ sử dụng nhiều bộ nhớ stack hơn được cấp phát.
  2. Heap Fragmentation: Khi bộ nhớ heap bị phân mảnh do cấp phát và giải phóng liên tục.
  3. Memory Leaks: Khi bộ nhớ được cấp phát nhưng không được giải phóng.

Ví dụ và cách xử lý:

void stackOverflowHook(TaskHandle_t xTask, char *pcTaskName) {
    Serial.printf("Stack overflow in task: %s\n", pcTaskName);
    // Có thể thực hiện các hành động khắc phục ở đây, ví dụ: khởi động lại hệ thống
}

void setup() {
    Serial.begin(115200);
    
    // Đặt hook function cho stack overflow
    vTaskSetApplicationTaskTag(NULL, stackOverflowHook);

    // Tạo task với kích thước stack nhỏ để minh họa stack overflow
    xTaskCreate(
        [](void* parameter) {
            volatile int largeArray[1000]; // Có thể gây stack overflow
            vTaskDelay(portMAX_DELAY);
        },
        "Overflow Task",
        100, // Stack size quá nhỏ
        NULL,
        1,
        NULL
    );

    // Kiểm tra bộ nhớ heap còn trống
    Serial.printf("Free heap: %d bytes\n", xPortGetFreeHeapSize());
}

void loop() {
    // Kiểm tra và báo cáo bộ nhớ heap còn trống định kỳ
    static uint32_t lastMemCheck = 0;
    if (millis() - lastMemCheck > 5000) {
        Serial.printf("Free heap: %d bytes\n", xPortGetFreeHeapSize());
        lastMemCheck = millis();
    }
    
    delay(100);
}

Lưu ý quan trọng:

  1. Luôn kiểm tra kết quả trả về của pvPortMalloc() để đảm bảo cấp phát bộ nhớ thành công.
  2. Sử dụng công cụ như heap_caps_check_integrity() (ESP-IDF) để kiểm tra tính toàn vẹn của heap.
  3. Cân nhắc sử dụng bộ nhớ tĩnh khi có thể để giảm thiểu rủi ro từ việc cấp phát động.
  4. Theo dõi việc sử dụng bộ nhớ trong quá trình phát triển để phát hiện sớm các vấn đề như memory leaks.
  5. Sử dụng các công cụ debug và phân tích bộ nhớ như ESP-IDF's heap tracing để xác định và khắc phục các vấn đề bộ nhớ.

Tags

Tony Phạm

Là một người thích vọc vạch và tò mò với tất cả các lĩnh vực từ khoa học tự nhiên, lập trình, thiết kế đến ... triết học. Luôn mong muốn chia sẻ những điều thú vị mà bản thân khám phá được.