如何配置X86应用程序启用大地址模式(将用户态虚拟内存从2GB扩充到3GB),以解决用户态虚拟内存不够用问题?(项目实战案例解析)

server/2024/10/18 6:32:05/

目录

1、概述

2、为什么不直接将程序做成64位的?

3、进程内存不足导致程序发生闪退的案例分析

3.1、问题说明

3.2、将Windbg附加到程序进程上进行动态调试

3.3、动态调试的Windbg感知到了中断,中断在DebugBreak函数调用上

3.4、malloc或new失败的可能原因分析

3.5、为什么没能生成dump文件?

3.6、本例中malloc返回NULL的原因分析

3.7、为啥有的机器不出现,只在个别电脑上出现?

4、程序用户态虚拟内存占用高导致不够用的解决办法

4.1、修改WebRTC编译选项,减少内存占用

4.2、将程序做成64位的

4.3、使用多进程模式

4.4、使用Visual Studio的链接选项,将用户态虚拟内存从2GB扩充到3GB(最终选择的这个方法)

5、最后


C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/125529931C/C++基础与进阶(专栏文章,持续更新中...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/category_11931267.htmlVC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/124272585C++软件分析工具从入门到精通案例集锦(专栏文章,持续更新中...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/131405795网络编程与网络问题分享(专栏文章,持续更新中...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/category_2276111.html       对于32位程序,默认情况下其用户态虚拟内存只有2GB,可能会出现内存不够用的情况,继而出现后续内存申请失败,导致软件出现异常。本文结合项目中出现的一个具体问题实例,详细讲述问题的排查定位的过程,并详细讨论了解决用户态虚拟内存不够用的手段与策略,最后讲述如何配置X86应用程序启用大地址模式(将用户态虚拟内存从2GB扩充到3GB)去解决内存不够用的问题。

1、概述

        对于32位程序,系统给程序进程分配4GB的虚拟内存,默认情况下,用户态虚拟内存占2GB,内核态虚拟内存占2GB。对于应用程序,业务代码基本都是运行在用户态中的,占用的是用户态的虚拟内存。可能会因为程序模块多占用的内存过大,也可能是程序中存在内存泄漏,导致程序进程占用的用户态虚拟内存达到或者接近2GB的上限,导致后续内存申请失败,或者产生Out of memory内存耗尽的异常。

        如果是内存泄漏导致的,则要排查泄漏的原因,解决泄漏问题。

        如果是程序业务模块过多,占用了大量的内存,使程序占用的用户态虚拟内存接近2GB(快达到2GB的上限),导致后续内存申请失败,则需要对程序占用的内存进行优化,减少程序对虚拟内存的占用。如果内存优化空间有限,仍然无法解决问题,则可以将程序的用户态虚拟内存从2GB扩充到3GB,将问题规避掉。

对于32程序,总的虚拟内存是4GB,默认情况下,用户态虚拟内存占2GB,内核态虚拟内存占2GB。如果将用户态虚拟内存由2GB扩充到3GB,则内核态虚拟内存会从2GB较少到1GB,即内核态虚拟内存就变小了,对运行在内核态的模块的执行效率会带来一定的影响,虽然这种影响不大。另外,尽量对虚拟内存进行优化,如果程序占用的虚拟内存较大,要频繁地在虚拟内存与物理内存之间切换,也会对程序的执行效率产生影响。

2、为什么不直接将程序做成64位的?

       64位程序的虚拟内存到大的多,既然32位程序的虚拟内存有限,为什么不做成64位的呢?32位程序可以在32位操作系统中运行,也可以在64位操作系统中运行(64位系统兼容32位程序)。但64位程序只能在64系统中运行,无法在32位系统运行。

        为了同时支持32位和64位操作系统,将程序都做成32位的,当然,有些软件做成了32位和64位两个版本,可以根据操作系统的位数,选择安装对应位数的程序。

       如果要将程序做成64位的,则程序从上到下的所有模块都要编译成64位的,因为32位模块和64位模块是不同混在一起使用的,如果强行混在一起使用,程序会报错的。

       以64位Windows系统为例,64位系统是如何保证32位程序与64位都能正常运行的呢?程序会依赖很多系统dll库,而32位程序只能依赖使用32位的dll库,64位程序也只能依赖使用64位的dll库。系统为了同时支持32位与64位程序的运行,提供了32位版本和64位版本的系统dll库:

1)C:\Windows\System32:64位系统dll库目录。

2)C:\Windows\SysWOW64:32位系统dll库目录。

系统在启动程序时会根据程序的位数,去选择加载对应位数的系统dll库。

       方便大家理解和记忆,此处说一下C:\Windows\SysWOW64路径中的WOW64的含义,MSDN上对WOW64的解释如下:

WOW64 is the x86 emulator that allows Win32-based applications to run on 64-bit Windows. It is intended to run 32-bit personal productivity applications needed by software developers and administrators. It is not intended to run 32-bit server applications. 

WOW64的大致含义是,W-Win32,O-On,W64-Win64(64-bit Windows),32位程序运行在64系统上。

3、进程内存不足导致程序发生闪退的案例分析

3.1、问题说明

       之前有客户反馈,我们的客户端软件在他们某台华为MATE笔记本电脑上运行时,会时不时地出现闪退问题(问题不是必现的)。程序闪退时并没有弹出崩溃的提示框(如果程序的异常捕获模块感知到程序发生了崩溃,会弹出一个崩溃提示框),说明程序中安装的异常捕获模块并没有感知到异常,所以没有生成dump文件,所以也就没法使用Windbg静态分析dump文件的方式去分析这个问题。

程序中安装的异常捕获模块,大概只能捕获到90%左右场景的异常,有少部分异常是捕捉不到的。对于异常捕获不到的场景,则需要使用其他方法排查分析,比如使用Windbg进行动态调试。

3.2、将Windbg附加到程序进程上进行动态调试

       既然异常捕获模块没有感知到,我们只能将Windbg附加到程序进程上进行动态调试,即将Windbg附加到程序进程上,和程序一起跑,如果程序发生异常,Windbg会第一时间感知到并中断下来,这个时候我们就趁这个中断的机会,去查看函数调用堆栈去分析。

       但这个问题不是必现的,只能让客户每次启动程序时,都手动将Windbg附加到目标进程上,让Windbg跟着程序一起跑。一旦问题复现,Windbg就会感知到并中断下来。


        在这里,给大家重点推荐一下我的几个热门畅销专栏(博客主页还有其他专栏,可以去查看)

专栏1:(该精品技术专栏的订阅量已达到430多个,专栏中包含大量项目实战分析案例,有很强的实战参考价值,广受好评!专栏文章持续更新中,预计更新到200篇以上!)

C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/125529931

本专栏根据多年C++软件异常排查的项目实践,系统地总结了引发C++软件异常的常见原因以及排查C++软件异常的常用思路与方法,详细讲述了C++软件的调试方法与手段,以图文并茂的方式给出具体的项目问题实战分析实例(很有实战参考价值),带领大家逐步掌握C++软件调试与异常排查的相关技术,适合基础进阶和想做技术提升的相关C++开发人员!

考察一个开发人员的水平,一是看其编码及设计能力,二是要看其软件调试能力!所以软件调试能力(排查软件异常的能力)很重要,必须重视起来!能解决一般人解决不了的问题,既能提升个人能力及价值,也能体现对团队及公司的贡献!

专栏中的文章都是通过项目实战总结出来的,包含大量项目问题实战分析案例,有很强的实战参考价值!专栏文章还在持续更新中,预计文章篇数能更新到200篇以上!

专栏2: 

C/C++基础与进阶(专栏文章,持续更新中...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/category_11931267.html

以多年的开发实战经验为基础,总结并讲解一些的C/C++基础与进阶内容,以图文并茂的方式对C++相关知识点进行详细地展开与剖析!专栏涉及了C/C++开发领域多个方面的内容,同时给出C/C++及网络方面的常见笔试面试题,并详细讲述Visual Studio常用调试手段与技巧!

专栏3: 

VC++常用功能开发汇总icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/124272585

专栏将10多年C++开发实践中常用的功能,以高质量的代码展现出来,并对相关功能的实现细节进行了详细的说明。这些常用的代码,其质量与稳定性是有保证的,可以直接拿过去使用,可以有效地解决C++软件开发过程中遇到的问题。


3.3、动态调试的Windbg感知到了中断,中断在DebugBreak函数调用上

       后来同事每次运行程序时都将Windbg附加到程序进程上,复现了问题,正在调试的Windbg中断了下来,发现中断在DebugBreak接口调用处,如下所示:

输入kn命令查看此时的函数调用堆栈: 

正是DebugBreak接口就是让正在调试的进程中断下来的。DebugBreak是Windows API函数,从函数名称上也能看出来,该函数就是让正在调试的调试器中断下来,此时的中断确实是调用了DebugBreak引起的中断。

       于是顺着函数调用堆栈向上看,问题是出在WebRTC开源库中的,然后根据函数调用堆栈中的函数,找到对应的C++源码,看到是代码中调用malloc申请动态内存时,申请内存失败,返回空指针NULL,然后引发DebugBreak的调用,具体流程可以对照下列代码看:

1)申请内存的malloc返回NULL:

2)malloc返回NULL,会执行到RTC_CHECK宏中的rtc_FatalMessage接口: 

