C語言記憶體洩露很嚴重,如何應對?

at_1發表於2021-09-11
摘要:透過介紹記憶體洩漏問題原理及檢視方法,希望後續能夠從編碼檢視環節就杜絕記憶體洩漏導致的網上問題發生。

1. 前言

最近部門不同產品接連出現記憶體洩漏導致的網上問題,具體表現為單板在現網執行數月以後,因為記憶體耗盡而導致單板復位現象。一方面,記憶體洩漏問題屬於低階錯誤,此類問題遺漏到現網,影響很壞;另一方面,由於記憶體洩漏問題很可能導致單板執行固定時間以後就復位,只能透過批次升級才能解決,實際影響也很惡劣。同時,接連出現此類問題,尤其是其中一例問題還是我們老員工修改引入,說明我們不少員工對記憶體洩漏問題認識還是不夠深刻的。本文透過介紹記憶體洩漏問題原理及檢視方法,希望後續能夠從編碼檢視環節就杜絕此類問題發生。

說明:預防記憶體洩漏問題有多種方法,如加強程式碼檢視、工具檢測和記憶體測試等,本文聚集於開發人員能力提升方面。

2. 記憶體洩漏問題原理

2.1堆記憶體在C程式碼中的儲存方式

記憶體洩漏問題只有在使用堆記憶體的時候才會出現,棧記憶體不存在記憶體洩漏問題,因為棧記憶體會自動分配和釋放。C程式碼中堆記憶體的申請函式是malloc,常見的記憶體申請程式碼如下:

  char *info = NULL;    /**轉換後的字串**/
 
    info = (char*)malloc(NB_MEM_SPD_INFO_MAX_SIZE);
    if( NULL == info)
    {
        (void)tdm_error("malloc error!n");
        return NB_SA_ERR_HPI_OUT_OF_MEMORY;
    }

由於malloc函式返回的實際上是一個記憶體地址,所以儲存堆記憶體的變數一定是一個指標(除非程式碼編寫極其不規範)。再重複一遍,儲存堆記憶體的變數一定是一個指標,這對本文主旨的理解很重要。當然,這個指標可以是單指標,也可以是多重指標。

malloc函式有很多變種或封裝,如g_malloc、g_malloc0、VOS_Malloc等,這些函式最終都會呼叫malloc函式。

2.2堆記憶體的獲取方法

看到本小節標題,可能有些同學有疑惑,上一小節中的malloc函式,不就是堆記憶體的獲取方法嗎?的確是,透過malloc函式申請是最直接的獲取方法,如果只知道這種堆記憶體獲取方法,就容易掉到坑裡了。一般的來講,堆記憶體有如下兩種獲取方法:

方法一:將函式返回值直接賦給指標,一般表現形式如下:

    char *local_pointer_xx = NULL;
local_pointer_xx = (char*)function_xx(para_xx, …);

該類涉及到記憶體申請的函式,返回值一般都指標型別,例如:

GSList* g_slist_append (GSList   *list, gpointer  data)

方法二:將指標地址作為函式返回引數,透過返回引數儲存堆記憶體地址,一般表現形式如下:

    int ret;
    char *local_pointer_xx = NULL;    /**轉換後的字串**/
    ret = (char*)function_xx(..., &local_pointer_xx, ...);

該類涉及到記憶體申請的函式,一般都有一個入參是雙重指標,例如:

__STDIO_INLINE _IO_ssize_t
getline (char **__lineptr, size_t *__n, FILE *__stream)

前面說透過malloc申請記憶體,就屬於方法一的一個具體表現形式。其實這兩類方法的本質是一樣的,都是函式內部間接申請了記憶體,只是傳遞記憶體的方法不一樣,方法一透過返回值傳遞記憶體指標,方法二透過引數傳遞記憶體指標。

2.3記憶體洩漏三要素

最常見的記憶體洩漏問題,包含以下三個要素:

要素一:函式內有區域性指標變數定義;

要素二:對該區域性指標有透過上一小節中“兩種堆記憶體獲取方法”之一獲取記憶體;

要素三:在函式返回前(含正常分支和異常分支)未釋放該記憶體,也未儲存到其它全域性變數或返回給上一級函式。

2.4記憶體釋放誤區

稍微使用過C語言編寫程式碼的人,都應該知道堆記憶體申請之後是需要釋放的。但為何還這麼容易出現記憶體洩漏問題呢?一方面,是開發人員經驗不足、意識不到位或一時疏忽導致;另一方面,是記憶體釋放誤區導致。很多開發人員,認為要釋放的記憶體應該侷限於以下兩種:

1)直接使用記憶體申請函式申請出來的記憶體,如malloc、g_malloc等;

2)該開發人員熟悉的介面中,存在記憶體申請的情況,如iBMC的兄弟,都應該知道呼叫如下介面需要釋放list指向的記憶體:

dfl_get_object_list(const char* class_name, GSList **list)

按照以上思維編寫程式碼,一旦遇到不熟悉的介面中需要釋放記憶體的問題,就完全沒有釋放記憶體的意識,記憶體洩漏問題就自然產生了。

3. 記憶體洩漏問題檢視方法

檢視記憶體洩漏問題,關鍵還是要養成良好的編碼檢視習慣。與記憶體洩漏三要素對應,需

要做到如下三點:

(1)在函式中看到有區域性指標,就要警惕記憶體洩漏問題,養成進一步排查的習慣

(2)分析對區域性指標的賦值操作,是否屬於前面所說的“兩種堆記憶體獲取方法”之一,如果是,就要分析函式返回的指標到底指向啥?是全域性資料、靜態資料還是堆記憶體?對於不熟悉的介面,要找到對應的介面文件或原始碼分析;又或者看看程式碼中其它地方對該介面的引用,是否進行了記憶體釋放;

(3)如果確認對區域性指標存在記憶體申請操作,就需要分析該記憶體的去向,是會被儲存在全域性變數嗎?又或者會被作為函式返回值嗎?如果都不是,就需要排查函式所有有”return“的地方,保證記憶體被正確釋放。

 

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/1817/viewspace-2796443/,如需轉載,請註明出處,否則將追究法律責任。

相關文章