0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫(xiě)文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

如何處理服務(wù)存在內(nèi)存泄漏問(wèn)題?

GReq_mcu168 ? 來(lái)源:玩轉(zhuǎn)單片機(jī) ? 作者:玩轉(zhuǎn)單片機(jī) ? 2021-03-02 10:23 ? 次閱讀

上周像往常一樣例行檢查線上機(jī)器性能,突然發(fā)現(xiàn)一個(gè)服務(wù)的內(nèi)存使用率是這樣的:

很顯然該服務(wù)存在內(nèi)存泄漏問(wèn)題,趕緊排查問(wèn)題。

問(wèn)題排查

首先確定內(nèi)存泄漏問(wèn)題出現(xiàn)的時(shí)間,發(fā)現(xiàn)在該時(shí)間點(diǎn)的上線有兩次代碼提交,其中一個(gè)就是我的。于是立刻排查這兩次代碼的改動(dòng),確定了另一個(gè)同事的代碼不可能會(huì)有內(nèi)存問(wèn)題后(因?yàn)榱硪粋€(gè)同事的上線僅僅修改了配置)我知道肯定是自己的代碼出現(xiàn)了問(wèn)題。

確定了問(wèn)題所在后趕緊把自己的代碼回滾掉,接下來(lái)就可以放心debug了。

Debug

什么是內(nèi)存泄漏?

簡(jiǎn)單的講就是程序員申請(qǐng)的內(nèi)存在使用完后沒(méi)有還給操作系統(tǒng),由于筆者使用的是C++語(yǔ)言,因此內(nèi)存泄漏一般是這樣的:

obj* o = new obj(); ... // 使用完obj后沒(méi)有delete

肯定有什么地方申請(qǐng)了內(nèi)存后沒(méi)有調(diào)用delete釋放內(nèi)存。

在這里介紹一下筆者的代碼改動(dòng),我的任務(wù)其實(shí)是重構(gòu)一段代碼,把這段代碼并行化。也就是舊的邏輯是在一個(gè)線程中串行執(zhí)行的,現(xiàn)在我要把這段邏輯放到兩個(gè)線程中并行執(zhí)行,這是最讓人頭疼的任務(wù)之一,并行化改造是比較容易出bug的。

接下來(lái)梳理了一遍中所有內(nèi)存的申請(qǐng)和釋放,這其中包括:

使用new/delete分配釋放的內(nèi)存

使用內(nèi)存池分配釋放的內(nèi)存

仔細(xì)梳理一遍后沒(méi)有發(fā)現(xiàn)任何問(wèn)題,該釋放的內(nèi)存都已經(jīng)釋放掉了,這時(shí)筆者已經(jīng)開(kāi)始懷疑人生了 :) ,很顯然還有一段沒(méi)有注意到的地方出現(xiàn)了問(wèn)題,這是必然的,雖然知道問(wèn)題必然出現(xiàn)在改動(dòng)的這些代碼里但是我并不能確定出現(xiàn)的位置。

沒(méi)有辦法,到這里基本上已經(jīng)要放棄自己人肉debug了,想利用一些內(nèi)存檢測(cè)工具來(lái)幫助自己確定問(wèn)題。

常見(jiàn)的內(nèi)存泄漏檢測(cè)工具包括valgrind、gperftools等,valgrind的好處在于無(wú)需重新編譯代碼即可進(jìn)行內(nèi)存檢測(cè),但是缺點(diǎn)是會(huì)使得程序運(yùn)行非常緩慢,官方文檔給的說(shuō)法是會(huì)比正常的程序運(yùn)行慢20-30倍;gperftools則需要重新編譯可執(zhí)行程序。這些工具需要下載安裝測(cè)試,其中還涉及到申請(qǐng)機(jī)器權(quán)限等問(wèn)題,筆者覺(jué)得還是比較麻煩,況且這個(gè)問(wèn)題也不是大海撈針一樣,問(wèn)題肯定出在了并行化的這段代碼中。

