---
# System prepended metadata

title: Out-of-Memory in C
tags: [C]

---

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