# 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 的發生 (靜態分配、記憶體池) ,並在萬一發生時,採取 **可預測、可控制** 的降級或恢復措施,而非簡單中止。