到這里我決定再換一個(gè)思路來(lái)排查問(wèn)題,既然代碼重構(gòu)后開(kāi)始并行執(zhí)行,那么出現(xiàn)問(wèn)題大概率是因?yàn)槎嗑€程問(wèn)題,遇到多線程問(wèn)題首先重點(diǎn)排查的就是線程間的共享數(shù)據(jù)。

多線程問(wèn)題的關(guān)鍵——共享數(shù)據(jù)

我們知道如果線程之間沒(méi)有共享數(shù)據(jù)那么就不會(huì)有線程安全問(wèn)題,我們使用的鎖、信號(hào)量、條件變量等其實(shí)都是用來(lái)保護(hù)共享數(shù)據(jù)的,比如鎖通常是用來(lái)包括臨界區(qū)的,臨界區(qū)中的代碼操作的就是線程共享數(shù)據(jù);信號(hào)量使用的一個(gè)經(jīng)典場(chǎng)景就是生產(chǎn)者消費(fèi)者問(wèn)題,生產(chǎn)者線程以及消費(fèi)者線程都會(huì)操作同一個(gè)隊(duì)列,這里的隊(duì)列就是共享數(shù)據(jù)。

沿著這個(gè)思路開(kāi)始找在兩個(gè)線程中都使用到的共享數(shù)據(jù),果不其然,在一個(gè)角落中發(fā)現(xiàn)了這樣一段代碼:

auto* pb = global->mutable_obj();

這是分配protobuf對(duì)象的一段代碼,protobuf是Google開(kāi)發(fā)是一種類似于JSON、XML的技術(shù),因此常用于網(wǎng)絡(luò)通信和數(shù)據(jù)交換等場(chǎng)景,比如RPC等。

如果你不了解protobuf也沒(méi)有關(guān)系,實(shí)際上上面的這段代碼的要做的事情是這樣的:

if (global->obj == NULL) { global->obj = new obj();}return global->obj;

值得注意的是這段代碼現(xiàn)在會(huì)在兩個(gè)線程中執(zhí)行,顯然問(wèn)題就出現(xiàn)在了這里。

那么問(wèn)題是怎么出現(xiàn)的呢?

我們假設(shè)有兩個(gè)線程,線程A和線程B,當(dāng)這樣一段代碼在線程AB中同時(shí)執(zhí)行時(shí)可能會(huì)有以下場(chǎng)景:

線程A拿到global->obj并檢測(cè)到此時(shí)的global->obj為空,因此決定為其分配內(nèi)存,但不巧的是此時(shí)發(fā)生線程切換,線程A在為global->obj分配內(nèi)存前被暫停運(yùn)行,如下所示:

if (global->obj == NULL) { <------- 線程切換,線程A被暫停執(zhí)行 global->obj = new obj();}return global->obj;

線程A被暫停運(yùn)行后線程B開(kāi)始執(zhí)行,這段代碼同樣會(huì)在線程B中執(zhí)行一遍,因此線程B會(huì)首先檢查global->obj發(fā)現(xiàn)為空,因此為global->obj分配內(nèi)存,分配完內(nèi)存后發(fā)生線程切換,線程B被暫停運(yùn)行,如下所示:

if (global->obj == NULL) { global->obj = new obj(); <------- 線程切換,線程B被暫停執(zhí)行 }return global->obj;

線程B被暫停運(yùn)行后調(diào)度器決定重新運(yùn)行線程A,此時(shí)線程A開(kāi)始從被中斷的地方繼續(xù)運(yùn)行,還記得線程A是從哪里被中斷的嗎,沒(méi)錯(cuò),就是在為global->obj分配內(nèi)存前被中斷的,此時(shí)線程A繼續(xù)運(yùn)行,也就是說(shuō)global->obj = new obj()這段代碼又被執(zhí)行了一次,雖然線程B已經(jīng)為global->obj分配了內(nèi)存。

Oops,典型的內(nèi)存泄漏,線程B分配的內(nèi)存再也無(wú)法被正常釋放掉了。