3)rtc_FatalMessage接口中紧接着调用到FatalLog接口: 

4) FatalLog接口中调用了DebugBreak接口

       此外,我们在实际调试时发现,使用g命令将DebugBreak函数调用引发的中断跳过去,Windbg还会产生一次中断,是因为调用abort系统函数,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);
}

       abort函数内部是通过调用exit系统函数将当前进程强制退出的,但在退出之前会调用raise(SIGABRT),该函数触发一个SIGABRT信号终止异常,如果当前正在调试状态,会让调试器中断下来。

3.4、malloc或new失败的可能原因分析

       如果malloc申请内存失败,则会返回NULL;如果new申请内存失败,默认会抛出bad_alloc异常。  那为啥会出现malloc或new操作失败的问题呢?之前我们总结过,一般malloc或new失败可能是以下几种原因导致的:

1)申请的内存过大,进程中没有这么大内存可用了
可能受一些异常数据的影响,申请了很大尺寸的内存。比如前段时间排查一个崩溃问题,当时因为数据有异常,一次性申请了9999*9999*4*2 = 762MB的堆内存,进程中没有这么大可用的堆内存了,所以申请失败了。
2)用户态的内存已经达到了上限,申请不到内存了       
有可能是虚拟内存占用太多,也有可能代码中有内存泄露,导致用户态的虚拟内存被消耗完了。对于一个32程序,一个进程分配了4GB的虚拟地址空间,而用户态和内核态内存各占一半,即用户态的虚拟内存只有2GB,如果程序占用的虚拟内存比较大,比如接近2GB的用户虚拟内存了,再申请大的内存就会申请失败了。或者程序中有内存泄露,快要把用户态的2GB的虚拟内存给占用完了,再申请内存可能会申请失败的。
3)进程中的内存碎片过多    
如果进程中在大量的new和delete,产生了大量的小块内存碎片,可用的内存被切割成一小块一小块的小内存块,如果要申请一块长度很长的内存,因为到处是内存碎片,没有这么一大块连续的可用内存,可能会导致内存申请失败的。
4)发生堆内存越界
堆内存被破坏,导致new操作产生异常(此时new不会返回NULL,会抛出异常)。我们可以在出问题的地方,对该处的new添加一个保护(但不可能对代码中所有new的地方都加这样的保护),我们通过添加try...catch去捕获new抛出的异常,并将异常码打印出来,如下所示:(下面的代码在循环申请内存,直到内存申请失败为止,主要用来测试用)

