記憶體的動態配置(Dynamic Memory Allocation)是指在程式執行過程中根據需要分配記憶體的一種方法。在C語言中,malloc和calloc,而在C++中則使用new運算子來實現這一功能。
然而,在嵌入式系統或即時系統的開發中,動態記憶體配置經常會被避開。
本文將探討其原因。
遵循傳統的格言 「讀源碼」,我們來看看實際的源碼。作為最簡單的實現之一,我們將關注使用於元祖Arduino的針對AVR微控制器的malloc實現。
malloc從堆區域(用於動態配置的記憶體區域)中分配所要求大小的記憶體區塊。
堆的可用空間是以列表結構進行管理的。malloc會遍歷這個列表,找到適合要求大小的空閒記憶體區塊並進行分配,剩餘的部分則作為新的空閒區塊被加入列表中。
這裡簡單介紹了一下,實際上,存在為了尋找最佳空閒空間的更智慧的實現,詳細資訊請參見源碼。
free用於釋放通過malloc分配的記憶體區塊。被釋放的區塊會重新加入堆的空閒區域列表中。
此外,如果存在相鄰的空閒區塊,則會將它們合併成一個更大的空閒區塊。
透過動態的記憶體分配和釋放,堆區域可能被細分,導致可用的大連續記憶體區塊不足。
我們來看一個實例。元祖Arduino Duemilanove(搭載ATmega328P)擁有2KB的SRAM,去掉堆疊和其他區域後,堆可用的最大空間約為1800位元組。
首先,以下代碼可以正常運行。
// 分配1600位元組的記憶體
void* a = malloc(1600);
Serial.print("a = ");
Serial.println((uint16_t)a);
輸出:
a = 512
成功分配了1600位元組的記憶體。雖然佔用了SRAM的大部分,但並不成問題。
接下來,我們執行以下代碼。
// 分配1000位元組的記憶體
void* b = malloc(1000);
Serial.print("b = ");
Serial.println((uint16_t)b);
// 分配1位元組的記憶體
void* c = malloc(1);
Serial.print("c = ");
Serial.println((uint16_t)c);
// 釋放最初分配的記憶體(1000位元組)
free(b);
// 分配1200位元組的記憶體
void* d = malloc(1200);
Serial.print("d = ");
Serial.println((uint16_t)d);
輸出:
b = 512
c = 1514
d = 0
儘管實際上只用了1位元組,但1200位元組的記憶體分配失敗。這是因為堆區域碎片化如下所示:
+-------------------+
| |
| 1000位元組空閒 |
|(分配給b的區域) |
| |
+-------------------+
| 1位元組使用中 |
|(分配給c的區域) |
+-------------------+
| |
| 剩餘空閒區域 |
| 約800位元組 |
| |
+-------------------+
如您所見,並不存在1200位元組的連續空閒區域,因此malloc(1200)失敗。
在這種簡單的情況下處理起來還算容易,但在實際應用中,記憶體的配置和釋放會變得複雜,因此可能會產生難以預測的記憶體碎片化。
在當今的豐富環境中,虛擬記憶體和垃圾回收的存在使得這並不成為太大問題,但在資源有限的嵌入式系統中,記憶體碎片化卻是一個嚴重的問題。
如前面所述,malloc和free需要掃描堆,可能會進行相鄰區塊的合併,因此在堆狀態的不同時,操作時間可能會有很大變化。在即時系統中,需要在一定時間內完成處理,因此無法預測的操作時間是不可接受的。
如果不小心忘記釋放使用malloc分配的記憶體,就會發生所謂的記憶體洩漏。在C語言的語法上並不會報錯,靜態分析工具也難以檢測,在小型記憶體中長時間運行的嵌入式系統中可能會造成致命的後果。
為了迴避動態記憶體配置的問題,常用的幾種方法如下:
在程式編譯時就分配所需的記憶體。C語言使用static關鍵字或全局變數來實現。
在程式啟動時(例如Arduino的setup()函數中)一次性分配所需的記憶體,之後不再使用malloc或free。這樣可以避免無法預測的記憶體碎片化或操作時間的變動。
簡單來說,即是自己管理記憶體的方式。雖然麻煩,但這樣可以確保自己的運行行為。
動態記憶體分配很方便,但在需要穩定運行的嵌入式系統或即時系統中,由於記憶體碎片化、無法預測的操作時間和記憶體洩漏等問題,經常會被避開。考慮替代方法,選擇最適合系統需求的記憶體管理方式是非常重要的。
原文出處:https://qiita.com/felis_silv/items/d7e7c84a6b712299102b