快速定位內存泄漏的套路

背景

偶然間發現一個模塊掛掉了,並且沒有生成core文件。這就讓我很奇怪,因爲一般如果是段錯誤導致程序掛掉,是會生成core文件的(我已經開啓了coredump ulimit -c unlimited)。通過dmesg查看內核日誌,發現是由於OOM kill機制導致的。如圖:
在這裏插入圖片描述
既然發現了問題就一定要解決。通過查閱資料以及分析log終於定位到了內存泄漏的代碼部分。本章我會結合自己的理解,一步一步的帶大家分析,希望能夠幫助到大家。

什麼是OOM kill 機制?

簡單的說就是當你的內存不足時,linux 內核爲了不影響所有進程的正常使用,會啓動該機制。首先會依據一些條件(進程內存佔用大小,進程運行的時間等,一般都是那些內存佔用比較多的進程)選出bad process。將其kill,釋放它佔用的內存。

暴力分析法

如果對於整體代碼比較熟悉時,出現了內存泄漏,我們是可以估摸出大概位置的。比如:昨天還沒有內存泄漏,今天就有了。那麼這個bug肯定是某某在今天commit的。只要查看一下log就能知道大概位置。之後通過註釋法(依次註釋接口),也能夠很快的定位到問題代碼行。

valgrind 工具

如果對代碼不是很熟悉(我就是這種情況,代碼是外包人員寫的,現在交付不管了),那我們最好的方式就是引用一些工具了。 網上推薦的工具有很多,我在這裏使用的是valgrind。該工具的功能強大:

  1. memcheck :檢測程序中的內存問題,如內存泄漏,越界,非法指針等
  2. callgrind:檢測程序代碼的運行時間和調用過程,以及分析程序性能。
  3. cachegrind:分析CPU的cache命中率,丟失率,用於進行代碼優化。
  4. helgrind:用於檢測多線程程序中出現的競爭問題
  5. Massif:堆棧分析器,它能測量程序在堆棧中使用了多少內存,告訴我們堆塊,堆管理和棧的大小。

本篇主要從內存問題分析介紹,有時間我再研究一下其它功能。
案例分析 test.c:

#include <stdlib.h>
#include<string.h>
#include<stdio.h>
int main()
{
	char * p = malloc(1024);
	p=NULL;
	return 0;
}

上面的代碼一眼就看出內存泄漏的問題。之後我們通過執行以下命令進行編譯調試:
在這裏插入圖片描述
gcc test.c -g 其中-g是爲了保留符號表,可以定位到代碼中具體某一行。
valgrind --tool=memcheck --leak-check=full --show-leak-kinds=all --undef-value-errors=no --log-file=log ./a.out
tool=memcheck 表示檢測內存問題。
leak-check=full 表示完全檢測內存泄漏
–log-file=log 表示信息會輸入到log文件中(有時文件內容比較多,這樣方便分析)

再查看log文件,內容如下:(內容較少)

==9393== Memcheck, a memory error detector
==9393== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==9393== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==9393== Command: ./a.out
==9393== Parent PID: 90214
==9393==
==9393==
==9393== HEAP SUMMARY:  //關鍵信息,表示你的程序內存泄漏的大小
==9393==     in use at exit: 1,024 bytes in 1 blocks
==9393==   total heap usage: 1 allocs, 0 frees, 1,024 bytes allocated
==9393==
==9393== 1,024 bytes in 1 blocks are definitely lost in loss record 1 of 1
==9393==    at 0x4C29BC3: malloc (vg_replace_malloc.c:299)
==9393==    by 0x40052E: main (test.c:6)
==9393==
==9393== LEAK SUMMARY:
==9393==    definitely lost: 1,024 bytes in 1 blocks
==9393==    indirectly lost: 0 bytes in 0 blocks
==9393==      possibly lost: 0 bytes in 0 blocks
==9393==    still reachable: 0 bytes in 0 blocks
==9393==         suppressed: 0 bytes in 0 blocks
==9393==
==9393== For counts of detected and suppressed errors, rerun with: -v
==9393== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

