[ESP32] 7. Quản lý bộ nhớ trong lập trình đa nhân
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:
-
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.
-
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:
- 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.
- 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.
- 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:
- 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.
- 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.
- 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.
- 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.
- 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ớ.