​#include <iostream>
using namespace std;int main(){char *p;int i = 0;try{do{p = new char[10*1024*1024];i++;Sleep(5);}while(p);}catch(const std::exception& e){std::cout << e.what() << "\n"<< "分配了" << i*10 << "M" << std::endl;}return 0;   
}

3.5、为什么没能生成dump文件?

       现在我们再回过头去看看,程序发生闪退时为什么没有生成dump文件。 上面已经分析出程序闪退的原因了,是因为WebRTC开源库中调用malloc申请内存失败(因为进程的用户态虚拟内部不够用了,没有足够空闲的内存可供分配了)返回空指针NULL,WebRTC认为申请内存失败了,业务没法正常跑下去了,认为是致命的,然后最终调用abort系统函数强行将当前进程终止的。

       程序中只是调用malloc申请内存失败,然后调用abort强行终止进程,并没有产生C++异常或崩溃。并没有产生C++异常或崩溃,异常捕获模块是感知不到的,所以没生成dump文件的,这和运行时实际表现出来的现象是完全吻合的!

       如果代码中不是调用malloc去动态申请内存,而是使用new去申请,则在申请不到内存时new内部会抛出bad_alloc异常,这个会导致程序崩溃的,异常捕获模块应该能感知到,会生成dump文件。