分析:
由於程序簡單,日誌內容也比較少。如果log文件內容比較大,分析步驟也是一樣的。
第一步:
搜索:HEAP SUMMARY關鍵字,表示當你程序結束時,內存泄漏的信息統計。很清晰的看到程序有1024個字節沒有釋放。之後的內容就是泄漏的詳細信息(在代碼的哪一行,以及泄漏多少字節)
通過log可以看到,在main函數中(test.c的第6行出現了內存泄漏)。大功告成!!!!
如果第一步就找到了泄露位置,那麼證明你很幸運。當無法簡單一目瞭然的分析時(log文件內容很多,待會上實際圖),你可以參考第二步。
第二步:
搜索:LEAK SUMMARY關鍵字。表示內存泄漏的類型:

  • definitely lost:確定的內存泄漏,已經不能訪問這塊內存
  • indirectly lost:指向該內存的指針都位於內存泄露處
  • possibly lost:可能的內存泄露,仍然存在某個指針能夠訪問某快內存,但該指針指向的已經不是該內存首位置
  • still reachable:內存指針還在還有機會使用或者釋放,指針指向的動態內存還沒有被釋放就退出了
當進程不會自動停止還能夠測試嗎?

答案是肯定的。剛開始我也懷疑,於是自己試了一試。只要你運行之後,通過ctrl+c停止,就可以了。同樣會生成詳細信息(勇於嘗試)

實戰

上面的列子比較簡單,所以很容易分析。現在通過分析項目中的log文件,來加強鞏固,先上圖:
在這裏插入圖片描述
如圖所示,實際工作中生成的log信息是很多的(8萬多行)。
第一步:找HEAP SUMMARY關鍵字,如圖:
在這裏插入圖片描述
如圖所示:大概知道當程序結束時大約還有165M的內存沒有釋放。並且泄漏的記錄有58763條之多。
想要從中找到準確的內存泄漏的地方,還是很難的。

於是我建議執行第二步如圖:
在這裏插入圖片描述
之後再執行valgrind --tool=memcheck --leak-check=full --show-leak-kinds=all --undef-value-errors=no --log-file=log-2 taskname調試,比如第一次調試,你讓它運行10分鐘,第二次讓它運行30分鐘。同樣的環境,如果存在內存泄漏,那麼肯定大小不一樣。
第二次測試結果:
在這裏插入圖片描述
之後發現差距主要是在indireactly lost中,之後就在兩個文件中搜索are indirectly lost in loss關鍵字。如圖:
log-2文件:
log文件:
在這裏插入圖片描述
通過定位就很快的找到是video_task.cpp中349行的av_read_frame接口導致的內存泄漏。代碼下:

349           if(av_read_frame(pFormatCtx, packet) >= 0)
350           {
351                 timeout = 0;
352                 if (packet->stream_index == videoindex) {
353                         int got_frame = 0;
354
355                         avcodec_decode_video2(pCodecCtx, pFrame,&got_frame, packet);
356                         if (got_frame)
357                         {
358                                 if (pFrame->key_frame)
359                                 {
360                                         int width = pFrame->width;
361                                         int height = pFrame->height;
362                                         tmp_img = cv::Mat::zeros( height*3/2, width, CV_8UC1 );
363                                         memcpy( tmp_img.data, pFrame->data[0], width*height );
364                                         memcpy( tmp_img.data + width*height, pFrame->data[1], width*height/4 );
365                                         memcpy( tmp_img.data + width*height*5/4, pFrame->data[2], width*height/4 );
366                                         cv::cvtColor( tmp_img, bgr, 101 );
367                                         set_image(bgr);
368                                 }
369                         }
370                 }
371               //  av_packet_unref(packet);
372            }

由於對ffmpeg庫的不熟悉,網上稍微一搜索,就找到了av_read_frame引起內存泄漏的相關博客,加上371行的av_packet_unref(packet);即可。

至此,內存泄漏的問題就解決了。希望能夠幫助到你。

==================================== 雜談 =====================================

問題

在這個分析和解決的過程中,剛開始我也有自己的一些疑問,不知道你是否和我一樣,有不同見解的可以留言一起討論一下:

  1. 通過log文件分析,爲什麼會存在那麼多沒有釋放的內存?
    歸根而言,還是編碼導致的。
    如果編碼足夠規範,我覺得應該是可以避免的,但是想做到真的很難。
    比如,你的項目中你需要引用一些模塊。難道你會對每一個API的實現原理都去研究嗎?實際上,我們只要能夠實現功能就可以了。比如在ffmpeg庫中avformat_find_stream_info接口其實也會產生內存泄漏(我看過很多的blog,基很少有人會去釋放)但是爲什麼沒有顯現出來呢?那是因爲在項目中它只會被執行一次。不會隨着時間的推移,造成更大的損失。(當然原則上是不允許的)。所以也就容易被我們忽略。
    在一些大廠中,一般會進行編譯靜態檢測,這是一個非常好的方式,可以將bug儘早發現。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章