至此,我們已經(jīng)找到了問(wèn)題的原因,罪魁禍?zhǔn)拙褪枪蚕頂?shù)據(jù),關(guān)鍵的一點(diǎn)是要意識(shí)到你的線程會(huì)隨時(shí)被中斷執(zhí)行,CPU會(huì)隨時(shí)切換到其它線程。

代碼修復(fù)也非常簡(jiǎn)單,再新增一個(gè)變量,兩個(gè)線程不在使用共享數(shù)據(jù),到這里問(wèn)題就解決了,從發(fā)現(xiàn)問(wèn)題到完成修復(fù)耗時(shí)大概4小時(shí)。

經(jīng)驗(yàn)教訓(xùn)

代碼的并行化重構(gòu)是一件非常棘手的任務(wù),很容易出現(xiàn)線程安全問(wèn)題,解決線程安全問(wèn)題首先要考慮的不是要不要加鎖,而是多個(gè)線程是否真的有必要使用共享數(shù)據(jù),沒(méi)有必要的話多個(gè)線程操作私有數(shù)據(jù)根本就不會(huì)出現(xiàn)線程安全問(wèn)題。

當(dāng)出現(xiàn)線程安全問(wèn)題時(shí),第一時(shí)間重點(diǎn)排查線程使用的共享數(shù)據(jù)。

內(nèi)存泄漏檢測(cè)工具

雖然這些沒(méi)有使用檢測(cè)工具全靠人肉debug其實(shí)還是因?yàn)閱?wèn)題排查范圍比較小,如果我們根本就不知道問(wèn)題出現(xiàn)在了那次代碼改動(dòng)那么檢測(cè)工具就非常重要了,在這里簡(jiǎn)單介紹一下valgrind的使用,詳細(xì)的介紹請(qǐng)參考官方文檔。

假設(shè)有這樣一段問(wèn)題代碼:

#include voidf(void) { int* x = malloc(10 * sizeof(int)); x[10] = 0; // 問(wèn)題1: 越界} // 問(wèn)題2: 內(nèi)存泄漏,x沒(méi)有被釋放掉 intmain(){ f(); return 0;}

這段代碼中有兩個(gè)問(wèn)題:一個(gè)是數(shù)據(jù)的越界訪問(wèn);另一個(gè)是內(nèi)存泄漏。將該程序編譯為myprog。

接下來(lái)使用valgrind來(lái)檢查該程序,使用以下命令:

valgrind --leak-check=yes myprog

運(yùn)行完成后valgrind會(huì)給出檢測(cè)報(bào)告,關(guān)于程序越界訪問(wèn)會(huì)給出這樣的輸出:

==19182== Invalid write of size 4==19182== at 0x804838F: f (example.c:6)==19182== by 0x80483AB: main (example.c:11)==19182== Address 0x1BA45050 is 0 bytes after a block of size 40 alloc'd==19182== at 0x1B8FF5CD: malloc (vg_replace_malloc.c:130)==19182== by 0x8048385: f (example.c:5)==19182== by 0x80483AB: main (example.c:11)

第一行告訴你代碼中存在Invalid write,也就是無(wú)效的寫(xiě),并給出了問(wèn)題出現(xiàn)的位置。

關(guān)于內(nèi)存泄漏問(wèn)題會(huì)給出這樣的輸出:

==19182== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1==19182== at 0x1B8FF5CD: malloc (vg_replace_malloc.c:130)==19182== by 0x8048385: f (example.c:5)==19182== by 0x80483AB: main (example.c:11)

這里第一行報(bào)告了內(nèi)存"definitely lost",也就是說(shuō)一定會(huì)存在內(nèi)存泄漏,并給出了問(wèn)題出現(xiàn)的位置。

實(shí)際上除了"definitely lost",valgrind還會(huì)給出"probably lost"的報(bào)告,這兩種報(bào)告的含義是這樣的:

"definitely lost":你的程序一定存在內(nèi)存泄漏問(wèn)題,修復(fù)。

"probably lost":你的程序看起來(lái)像是有內(nèi)存泄漏,有可能你在使用指針完成一些特定操作,因此不一定100%存在問(wèn)題。