3.6、本例中malloc返回NULL的原因分析

       在本例中排除了内存泄漏的可能,推测是程序占用的虚拟内存过多,接近程序用户态虚拟内存2GB的上限,导致后续申请内存时没有足够的内存可供分配了,所以申请内存失败!

       我们软件之前的版本,没有使用WebRTC开源库,一直没有这个问题的。这个问题是在引入WebRTC开源库后才出现的,可能和引入的WebRTC有关系的。WebRTC开源库内部功能庞大,内部包含了大量的业务和逻辑,会占用大量的内存,按讲是不适合用在32位程序,因为32位程序用户态虚拟内存默认只有2GB,很有可能会出现用户态虚拟内存不够用的情况。

3.7、为啥有的机器不出现,只在个别电脑上出现?

       为啥这个问题有的机器不出现,只在个别电脑上出现呢?可能和机器的硬件配置及操作系统版本有关系。不同版本的操作系统的内存管理机制可能是有差异的。WebRTC开源库内部会根据机器的配置及网络带宽,去动态地调整音视频编码的分辨率等参数,会消耗不同大小的内存。

4、程序用户态虚拟内存占用高导致不够用的解决办法

        WebRTC开源库比较大,会消耗很多的内存,如何解决WebRTC占用大量虚拟内存的问题,有如下的方法。

4.1、修改WebRTC编译选项,减少内存占用

        可以尝试修改WebRTC编译选项,对其进行裁剪缩编,释放出一些占用内存的代码,但这种做法降低内存的效果有限,因为WebRTC作为大型库本来就需要占用大量的内存资源。

WebRTC库的源码就有10多个GB,是个非常庞大的开源库,内部包含了大量的业务逻辑和功能,需要占用大量的内存!有些内存在库初始化的时候就申请了,即很多内存一上来就占用上了,而不是需要使用时再去申请。

4.2、将程序做成64位的

       要将程序做成64位的,底层的模块都要编译成64位的,32位模块与64位模块是不同混在一起使用的。如果强行混在一起,则运行会报错的。程序底层包含了上百个模块,都要将代码移植到64位上,可能会遇到这样那样的问题,短时间内完成迁移,会产生很多bug的。

        再就是我们的程序要兼容32位操作系统,目前只能做成32位的,没有人力去分别制作32位版本和64位版本。

即便可以将主程序做成64位的,64位程序的用户态虚拟内存非常大,可以“肆无忌惮”的使用。但占用的虚拟内存过大,在代码执行过程中虚拟内存要切换到物理内存上,会来回在虚拟内存与物理内存之间频繁地切换,也会影响程序的执行效率。此外,物理内存较小,也会影响虚拟内存到物理内存的切换,也会显著降低程序的运行速度。

4.3、使用多进程模式

        但上述方法,在使用WebRTC开源库时可能有问题,如果要解码更多路数的视频,会占用更多的内存。可以考虑将WebRTC封装成进程,使用多进程的模式,主进程与WebRTC进程使用RPC方式进行接口的调用。像Chrome那样,搞多个进程,不同的进程处理不同的事务,可以将程序占用的内存分摊到不同的进程上。并且一个进程崩溃了,也不会影响到主进程,将崩溃的进程重新启动起来就好。

       但多个进程之间需要通信,需要协同控制,控制不好也容易出问题。进程之间如何高效地的传递数据也是个问题,这都需要人力和技术去支撑。但多进程模式是比较稳妥的解决方案之一。

4.4、使用Visual Studio的链接选项,将用户态虚拟内存从2GB扩充到3GB(最终选择这个方法)

       可以在Visual Studio链接选项中打开扩大用户态虚拟内存的选项/L largeAddressAware,如下所示:

