# C 語言中的記憶體不足 (OOM) 處理策略
> 參考資料:[Handling out-of-memory conditions in C](http://eli.thegreenplace.net/2009/10/30/handling-out-of-memory-conditions-in-c)
## 問題背景
* 在 C 語言中,`malloc`、`calloc`、`realloc` 等動態記憶體分配函式可能因系統資源耗盡而失敗,此時它們會返回 `NULL` 指標。
* 未能正確處理 `malloc` 返回的 `NULL` 可能導致程式嘗試對空指標解引用,引發 Segfault (Segmentation Fault) 並崩潰。
* C 語言缺乏內建的異常處理機制 (Exceptions),使得錯誤狀態 (如 OOM) 的傳遞和處理相對繁瑣,需要開發者顯式地檢查返回值並在函式呼叫鏈中傳遞錯誤。
## 通用策略 (主要針對桌面/伺服器應用)
主要有三種處理 OOM 的策略:
### 策略一:恢復 (Recovery)
* **目標**:應用程式嘗試從 OOM 狀態中「優雅地」恢復。
* **作法:**
* 釋放部分已分配但非必要的資源,然後重試記憶體分配。
* 保存使用者工作,然後正常退出。
* 清理臨時檔案或資源,然後退出。
* **優缺點:**
* 優點:提供最佳的使用者體驗和系統穩定性 (如果成功) 。
* 缺點:實作 **極其困難** 且高度依賴應用場景。必須確保恢復過程本身不需額外分配記憶體,且錯誤狀態需要仔細地逐層傳遞。這是最少被採用的策略。
* **範例:**
* Git (嘗試釋放快取後重試)
```c=
void *xmalloc(size_t size)
{
void *ret = malloc(size);
if (!ret && !size)
ret = malloc(1);
if (!ret) {
release_pack_memory(size, -1);
ret = malloc(size);
if (!ret && !size)
ret = malloc(1);
if (!ret)
die("Out of memory, malloc failed");
}
#ifdef XMALLOC_POISON
memset(ret, 0xA5, size);
#endif
return ret;
}
```
* SQLite (將 `NULL` 返回給呼叫者,由應用程式決定如何恢復)。
```c=
static void *sqlite3MemMalloc(int nByte)
{
sqlite3_int64 *p;
assert( nByte>0 );
nByte = ROUND8(nByte);
p = malloc( nByte+8 );
if( p ){
p[0] = nByte;
p++;
}
return (void *)p;
}
```
### 策略二:中止 (Abort)
* **目標**:簡單直接地處理 OOM。
* **作法**:當偵測到 `malloc` 返回 `NULL` 時,印出錯誤訊息,然後呼叫 `abort()` 或 `exit()` 終止程式。
* **優缺點:**
* 優點:實作簡單,顯著減少程式碼中錯誤檢查和傳遞的複雜度。許多 Unix 工具使用 `xmalloc` 這類的包裝函式自動處理。
```c=
void *
xmalloc (size_t n)
{
void *p = malloc (n);
if (!p && n != 0)
xalloc_die ();
return p;
}
```
調用此函數時,不會檢查其返回值,從而降低了程式碼的複雜性。以下是 find 工具中的代表性用法:
```c=
cur_path = xmalloc (cur_path_size);
strcpy (cur_path, pathname);
cur_path[pathname_len - 2] = '/';
```
* 缺點:使用者工作可能遺失,體驗較差。不適用於需要高可靠性、不能隨意終止的系統。
* **普遍性**:**最常用** 的策略,尤其在命令列工具和一般桌面應用中。
* **範例:**
* Glib 提供了兩種主要的記憶體配置方式:
* `g_malloc`:嘗試配置記憶體,若失敗則直接中止程式並報錯 (**中止策略**) 。 (例如 `g_array`)
* `g_try_malloc`:嘗試配置記憶體,若失敗則返回 `NULL`,讓程式可以**自行處理錯誤**。 (例如 `gfileutils`)
* Redis (明確選擇此策略以簡化程式碼),許多使用 `gnulib` 的工具。
```c=
/* Redis generally does not try to recover from out
* of memory conditions when allocating objects or
* strings, it is not clear if it will be possible
* to report this condition to the client since the
* networking layer itself is based on heap
* allocation for send buffers, so we simply abort.
* At least the code will be simpler to read... */
static void oom(const char *msg)
{
fprintf(stderr, "%s: Out of memory\n",msg);
fflush(stderr);
sleep(1);
abort();
}
```
### 策略三:Segfault
* **目標**:最簡單 (但不推薦) 。
* **作法**:完全不檢查 `malloc` 的返回值。OOM 發生時,`NULL` 指標被後續程式碼使用,導致 Segfault 崩潰。
* **優缺點:**
* 優點:程式碼最簡單 (省略檢查) 。
* 缺點:極不健壯,崩潰突然且不友好,可能遺失資料。雖然崩潰可產生核心轉儲 (core dump) 供除錯,但這不是良好的錯誤處理方式。
* **範例:**
* lighttpd (其程式碼中有許多未檢查 `malloc`/`calloc` 的情況)。
* 來自 network_server_init:
```c=
srv_socket = calloc(1, sizeof(*srv_socket));
srv_socket->fd = -1;
```
* 來自 rewrite_rule_buffer_append:
```c=
kvb->ptr = malloc(kvb->size * sizeof(*kvb->ptr));
for(i = 0; i < kvb->size; i++) {
kvb->ptr[i] = calloc(1, sizeof(**kvb->ptr));
```
## 通用建議
* **開發函式庫 (Library) 時:**
* **應採用 Recovery 策略**,即將 `NULL` 或錯誤碼返回給呼叫者。函式庫不應擅自中止應用程式,應將 OOM 的最終處理權交給應用程式。
* 考慮提供讓應用程式註冊自訂分配器或錯誤處理器的機制 (如 SQLite) 。
* **開發應用程式 (Application) 時:**
* 除非有極高的可靠性要求必須實現 Recovery,否則 **Abort 策略通常是最佳選擇**。
* 建議使用 **包裝函式** (如自訂的 `xmalloc`) ,封裝 `malloc` 呼叫和 OOM 時的中止邏輯,以簡化主程式碼並利於未來可能的策略調整。
## 嵌入式系統的 OOM 策略
嵌入式系統因其 **資源限制 (記憶體極少) 、高可靠性要求、可能的即時性需求**,其 OOM (Out of Memory) 處理策略與桌面/伺服器有很大不同,通常**不能接受簡單的 Abort 或 Segfault**。核心思想是 **預防、可預測性、可靠性優先**。
1. **避免動態記憶體分配 (根本策略)**
* **作法**:使用**靜態分配** (全域/靜態變數、固定大小區域變數) 或在系統**啟動時一次性分配**所有所需記憶體。
* **結果**:執行時期不會發生 `malloc` 失敗,記憶體使用量完全可預測。是安全關鍵系統的首選。
2. **記憶體池 (Memory Pools)**
* **作法**:預先分配一大塊記憶體,實作自訂分配器從中管理固定大小或可變大小的區塊。
* **OOM 處理**:池耗盡時分配失敗。可採取:拒絕請求、嘗試釋放池中其他區塊 (如舊資料)、進入降級模式、記錄錯誤。
* **優點**:比通用 `malloc` 快、減少碎片、更可控。
* **RTOS 支援**:許多 RTOS (如 FreeRTOS, Zephyr) 內建多種記憶體池機制,可直接利用。
* **碎片風險**:若使用可變大小區塊的池,長期運行仍可能產生**外部碎片**,導致即使總空間足夠,也無合適連續區塊而分配失敗。固定大小池可避免此問題。
3. **物件重用/快取 (Object Reuse / Caching)**
* **作法**:將不再使用的同類型物件放入**快取列表**,需要時重用,而非真正 `free` 和 `malloc`。
* **優點**:減少分配開銷和碎片。
4. **嚴格的錯誤檢查與恢復/降級 (若必須動態分配)**
* **作法**:每次分配都必須檢查返回值。
* **恢復/處理方式:**
* **資源釋放與重試**:釋放非關鍵資源後再試。
* **功能降級**:關閉次要功能,維持核心運作。
* **明確錯誤回報**:通知監控系統或上層。
* **拒絕服務**:暫停接受新請求。
* **OOM Hook / call back**:部分分配器允許註冊 OOM call back 函數,在分配失敗時自動執行預設的清理、降級或記錄操作。
* **關鍵**:恢復/降級邏輯本身必須保證不會觸發 OOM。
5. **監看門狗計時器與重置 (Watchdog Timer & Reset)**
* **作法**:硬體計時器,需由軟體定期重置 ("餵狗")。若系統因 OOM 卡死無法重置,計時器超時會強制重啟系統。
* **定位**:最後的防線,用於從嚴重錯誤 (包括 OOM 造成的死鎖) 中恢復,但會中斷服務和丟失狀態。
6. **堆疊溢位管理 (Stack Overflow Management)**
* **注意點**:除了堆 (Heap) OOM,任務的**堆疊 (Stack)** 耗盡同樣致命。
* **策略**:需仔細**靜態估算**每個任務所需堆疊大小、利用 RTOS 的**堆疊使用監控** (Stack Watermarking) 功能、或使用 MPU 設定**保護區** (Guard Page) 來檢測溢位。
7. **記憶體保護單元 (MPU - Memory Protection Unit)**
* **作用**:若硬體支援,可配置 MPU 限制任務僅能訪問其合法記憶體區域。
* **效益**:雖不直接防 OOM,但能防止記憶體錯誤 (如越界寫) 破壞其他任務或核心,提高系統**錯誤隔離**能力和穩定性。
8. **策略權衡**
* **核心**:最佳策略組合取決於具體應用:系統複雜度、可靠性要求、可用資源、開發週期等。需綜合考量選擇最合適的方法。
## 總結
處理 OOM 沒有唯一正確的方法。桌面/伺服器應用為了開發效率和可接受的健壯性,常選擇 Abort 策略。而函式庫應將控制權交還,採用 Recovery (返回錯誤) 。嵌入式系統則因其特性,更側重於 **預防** OOM 的發生 (靜態分配、記憶體池) ,並在萬一發生時,採取 **可預測、可控制** 的降級或恢復措施,而非簡單中止。