原文標(biāo)題:一個(gè)耗時(shí)4小時(shí)的內(nèi)存泄漏問(wèn)題

文章出處:【微信公眾號(hào):玩轉(zhuǎn)單片機(jī)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

責(zé)任編輯:haq

聲明:本文內(nèi)容及配圖由入駐作者撰寫(xiě)或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問(wèn)題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • 數(shù)據(jù)
    +關(guān)注

    關(guān)注

    8

    文章

    6820

    瀏覽量

    88747
  • 服務(wù)器
    +關(guān)注

    關(guān)注

    12

    文章

    8965

    瀏覽量

    85087
  • 內(nèi)存
    +關(guān)注

    關(guān)注

    8

    文章

    2978

    瀏覽量

    73818

原文標(biāo)題:一個(gè)耗時(shí)4小時(shí)的內(nèi)存泄漏問(wèn)題

文章出處:【微信號(hào):mcu168,微信公眾號(hào):硬件攻城獅】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    如何檢測(cè)內(nèi)存泄漏

    檢測(cè)內(nèi)存泄漏是軟件開(kāi)發(fā)過(guò)程中一項(xiàng)至關(guān)重要的任務(wù),它有助于識(shí)別和解決那些導(dǎo)致程序占用過(guò)多內(nèi)存資源,從而影響程序性能甚至導(dǎo)致程序崩潰的問(wèn)題。以下將詳細(xì)闡述幾種常見(jiàn)的內(nèi)存
    的頭像 發(fā)表于 07-30 11:50 ?1350次閱讀

    NONOS 1.5.3/1.5.4 SSL內(nèi)存泄漏的原因?

    我已經(jīng)通過(guò)隨附的代碼驗(yàn)證了當(dāng)發(fā)生 SSL 握手錯(cuò)誤時(shí),會(huì)生成內(nèi)存泄漏 此外,espconn_reconnect_callback不稱為信令ESPCONN_HANDSHAKE - TCP SSL 握手
    發(fā)表于 07-18 07:24

    使用system_show_malloc()檢查內(nèi)存泄漏遇到異常怎么解決?

    我想使用system_show_malloc()檢查內(nèi)存泄漏,但是當(dāng)我調(diào)用該函數(shù)時(shí),我得到了致命的異常: 致命異常 28 (LoadProhibitedCause): epc1
    發(fā)表于 07-10 06:32

    C語(yǔ)言內(nèi)存泄漏問(wèn)題原理

    內(nèi)存泄漏問(wèn)題只有在使用堆內(nèi)存的時(shí)候才會(huì)出現(xiàn),棧內(nèi)存存在內(nèi)存泄漏問(wèn)題,因?yàn)闂?/div>
    發(fā)表于 03-19 11:38 ?455次閱讀
    C語(yǔ)言<b class='flag-5'>內(nèi)存</b><b class='flag-5'>泄漏</b>問(wèn)題原理

    減速機(jī)滲油問(wèn)題如何處理

    電子發(fā)燒友網(wǎng)站提供《減速機(jī)滲油問(wèn)題如何處理.docx》資料免費(fèi)下載
    發(fā)表于 03-05 09:18 ?2次下載

    【鴻蒙】webview內(nèi)存泄漏問(wèn)題的分析報(bào)告

    1 關(guān)鍵字 webview;內(nèi)存泄漏 2 問(wèn)題描述 問(wèn)題現(xiàn)象:在 3.1release 版本和 3.2bete1 版本中,在 RK3568 上使用 etsWeb 和其他瀏覽器時(shí),webview 所占
    的頭像 發(fā)表于 03-02 15:12 ?2040次閱讀

    數(shù)組和鏈表在內(nèi)存中的區(qū)別 數(shù)組和鏈表的優(yōu)缺點(diǎn)

    數(shù)組和鏈表在內(nèi)存中的區(qū)別 數(shù)組和鏈表的優(yōu)缺點(diǎn)? 數(shù)組和鏈表是常見(jiàn)的數(shù)據(jù)結(jié)構(gòu),用于組織和存儲(chǔ)數(shù)據(jù)。它們在內(nèi)存中的存儲(chǔ)方式以及優(yōu)缺點(diǎn)方面存在一些顯著的差異。本文將詳細(xì)探討這些差異以及它們的優(yōu)缺點(diǎn)。 1.
    的頭像 發(fā)表于 02-21 11:30 ?859次閱讀

    TC275在內(nèi)存分段預(yù)警處理之后,設(shè)置的全局變量初始值不正確怎么解決?

    大家好想問(wèn)一下,tc275里,自己在地圖文件里定義有了新的存檔段,又設(shè)置了首地位置,段內(nèi)對(duì)象可寫(xiě),4字節(jié)對(duì)齊。但是在內(nèi)存分段預(yù)警處理之后,設(shè)置的全局變量初始值不正確,板子上電后會(huì)給出一個(gè)隨機(jī)值,而不會(huì)是自己設(shè)定的初始值,這怎么解決了呢,具體附圖 ?
    發(fā)表于 01-22 06:40

    內(nèi)存溢出與內(nèi)存泄漏:定義、區(qū)別與解決方案

    內(nèi)存溢出與內(nèi)存泄漏:定義、區(qū)別與解決方案? 內(nèi)存溢出和內(nèi)存泄漏是計(jì)算機(jī)科學(xué)中常見(jiàn)的問(wèn)題,在開(kāi)發(fā)和
    的頭像 發(fā)表于 12-19 14:10 ?2361次閱讀

    何處理MOS管小電流發(fā)熱?

    何處理MOS管小電流發(fā)熱?
    的頭像 發(fā)表于 12-07 15:13 ?575次閱讀
    如<b class='flag-5'>何處理</b>MOS管小電流發(fā)熱?

    什么是串?dāng)_?該如何處理它?

    什么是串?dāng)_?該如何處理它?
    的頭像 發(fā)表于 12-05 16:39 ?774次閱讀
    什么是串?dāng)_?該如<b class='flag-5'>何處理</b>它?

    java內(nèi)存溢出的幾種原因和解決辦法

    內(nèi)存,但是如果程序中存在內(nèi)存泄漏(Memory Leak)或者使用不當(dāng)?shù)臄?shù)據(jù)結(jié)構(gòu)等問(wèn)題,仍然有可能導(dǎo)致內(nèi)存溢出。下面將詳細(xì)介紹Java內(nèi)存
    的頭像 發(fā)表于 11-23 14:44 ?5959次閱讀

    全志R128內(nèi)存泄漏調(diào)試案例

    ,音樂(lè)停止播放,報(bào)錯(cuò)如下: 問(wèn)題分析 根據(jù)上面報(bào)錯(cuò)的log,播放停止時(shí),系統(tǒng)內(nèi)存不足;在老化過(guò)程中出現(xiàn)的內(nèi)存不足,一般是某處存在內(nèi)存泄漏 reboot重啟,重新執(zhí)行老化播放流程,串口
    發(fā)表于 11-20 16:54

    如何發(fā)現(xiàn)內(nèi)存泄漏

    檢測(cè)兩個(gè)角度介紹在 Linux 環(huán)境進(jìn)行內(nèi)存泄漏檢測(cè)的方法,并重點(diǎn)介紹靜態(tài)分析工具 BEAM、動(dòng)態(tài)監(jiān)測(cè)工具 Valgrind 和 rational purify 的使用方法。相信通過(guò)本文的介紹,能給大家對(duì)處理其它產(chǎn)品或項(xiàng)目
    的頭像 發(fā)表于 11-13 15:41 ?555次閱讀

    線程內(nèi)存泄漏問(wèn)題的定位

    記錄一個(gè)關(guān)于線程內(nèi)存泄漏問(wèn)題的定位過(guò)程,以及過(guò)程中的收獲。 1. 初步定位 是否存在內(nèi)存泄漏:想到內(nèi)存
    的頭像 發(fā)表于 11-13 11:38 ?571次閱讀
    線程<b class='flag-5'>內(nèi)存</b><b class='flag-5'>泄漏</b>問(wèn)題的定位