这样可以将用户态虚拟内存扩到3GB,这样可以有效缓解内存不够用的问题。

       32位进程只有4GB的虚拟内存,如果将用户态虚拟内存由2GB扩到3GB,内核态的虚拟内存应该会被压缩到1GB,这样会不会导致内核态的代码执行比较慢,导致程序的运行性能下降呢?可能运行性能会有一定的损失,但既然系统运行这种扩充用户态虚拟内存的方式,应该影响不会很大。这个方法简单快速,也不会引入新的bug,短期内最合适,所以最后选的是这个方法!

5、最后

       在Visual Studio中修改链接选项,可以直接将用户态虚拟内存从2GB扩充到3GB,可以有效的规避内存不够用的问题。这种方法简单方便,可以快速地解决问题。但最根本的还是要对内存进行优化,尽量减少对内存的占用,也能提高程序的执行效率。当然优化代码的过程中,也可能会引入这样那样的bug,需要根据时间和工作量评估可行性。


http://www.ppmy.cn/server/33457.html

相关文章

深入学习Redis(1):Redis内存模型

Redis的五个对象类型 字符串&#xff0c;哈希&#xff0c;列表&#xff0c;集合&#xff0c;有序集合 本节有关redis的内存模型 1.估算redis的内存使用情况 目前内存的价格比较的高&#xff0c;如果对于redis的内存使用情况能够进行计算&#xff0c;就可以选用合适的设备进…

linux基本操作

vim的基本操作 正常模式&#xff1a;启动vim后默认处于正常模式。不论位于什么模式&#xff0c;按下Esc建都会进入正常模式。 插入模式&#xff1a;在正常模式中按下i&#xff0c;l&#xff0c;a&#xff0c;A等键&#xff0c;会进入插入模式。现在只用记住按i键会进行插入模…

时间复杂度_空间复杂度

时间复杂度_空间复杂度 1.算法效率 算法效率分析分为两种:第一种是时间效率&#xff0c;第二种是空间效率。 时间效率被称为时间复杂度&#xff0c;而空间效率被称作空间复杂度。时间复杂度主要衡量的是一个算法的运行速度&#xff0c;而空间复杂度主要衡量一个算法所需要的…

超强动画制作软件blender

blender中文手册&#xff1a;Blender 4.1 Manual Blender 是一款集3D建模、渲染、动画、视频编辑、音频处理、游戏设计等多功能于一体的软件。由于其开源性质&#xff0c;它拥有庞大的用户群体和活跃的开发者社区&#xff0c;这使得Blender的功能和性能得到了不断的提升和优化…

Adobe 更新 Firefly Image 3 图像生成模型

一个工具或者模型&#xff0c;对于初次使用的人来说&#xff0c;易用性和超出预期的效果很能吸引使用者&#xff0c;suno和mj在这方面我感觉确实不错&#xff0c;第一次使用感觉很惊艳。 Adobe 更新 Firefly Image 3 图像生成模型&#xff0c;我用了mj的提示词&#xff0c;最后…

C 408—《数据结构》图、查找、排序专题考点(含解析)

目录 Δ前言 六、图 6.1 图的基本概念 6.2 图的存储及基本操作 6.3 图的遍历 6.4 图的应用 七、查找 7.2 顺序查找和折半查找 7.3 树型查找 7.4 B树和B树 7.5 散列表 八、排序 8.2 插入排序 8.3 交换排序 8.4 选择排序 8.5 归并排序和基数排序 8.6 各种内部排序算法的比较及…

OpenHarmony实战开发-如何实现单一手势

点击手势&#xff08;TapGesture&#xff09; TapGesture(value?:{count?:number; fingers?:number})点击手势支持单次点击和多次点击&#xff0c;拥有两个可选参数&#xff1a; count&#xff1a;声明该点击手势识别的连续点击次数。默认值为1&#xff0c;若设置小于1的非…

Python中class类和方法的用法详解及常见坑

在Python中&#xff0c;类&#xff08;class&#xff09;是实现面向对象编程&#xff08;OOP&#xff09;的核心概念。类允许我们定义对象的模板&#xff0c;包括数据属性和方法。本文将深入探讨Python中的类和方法的用法&#xff0c;并结合实际开发中遇到的一些常见问题进行分…