做嵌入式 Linux 開發那幾年,Memory Leak 大概是我遇過最多次、最難抓、最容易被忽略的問題。
桌面應用程式記憶體洩漏,頂多讓程式跑慢、被 OS 回收。但在嵌入式系統上,RAM 本來就不多,一旦洩漏,輕則效能下降,重則系統直接 OOM crash,而且往往要跑好幾個小時才會發作。
這篇文章整理我這幾年踩過的幾個經典 Memory Leak 案例,每個都有點不一樣,希望能幫你少走一些彎路。
⚠️ 程式碼為簡化示意版本,重點在於呈現問題模式
案例一:最經典的忘記 free 🐣
發生在哪
在路由器韌體上開發一個設定檔解析模組。
問題現象
系統運行幾天後,記憶體使用量緩慢爬升,最終 OOM。
問題程式碼
// 每次收到 HTTP 請求就解析設定
void handle_config_request(const char *raw_json) {
// 每次都 malloc,但...
config_t *cfg = (config_t *)malloc(sizeof(config_t));
parse_json(raw_json, cfg);
apply_config(cfg);
// ← 忘記 free 了!
// function 結束,cfg 指標消失,記憶體永遠回不來
}
根本原因
malloc 之後忘記 free,每次 HTTP 請求都洩漏一塊記憶體。
請求量不大時看不出來,但跑幾天就原形畢露。
修正方式
void handle_config_request(const char *raw_json) {
config_t *cfg = (config_t *)malloc(sizeof(config_t));
if (!cfg) return; // 也要記得檢查 NULL!
parse_json(raw_json, cfg);
apply_config(cfg);
free(cfg); // ✅ 用完就還
cfg = NULL; // ✅ 避免 dangling pointer
}
教訓
malloc 和 free 要像括號一樣成對出現。
寫完 malloc 的下一秒,就先把 free 的位置想好。
案例二:Error Path 沒有清理 😈
發生在哪
模組的網路連線初始化流程。
問題現象
網路連線失敗的情況下,記憶體緩慢洩漏。
正常連線完全沒問題,只有在網路不穩、頻繁重連的環境才會發作。
問題程式碼
int init_network(network_ctx_t **ctx) {
*ctx = malloc(sizeof(network_ctx_t));
(*ctx)->buffer = malloc(BUFFER_SIZE);
(*ctx)->socket = socket_create();
if ((*ctx)->socket < 0) {
return -1; // ← Error! 但上面兩個 malloc 沒有 free!
}
if (connect((*ctx)->socket, ...) < 0) {
return -1; // ← 同樣的問題
}
return 0;
}
根本原因
Happy Path 寫得很好,但 Error Path 完全(忘了)沒有清理資源。
這種問題特別隱蔽,因為正常測試都是走 Happy Path,CI 也過了,結果到了網路不穩的現場才爆發。
修正方式
int init_network(network_ctx_t **ctx) {
*ctx = malloc(sizeof(network_ctx_t));
if (!*ctx) return -1;
(*ctx)->buffer = malloc(BUFFER_SIZE);
if (!(*ctx)->buffer) {
goto cleanup_ctx; // ✅ 用 goto 統一清理
}
(*ctx)->socket = socket_create();
if ((*ctx)->socket < 0) {
goto cleanup_buffer;
}
if (connect((*ctx)->socket, ...) < 0) {
goto cleanup_socket;
}
return 0;
// ✅ 統一的清理區塊,逆序釋放
cleanup_socket:
socket_close((*ctx)->socket);
cleanup_buffer:
free((*ctx)->buffer);
cleanup_ctx:
free(*ctx);
*ctx = NULL;
return -1;
}
教訓
每條 Error Path 都是潛在的洩漏點。
Linux Kernel 大量使用goto cleanup模式,不是沒有原因的。
案例三:第三方 Library 的坑 😤
發生在哪
整合一個第三方的影像編解碼 library。
問題現象
系統跑久之後記憶體持續增長,但我把自己的程式碼翻了好幾遍,malloc/free 都是成對的,找不到問題。
追查過程
用 Valgrind 跑了一次:
valgrind --leak-check=full \
--show-leak-kinds=all \
--track-origins=yes \
./my_program
輸出結果:
==12345== LEAK SUMMARY:
==12345== definitely lost: 2,048 bytes in 1 blocks
==12345== indirectly lost: 65,536 bytes in 16 blocks
==12345== possibly lost: 0 bytes in 0 blocks
==12345== still reachable: 1,024 bytes in 4 blocks
==12345==
==12345== 2,048 bytes in 1 blocks are definitely lost
==12345== at 0x4C2FB0F: malloc (vg_replace_malloc.c:309)
==12345== by 0x401234: codec_init (libcodec.so) ← 第三方 library!
==12345== by 0x402345: my_init_function (main.c:45)
洩漏來源是 libcodec.so,不是我的程式碼。
根本原因
翻了 library 的文件,發現有一個 codec_global_cleanup() 函式,文件寫在不起眼的角落,說明程式結束前必須呼叫,否則會有資源洩漏。
// 文件上寫的,但很容易忽略:
// "Call codec_global_cleanup() before program exit to release
// internal resources allocated during codec_init()"
// 我的程式碼裡完全沒有呼叫這個...
修正方式
int main(void) {
codec_init();
// ... 主程式邏輯 ...
codec_global_cleanup(); // ✅ 補上這行
return 0;
}
教訓
整合第三方 library,一定要把文件從頭到尾看一遍。
特別注意init/cleanup、create/destroy、open/close這類成對的函式。
Valgrind 是找出「是誰洩漏」的好幫手。
案例四:迴圈裡的隱藏洩漏 🔄
發生在哪
資料處理的主迴圈。
問題現象
系統剛啟動時記憶體正常,但跑幾個小時後記憶體就滿了。
問題程式碼
void data_process_loop(void) {
while (1) {
// 每次迴圈都建立新的 buffer
uint8_t *process_buf = malloc(PROCESS_BUF_SIZE);
raw_data_t *raw = malloc(sizeof(raw_data_t));
read_sensor_data(raw);
process_data(raw, process_buf);
send_to_display(process_buf);
free(process_buf); // ✅ 這個有 free
// ← raw 沒有 free!每次迴圈洩漏一塊
vTaskDelay(100 / portTICK_PERIOD_MS);
}
}
根本原因
迴圈裡有兩個 malloc,只 free 了一個。
單次洩漏很小(幾十 bytes),但迴圈每 100ms 執行一次,一小時就洩漏幾萬次,幾個小時後記憶體就耗盡了。
修正方式
void data_process_loop(void) {
// ✅ 固定大小的 buffer 可以拉到迴圈外面,只 malloc 一次
uint8_t *process_buf = malloc(PROCESS_BUF_SIZE);
raw_data_t *raw = malloc(sizeof(raw_data_t));
if (!process_buf || !raw) {
// 處理 malloc 失敗
free(process_buf);
free(raw);
return;
}
while (1) {
read_sensor_data(raw);
process_data(raw, process_buf);
send_to_display(process_buf);
vTaskDelay(100 / portTICK_PERIOD_MS);
}
// 理論上不會到這裡,但好習慣還是寫上
free(process_buf);
free(raw);
}
教訓
迴圈裡的 malloc 要特別小心。
問自己:「這個 buffer 真的需要每次重新分配嗎?」
如果大小固定,拉到迴圈外面,只分配一次。
案例五:最難找的 — 間接洩漏 🕵️
發生在哪
路由器韌體,設定管理模組。
問題現象
這次最麻煩,Valgrind 報的是 indirectly lost,不是 definitely lost。
問題程式碼
typedef struct {
char *name; // ← 指向動態分配的字串
char *value; // ← 同上
} config_item_t;
typedef struct {
config_item_t *items;
int count;
} config_list_t;
void free_config_list(config_list_t *list) {
free(list->items); // ← 只 free 了 items 陣列本身
free(list);
// ← 但 items[i].name 和 items[i].value 指向的字串沒有 free!
}
根本原因
釋放結構體時,只釋放了「外層」,沒有釋放「內層」指標指向的記憶體。
這種洩漏 Valgrind 會標記為 indirectly lost,因為指向那塊記憶體的指標已經不見了(隨著 items 陣列被 free 而消失),但記憶體本身還沒被釋放。
修正方式
void free_config_list(config_list_t *list) {
if (!list) return;
// ✅ 先釋放內層的動態字串
for (int i = 0; i < list->count; i++) {
free(list->items[i].name);
free(list->items[i].value);
list->items[i].name = NULL;
list->items[i].value = NULL;
}
// ✅ 再釋放外層
free(list->items);
free(list);
}
教訓
釋放結構體之前,先想想裡面有沒有指標。
結構體越複雜,越容易漏掉內層的釋放。
建議為每個複雜結構體寫一個專用的destroy函式,統一管理。
我的 Memory Leak 預防清單 📋
經過這些年的教訓,我整理了一份自己在 Code Review 時會用的檢查清單:
撰寫程式碼時
- [ ]
malloc之後,立刻想好在哪裡free - [ ] 每條 Error Path 都有清理資源
- [ ] 迴圈裡的
malloc是否必要?能否移到迴圈外? - [ ] 結構體有指標成員時,
free前先釋放內層 - [ ] 第三方 library 的
cleanup函式有沒有呼叫?
監控記憶體趨勢(簡單腳本)
#!/bin/bash
# 監控指定 process 的記憶體使用趨勢
PID=$1
while true; do
MEM=$(cat /proc/$PID/status | grep VmRSS | awk '{print $2}')
echo "$(date '+%H:%M:%S') - RSS: ${MEM} kB"
sleep 10
done
跑個幾小時,如果 RSS 持續爬升不回頭,就要開始懷疑有洩漏了。
總結
回頭看這些案例,Memory Leak 的根本原因其實就幾種:
| 類型 | 典型場景 |
|---|---|
| 忘記 free | 菜鳥時期最常犯 |
| Error Path 沒清理 | 最容易被忽略 |
| 第三方 library | 文件沒看完 |
| 迴圈裡重複分配 | 累積效應,慢慢爆 |
| 間接洩漏 | 結構體指標沒處理 |
沒有哪個工程師能保證自己不寫出 Memory Leak,但可以透過好的編碼習慣 + 正確的工具,讓問題在開發階段就被抓出來,而不是等到客戶現場才爆發。
希望這篇文章能幫你少踩幾個坑 😄
💬 你有沒有遇過特別難找的 Memory Leak?歡迎留言分享!
說不定你的經驗比我的更精彩 😅