金融業(yè)反洗錢培訓網站seo咨詢推廣
目錄
1、WebRTC簡介
2、問題現象描述
3、將Windbg附加到目標進程上分析
3.1、Windbg沒有附加到主程序進程上,沒有感知到異常或中斷
3.2、Windbg感知到了中斷,中斷在DebugBreak函數調用上?? ?
3.3、32位進程用戶態(tài)虛擬地址和內核態(tài)虛擬地址的劃分
4、用戶態(tài)內存不足問題分析虛擬
4.1、判斷是內存不足導致了malloc申請內存失敗
4.2、為啥會中斷在DebugBreak函數調用處呢?
5、占用程序進程的虛擬內存的因素有哪些??? ?
5.1、二進制文件
5.2、線程的??臻g
5.3、程序中申請的堆內存
6、當前用戶態(tài)虛擬內存占用高的解決辦法
6.1、修改WebRTC編譯選項,減少內存占用
6.2、將程序做成64位的
6.3、使用Visual Studio的鏈接選項,將用戶態(tài)虛擬內存從2GB擴充到3GB
6.4、使用多進程模式
7、最后
VC++常用功能開發(fā)匯總(專欄文章列表,歡迎訂閱,持續(xù)更新...)https://blog.csdn.net/chenlycly/article/details/124272585C++軟件異常排查從入門到精通系列教程(專欄文章列表,歡迎訂閱,持續(xù)更新...)
https://blog.csdn.net/chenlycly/article/details/125529931C++軟件分析工具從入門到精通案例集錦(專欄文章正在更新中...)
https://blog.csdn.net/chenlycly/article/details/131405795C/C++基礎與進階(專欄文章,持續(xù)更新中...)
https://blog.csdn.net/chenlycly/category_11931267.html開源組件及數據庫技術(專欄文章,持續(xù)更新中...)
https://blog.csdn.net/chenlycly/category_12458859.html? ? ? ?本文分享一下最近遇到的一個因虛擬內存不足導致程序發(fā)生閃退的問題排查案例,案例具有一定的代表性,希望能給大家提供一定的借鑒或參考。
1、WebRTC簡介
? ? ? ?本案例中的軟件是基于開源庫WebRTC構建的,發(fā)生的軟件問題也是與WebRTC有關的,所以先給大家簡要地介紹一下WebRTC相關的內容。
? ? ? ?WebRTC(Web Real-Time Communication)是一個由Google發(fā)起的實時音視頻通訊C++開源庫,其提供了音視頻采集、編碼、網絡傳輸,解碼顯示等一整套音視頻解決方案,我們可以通過該開源庫快速地構建出一個音視頻通訊應用。
一個實時音視頻應用軟件一般都會包括這樣幾個環(huán)節(jié):音視頻采集、音視頻編碼(壓縮)、前后處理(美顏、濾鏡、回聲消除、噪聲抑制等)、網絡傳輸、解碼渲染(音視頻播放)等。其中每一個細分環(huán)節(jié),還有更細分的技術模塊。
? ? ? 雖然其名為WebRTC,但是實際上它不光支持Web之間的音視頻通訊,還支持Windows、Android以及iOS等移動平臺。WebRTC底層是用C/C++開發(fā)的,具有良好的跨平臺性能。
? ? ? ?WebRTC因為其較好的音視頻效果及良好的網絡適應性,目前已被廣泛的應用到視頻會議、實時音視頻直播等領域中。在視頻會議領域,騰訊會議、華為WeLink、字節(jié)飛書、阿里釘釘、小魚易連、廈門億聯(lián)等國產廠商均提供了基于WebRTC方案的視頻會議。
? ? ? ?大家熟知的音視頻專業(yè)服務商聲網(Agora),更是基于開源WebRTC庫,提供了社交直播、教育、游戲電競、IoT、AR/VR、金融、保險、醫(yī)療、企業(yè)協(xié)作等多個行業(yè)的音視頻互動解決方案。使用聲網服務的企業(yè)包括小米、陌陌、斗魚、嗶哩嗶哩、新東方、小紅書、HTC VIVE 、The Meet Group、Bunch、Yalla等遍布全球的巨頭、獨角獸及創(chuàng)業(yè)企業(yè)。除了頭部公司聲網之外,也陸續(xù)有多家公司基于開源的WebRTC,開發(fā)出了多個音視頻應用,提供了多個領域的音視頻通信解決方案。
2、問題現象描述
? ? ? ?基于WebRTC的會議軟件在加入會議后,會時不時發(fā)生閃退,雖然不是必現的,但已經有好幾個用戶反饋了,并且在某技術支持同事的電腦上也出現了頻繁閃退的問題。程序閃退時,程序中安裝的異常捕獲模塊沒有感知到,所以沒有生成dump文件。
雖然我們在程序中安裝了異常捕獲模塊,但并不能捕獲所有的軟件異?;虮罎?#xff0c;只能捕獲大部分的異常,所以程序發(fā)生異常時沒有感知到,也實屬正常。
3、將Windbg附加到目標進程上分析
? ? ? ?對于這類沒有生成dump文件的場景,就需要使用Windbg進行動態(tài)調試了,即將Windbg附加到目標進程上,和目標進程一起跑。于是讓這個技術支持同事每次運行軟件時,都手動將Windbg附加到進程上,和進程一起跑,這樣一旦程序發(fā)生異常,Windbg就會感知到,就會中斷下來,這樣就能看到出問題時的函數調用堆棧,就可以分析了。
3.1、Windbg沒有附加到主程序進程上,沒有感知到異?;蛑袛?/h4>
? ? ? ?但這位同事復現問題后,Windbg并沒有中斷下來,即Windbg沒有感知到,按講是不應該的,一旦程序發(fā)生異常,正在調試的Windbg會第一時間感知到并中斷下來。后來發(fā)現,他是在Windbg中打開桌面快捷方式文件去啟動軟件的,而桌面快捷方式指向的是個引導啟動的程序,不是主程序,該程序做一些初始校驗的操作,校驗通過后會自動將主程序啟動起來。這樣Windbg附加到的是引導程序的進程上,并沒有附加到主程序進程上。
就像QQ的桌面快捷方式指向的是QQ安裝目錄下的QQScLauncher.exe:
該程序會做一些啟動主程序QQ.exe前的一些檢查,然后會將主程序QQ.exe啟動起來。
在我們這個問題中,可以先將主程序啟動起來,然后再將Widnbg附加到主程序進程上,就可以了。也可以通過Windbg將主程序啟動起來,需要到安裝目錄中找到主程序的exe文件,打開該文件,不能通過桌面快捷方式去啟動。
3.2、Windbg感知到了中斷,中斷在DebugBreak函數調用上?? ?
? ? ? ?后來同事每次運行程序時都將Windbg附加到程序進程上,復現了問題,正在調試的Windbg中斷了下來,發(fā)現中斷在DebugBreak接口調用處,如下所示:
輸入kn命令查看此時的函數調用堆棧:?
正是DebugBreak接口就是讓正在調試的進程中斷下來的。使用kn命令查看此時的函數調用堆棧,發(fā)現是WebRTC庫中在調用malloc動態(tài)申請內存時返回了NULL,然后WebRTC庫內部認為是異常情況,可能是認為內存申請不到后相關的業(yè)務都沒法執(zhí)行了,程序繼續(xù)運行就沒有意義了,于是直接調用abort接口將進程終止了。
? ? ? ?這就能說明為啥程序閃退時異常捕獲模塊沒有感知到異常,因為malloc申請內存失敗返回NULL,并沒有產生C++異常,程序閃退是因為WebRTC內部調用abort接口強行將進程終止掉了。
? ? ? ?至于為啥會出現malloc申請內存失敗的問題呢?估計是用戶態(tài)的虛擬內存不夠用了,我們的程序是32位的,系統(tǒng)會給程序進程分配4GB的虛擬內存,其中2GB是用戶態(tài)的虛擬內存,我們的程序用戶態(tài)的內存快到達上限了,沒有空閑內存可用了,所以malloc申請內存失敗了,返回了NULL。
? ? ? 關于如何使用Windbg進行動態(tài)調試(使用Windbg進行動態(tài)調試的完整步驟),可以參見我之前寫的文章:
使用Windbg動態(tài)調試目標進程的一般步驟及要點詳解https://blog.csdn.net/chenlycly/article/details/131029795
3.3、32位進程用戶態(tài)虛擬地址和內核態(tài)虛擬地址的劃分
? ? ? ?對于32為程序,是按32位進行內存尋址的,所以給32位程序進程分配了4GB的虛擬內存,程序中所使用的內存均是從這4GB的虛擬內存上劃撥的,比如全局變量占用的內存、線程棧內存、程序申請的堆內存、二進制文件占用的代碼段內存等。
? ? ? ?32位進程的這4GB虛擬內存,在Windows平臺上,默認情況下,2GB是用戶態(tài)虛擬內存,2GB是內核態(tài)虛擬內存;在Linux平臺上,默認情況下,3GB是用戶態(tài)虛擬內存,1GB是內核態(tài)虛擬內存。
? ? ? ?此外,用戶態(tài)的代碼只能訪問用戶態(tài)的內存地址,是禁止訪問內核態(tài)內存地址的;內核態(tài)的代碼只能訪問內核態(tài)的內存地址,是禁止訪問用戶態(tài)內存地址的。關于32位進程和64位進程的虛擬內存地址劃分,可以參見《Windows核心編程》一書中的截圖:
? ? ? ?我們以前在排查軟件異常崩潰時,經常遇到崩潰的那條匯編代碼(位于用戶態(tài)代碼中)中訪問了內核態(tài)的內存地址(可能是訪問了未初始化的變量內存,也可能是訪問了因內存越界被篡改的內存),用戶態(tài)代碼是禁止訪問內核態(tài)內存地址的,強行訪問會觸發(fā)內存訪問違例,引發(fā)程序崩潰。
? ? ? C++軟件異常基本內存有關,關于引發(fā)C++程序內存錯誤的常見原因,可以參見我之前的文章:
引發(fā)C++程序內存錯誤的常見原因分析與總結https://blog.csdn.net/chenlycly/article/details/128599525
4、用戶態(tài)內存不足問題分析虛擬
4.1、判斷是內存不足導致了malloc申請內存失敗
? ? ? ?復現問題時Windbg正好中斷下來,軟件進程暫停下來,正好此時使用Process Explorer查看我們軟件進程的虛擬內存,確實已經用到1.8GB左右了,快接近2GB的上限了!其實離2GB的上限還有200MB的空余,但可能因為內存碎片的存在,都是一小塊一小塊分散的小內存塊,而程序中要申請的是一段連續(xù)的內存塊,找不到指定大小的連續(xù)內存塊,就會出現內存分配失敗了。
? ? ? ?如果在正在調試的Windbg中使用.dump命令手動導出dump文件,我們也可以事后通過dump文件的大小去初步估計出問題時進程占用的虛擬內存大小。在Windbg中導出的dump文件,屬于全dump文件,將當時進程的內存信息都保存了下來,dump文件的大小接近當時進程的虛擬內存大小,可能會略小一點點。
? ? ? ?內存碎片的概念之前聽說過,在這種場景下才感受到內存碎片的危害!有人說可以使用內存池,內存池可以減少內存碎片的出現,但實際上程序業(yè)務需要占用更多的內存,減少內存碎片也解決不了問題。
4.2、為啥會中斷在DebugBreak函數調用處呢?
? ? ? ?復現問題時,為啥會中斷在DebugBreak函數調用處呢?查看了函數調用堆棧中函數在WebRTC中的源碼,malloc返回失敗的代碼如下所示:
1)申請內存的malloc返回NULL:
2)malloc返回NULL,會執(zhí)行到RTC_CHECK宏中的rtc_FatalMessage接口:?
3)緊接著調用到FatalLog接口:?
4)?FatalLog接口的實現如下:
我們在FatalLog接口的結尾處我們看到了DebugBreak系統(tǒng)API接口的調用,然后緊接著調用C函數abort。
? ? ? ?Windows API接口DebugBreak的作用就是讓正在調試的調試器中斷下來,目的是讓調試器感知到當前的事件,所以Windbg中斷在DebugBreak函數的調用處。此外,在調用DebugBreak接口后,會緊接著調用abort接口將當前進程終止掉,應該是WebRTC內部認為內存申請失敗了,業(yè)務沒法正常展開了,程序沒法正常運行,沒有繼續(xù)存活下去的意義了,所以強行將程序進程終止了。
? ? ? ?調用C函數malloc申請內存申請失敗時,malloc會返回NULL,不會拋出異常;如果使用new去申請,申請失敗時默認會拋出bad_alloc異常,如果程序中沒有處理這個異常,則會導致程序發(fā)生異常崩潰。當然,我們可以在new時使用nothrow,不讓其拋出異常,返回NULL,示例代碼如下:
#include <iostream>int main(){char *p = NULL;int i = 0;do{p = new(std::nothrow) char[10*1024*1024]; // 每次申請10MBi++;Sleep(5);}while(p);if(NULL == p){std::cout << "分配了 " << (i-1)*10 << " M內存" //分配了 1890 Mn內存第 1891 次內存分配失敗 << "第 " << i << " 次內存分配失敗";}return 0;
}
? ? ? ?另外,abort的調用也會讓正在調試的Windbg中斷下來,我們來看看abort函數的內部實現:
/***
*void abort() - abort the current program by raising SIGABRT
*
*Purpose:
* print out an abort message and raise the SIGABRT signal. If the user
* hasn't defined an abort handler routine, terminate the program
* with exit status of 3 without cleaning up.
*
* Multi-thread version does not raise SIGABRT -- this isn't supported
* under multi-thread.
*******************************************************************************/
void __cdecl abort (void)
{_PHNDLR sigabrt_act = SIG_DFL;#ifdef _DEBUGif (__abort_behavior & _WRITE_ABORT_MSG){/* write the abort message */_NMSG_WRITE(_RT_ABORT);}
#endif /* _DEBUG *//* Check if the user installed a handler for SIGABRT.* We need to read the user handler atomically in the case* another thread is aborting while we change the signal* handler.*/sigabrt_act = __get_sigabrt();if (sigabrt_act != SIG_DFL){raise(SIGABRT);}/* If there is no user handler for SIGABRT or if the user* handler returns, then exit from the program anyway*/if (__abort_behavior & _CALL_REPORTFAULT){_call_reportfault(_CRT_DEBUGGER_ABORT, STATUS_FATAL_APP_EXIT, EXCEPTION_NONCONTINUABLE);}/* If we don't want to call ReportFault, then we call _exit(3), which is the* same as invoking the default handler for SIGABRT*/_exit(3);
}
上述代碼中先調用了raise(SIGABRT),該函數是觸發(fā)一個SIGABRT信號終止異常,如果當前正在調試狀態(tài),會讓調試器中斷下來。接下來調用C函數_exit退出當前進程。?
? ? ? ?關于調用abort強制終止程序導致程序閃退的案例,還可以查看我之前的文章:
C++程序中執(zhí)行abort等操作導致沒有生成dump文件的問題案例分析https://blog.csdn.net/chenlycly/article/details/129003869
5、占用程序進程的虛擬內存的因素有哪些??? ?
? ? ? 占用進程的虛擬內存空間的因素有很多,這里大概地羅列幾個,大家可以簡單地了解一下。我們可以從程序的五大內存分區(qū)的角度去看,程序的五大內存分區(qū)如下:
5.1、二進制文件
? ? ? ?主程序及主程序依賴的二進制庫文件,都需要加載到進程空間中,都占用一定的虛擬內存。exe主程序依賴底層的多個業(yè)務庫和系統(tǒng)dll庫,比如業(yè)務庫有組件dll庫、協(xié)議dll庫、網絡dll庫、開源庫等,可能會依賴上百個dll庫。這些dll庫在主程序啟動時,會加載到程序進程的進程空間中,會占用進程的虛擬內存空間,屬于代碼段的虛擬內存。
? ? ? ?如果能減少dll庫的依賴,減小依賴的dll庫文件的大小,可以減少程序對虛擬內存的占用。特別對于一些大型的開源庫,一方面要減少程序安裝包的大小,另一方面減少二進制文件對虛擬內存的占用,需要進行一些裁剪,開源庫中也提供了一些宏開關和編譯選項。比如google開源的嵌入式瀏覽器框架庫libcef,默認是比較大的,編譯后的dll庫要占到好幾十MB,可以對該庫進行裁剪。有時為了進行深度的裁剪和優(yōu)化,甚至會去直接修改開源代碼。
5.2、線程的??臻g
? ? ? ?程序中創(chuàng)建了多個線程(多個模塊都創(chuàng)建了線程),每個線程都要分配對應的棧空間,線程越多占用的??臻g越多,這是棧空間也是從進程的虛擬內存上劃撥的。但對于應用程序,開多個線程去并行處理任務,也是必不可少的。
對于線程占用的棧空間大小,Windows下每個線程默認的??臻g大小是1MB,Linux下每個線程的默認的??臻g是8MB。
5.3、程序中申請的堆內存
? ? ? ? C++程序中使用的大部分內存都是堆內存,堆內存占總虛擬內存的大部分。堆內存是通過使用new或者調用malloc等函數申請的。
在Windows平臺上,動態(tài)申請內存的方式有多種,比如使用new(要用delete去釋放),比如使用malloc(要用free去釋放),再比如調用系統(tǒng)API函數HeapCreate或者HeapAlloc(要用HeapFree去釋放),還有可以調用API函數VirtualAlloc(要用VirtualFree去釋放),還有其他的API函數。
? ? ? ?要盡量節(jié)約內存,按需分配,使用的buffer大小可能會動態(tài)變化,頻繁動態(tài)地去申請和釋放內存,可能會產生很多內存碎片。這也是上來就申請固定長度的buffer的好處,可以減小生成內存碎片。使用內存池可以減少內存碎片,但對于本問題,確實需要使用很多虛擬內存,使用內存池減少內存碎片作用也不是很大。
6、當前用戶態(tài)虛擬內存占用高的解決辦法
? ? ? ?WebRTC開源庫比較大,會消耗很多的內存,如何解決WebRTC占用大量虛擬內存的問題,有如下的方法。
6.1、修改WebRTC編譯選項,減少內存占用
? ? ? ?可以嘗試修改WebRTC編譯選項,對其進行裁剪縮編,釋放出一些占用內存的代碼,但這種做法降低內存的效果有限,因為WebRTC作為大型庫本來就需要占用大量的內存資源。
6.2、將程序做成64位的
? ? ? ?可以將主程序做成64位的,64位程序的用戶態(tài)虛擬內存非常大,可以“肆無忌憚”的使用。但占用的虛擬內存過大,在代碼執(zhí)行過程中虛擬內存要切換到物理內存上,會來回頻繁地切換,也會影響程序的執(zhí)行效率。此外,物理內存較小,也會影響虛擬內存到物理內存的切換,也會顯著降低程序的運行速度。
6.2.1、我們的程序為了兼容32位系統(tǒng),需要做成32位的
? ? ? ?我們的程序之所以還是32位的,是因為我們需要兼容32位系統(tǒng)。有些人說,為啥不直接將主程序做成x64(64位程序)的呢?因為我們的程序還要兼容32位的系統(tǒng),雖然現在普遍使用的Win10和Win11都是64位的,但還有少部分客戶在使用32位的Win7系統(tǒng),什么樣的客戶都有,不排除有使用32位系統(tǒng)的可能。
64位程序是沒法在32位系統(tǒng)上運行的,但32位程序可以在64位系統(tǒng)上運行,這是操作系統(tǒng)去做兼容的。
6.2.2、關于32位程序和64位程序的說明
? ? ? ?32位exe或dll是不能和64位exe或exe文件混用的,系統(tǒng)是嚴格區(qū)分二進制文件位數的,32位exe或dll文件只能使用(依賴)32位exe或dll文件,64位exe或dll文件只能使用(依賴)64位的exe或dll文件。比如32位的exe主程序如果使用64位dll庫,則啟動時會報錯。
? ? ? ?對于Windows系統(tǒng)庫,64位系統(tǒng)的系統(tǒng)庫都有兩個版本的,分別是32位系統(tǒng)庫和64位系統(tǒng)庫。32位的系統(tǒng)庫則存放在C:\Windows\SysWOW64目錄中(注意WOW64不是64位系統(tǒng)庫目錄,WOW64是32位程序運行在64位系統(tǒng)上的意思);64位系統(tǒng)庫存放在C:\Windows\System32目錄中:
? ? ? ?所以,32位程序依賴的32位系統(tǒng)庫都在C:\Windows\SysWOW64目錄中。這個SysWOW64和system32目錄很容易混淆,前幾天還有個同事問我這兩個目錄對應關系到底是啥,他之前一度以為system32目錄下的是32位系統(tǒng)庫,SysWOW64目錄下的是64位系統(tǒng)庫,事實卻正好相反。
? ? ? ?當32位程序在64位Windows上運行時,會有個重定向問題,可以查看我之前的文章:
關于32位程序在64位系統(tǒng)下運行中需要注意的重定向問題(有圖有真相)https://blog.csdn.net/chenlycly/article/details/53119127Win7用戶帳戶控制數據重定向
https://blog.csdn.net/chenlycly/article/details/53408212
6.3、使用Visual Studio的鏈接選項,將用戶態(tài)虛擬內存從2GB擴充到3GB
? ? ? ?可以在Visual Studio鏈接選項中打開擴大用戶態(tài)虛擬內存的選項/L largeAddressAware,如下所示:
這樣可以將用戶態(tài)虛擬內存擴到3GB,這樣可以有效緩解內存不夠用的問題。
? ? ? ?32進程只有4GB的虛擬內存,如果將用戶態(tài)虛擬內存由2GB擴到3GB,內核態(tài)的虛擬內存應該會被壓縮到1GB,這樣會不會導致內核態(tài)的代碼執(zhí)行比較慢,導致程序的運行性能下降呢?可能運行性能會有一定的損失,但既然系統(tǒng)運行這種擴充用戶態(tài)虛擬內存的方式,應該影響不會很大。
6.4、使用多進程模式
? ? ? ?但上述方法,在使用WebRTC開源庫時可能有問題,如果要解更多路數的視頻,會占用更多的內存??梢钥紤]將WebRTC封裝成進程,使用多進程的模式,主進程與WebRTC進程使用RPC方式進行接口的調用。像Chrome那樣,搞多個進程,不同的進程處理不同的事務,一個進程崩潰了,不會影響到主進程,將崩潰的進程重新啟動。但是多個進程之間需要通信,需要協(xié)同控制,控制不好也容易出問題。進程之間如何高效地的傳遞數據也是個問題,這都需要人力和技術去支撐。但多進程模式將是最終且最穩(wěn)妥的解決方案。
7、最后
? ? ? ?針對我們遇到的上述問題,目前的做法是,一邊優(yōu)化內存占用,一邊使用擴大用戶態(tài)虛擬內存的做法,同步進行。