Windows 内存管理知识总结

4,166 阅读11分钟

我正在参加「掘金·启航计划」

工作中遇到了 32位 windows 程序虚拟内存不足的问题,于是对 Windows 内存相关知识做了调研探索。文章内容总结自《Windows Internal》和 MSDN 文档,具体链接会注在文章最后,供大家参考

预备知识

在了解 Windows 内存知识前,需要弄清「虚拟内存」和「物理内存」的关系

虚拟内存和物理内存的关系

vm_pm.png

首先,了解一下内存分配过程涉及到的一些概念:

  • 进程分配的都是虚拟内存,不能直接使用物理内存
  • 虚拟内存地址通过 MMU (Mememory Management Unit),会被翻译为物理地址,找到对应的物理页
  • 分配连续的虚拟内存,对应的物理内存不一定是连续的,好处是在进程层面不用过多考虑内存碎片化的影响
  • 页命中,物理内存中存在对应的物理页
  • 缺页(paging fault)异常,物理内存中没有找到对应的物理页
  • 交换(swapping)或页面调度(paging),将当前没用的物理页(牺牲页)写入磁盘,将需要用的虚拟内存页映射到物理内存页

总的来说,我们的程序用的都是虚拟内存,操作系统和硬件帮助我们将虚拟地址翻译为真正的物理地址,然后程序才能访问到内存中的数据。

比如图中所示,物理内存一共只有4页。开始时,「进程A」分配了 4 页内存,此时物理内存已经占满。此时如果「进程B」又分配了 2 页内存「VP3」「VP4」,这时会触发缺页异常,操作系统会根据缓存策略将短时间用不到的内存数据交换到磁盘,比如「进程A」的 「VP3」「VP4」被换出到磁盘。然后,「进程B」的「VP3」「VP4」才能被使用。

上面的例子只是帮助大家大致理解内存分配的流程,实际情况会更加复杂,涉及到缓存优化,空间优化等过程,本文不再赘述。

我们还可以观察到,图中的虚拟内存处在不同的状态,「Reserved」「Commited」,这两个状态代表了什么呢?请继续看下节。

Windows中虚拟内存的两种状态 reserved & comitted

reserve_commiting.png

  • reserved 预留,表示预先分配的虚拟内存,但还没有映射到物理内存,在使用时需要先命中物理页
  • commited 已经提交,表示虚拟内存已经映射到了物理内存或已经缓存在磁盘
  • commited pages 也是 private pages,表示不能与其他进程共享

为什么虚拟内存需要 reserved,而不是直接使用 commited?

这是我在 stackoverflow 上找到的我比较认可的回答:

Why would I want to reserve? Why not just get committed memory? There are several reasons I have in mind:

  1. Some application needs a specific address range, say from 0x400000 to 0x600000, but does not need the memory for storing anything. It is used to trap memory access. E.g., if some code accesses such area, it will be caught. (Useful for some reason.)
  2. Some thread needs to store progressively expanding data. And the data needs to be in one contiguous chunk of memory. It is preferred not to commit large physical memory at one go because it is not needed and would be such a waste. The memory can be utilized by some other threads first. The physical memory is committed only on demand.

翻译一下:

  1. 某些应用需要特定的地址空间用于捕获内存捕获监测,一但某些代码开辟了这块空间,就捕获这个事件
  2. 预留连续的空间,后续再使用,比如开辟一条线程时,会先预留 1MB 的空间,而不会直接提交到物理内存

关于「32位程序」和「32位CPU」的 Q&A

Q1. 为什么 8G 甚至 16G 物理内存的笔记本电脑跑 winp32 程序还是会 OOM?

A:win32程序的内存瓶颈在于虚拟内存不足,而不是物理内存

下面做个比喻,解释 32位程序虚拟内存和物理内存的关系是什么。

比如虚拟内存是学校,物理内存是宿舍。

  • 学校盖的大,能招的学生就多,程序能分配的虚拟内存空间就大。

  • 如果学校盖的小,宿舍盖的大,那么宿舍一定会有空位,因为学校就算招满人了,宿舍也住不满(代表了单进程,虚拟内存小于物理内存的情况,不考虑使用 PAE 技术的情况)

  • 如果学校盖的大,宿舍盖的小,宿舍就会住满。那么就需要设定策略,让更需要住宿的同学住进宿舍,不太需要住宿的同学就要搬出宿舍,给需要的同学腾出位置(代表了虚拟内存大于物理内存的情况下,物理内存打满后,需要将不需要的内存数据写入磁盘)

Q2. 为什么32位程序瓶颈是在虚拟内存上?

A: 32位进程,虚拟内存空间是 4GB,Windows系统中,内核空间占用 2GB,用户空间只有 2GB

32位程序\操作系统的指针只能表示 2^32 = 4GB 范围内的地址,所以我们开辟的虚拟内存也只能在 4GB 以内。

一个进程的内存空间布局是什么样子,为什么我们可用的空间只有 2GB 会在介绍 Windows 进程内存布局一节中回答。

Q3. 32位CPU和32位操作系统的关系是什么?

A:32位操作系统的一条指令是32位,32位CPU一个时钟周期正好处理一条32位指令

  • 32位CPU 是不能使用 64 位操作系统的,因为 64位操作系统一条指令是 64位,32位 CPU 无法处理

  • 反过来,64位CPU 可以运行 32位操作系统,但无法发挥出 CPU 的全部能力,有点「大马拉小车」的感觉

Q4. 32位CPU只能使用 4GB 的物理内存么?CPU的寻址能力和CPU的位宽相关么?

A:不是。不相关,CPU的寻址范围和CPU的位宽毫无关系

  • 寻址范围和地址线宽度有关,和 CPU 位宽无关,Intel 32位CPU 早在1995年就支持36位地址线了,也就是 32位CPU 能使用 64GB 的物理内存

  • 为什么能访问更大的内存地址?可以详细了解 PAE(Physical Address Extension) 技术

  • PAE 技术是为了让多个 32位进程累计使用内存的情况下,能使用更多的物理内存(超过4GB)

Windows 内存布局(Windows Process Virtual Space)

用户地址空间(User Address Space Layout)

我们重点关注我们能用到的地址空间是什么样子的,对内核空间感兴趣的同学可以自己查阅其他资料。

下图出自《Windows Internals 6》

wpvs_1.png

我们知道程序需要先被加载到内存中,才能运行

上图描述了 x86(32位)进程的内存布局:

  • 分为了 3GB 的用户空间,和 1GB 的内核空间,但这并不是 Win32 程序的正常布局,而是开启了大地址空间模式的程序(LARGE_ADDRESS_AWARE)
  • 正常的 Win32 程序用户空间只有 2GB,内核空间也占用 2GB
  • 用户空间占用低地址(00000000 ~ 7FFFEFFF),内核空间占用高地址(7FFF000 ~ FFFFFFFF)
  • 用户空间存放了「代码」「全局变量」「线程栈」「DLL」等
  • 内核空间图中详细标明了包含什么,本文不再赘述,感兴趣的同学可以自行了解

wpvs_2.png

上图详细描述了用户空间的布局:

  • 最低地址存放了 .exe
  • 然后是 .dll
  • 然后是 Heap,Heap 中存放的是通过 HeapAlloc 等 API 分配的堆内存
  • 然后是 Thread Stack,存放的是线程栈内存,每开一条新线程就会对应开辟一块栈内存

图中还提到了 ASLR,这是什么,后文会具体介绍。

下面,再来看一张图,此图出自《程序员的自我修养》

vm_space.png

图中描述的用户空间非常「碎片化」,这可能也和 ASLR 相关。如果你要分析应用的虚拟内存布局,不要完全以图中的布局为准,要以自己程序真正运行的情况为准。

user_asl.png

这是书中对地址空间如何计算的一些描述:

  1. 线程栈、进程堆、已装载的镜像文件(exe、dll)的地址是动态计算获得的
  2. 其中 exe dll 需要应用支持 ASLR(随机选择地址)

ASLR 是什么?

下面具体看看,到底什么是 ASLR

what_is_aslr.png

  • ASLR 全称是 Address Space Layout Randomization,可以翻译为随机地址空间
  • 目的是为了防御恶意软件做注入攻击,因为固定地址更容易被攻击者破译
  • 这么做随之而来的缺点是更容易造成「内存碎片化」

如何关闭 ASLR?

close_aslr_1.png

修改链接器高级配置,关闭随机基址(/DYNAMICBASE:NO)

此能力我没有亲自试验过,有需求的同学可以自己尝试

在 Windows 中,Memory Manager 会为每个线程提供两个栈,用户栈(user stack)内核栈(kernel stack)

我们仍然只总结用户栈

user_stack_1.png

  • 线程创建时,默认预留 1MB 虚拟内存

  • 通过编译器指定参数 /STACK:reverse 可以将预留内存大小写入 PE Header 中(修改 stack size)

  • 尽管预留了 1 MB 虚拟内存,但只有 first page 虚拟内存会被提交(真正分配)

user_stack_2.png

  • 64 位系统跑 32 位程序,最大线程数量比 32 位机器跑 32 程序要少
  • 原因是 64 位机器跑 32 位程序,会额外创建 64 位的栈,同样只有 2GB 虚拟内存空间,但每个线程重复消耗了两份内存
  • 实测,64 位栈占用 256 kb 内存,每个线程栈合计占用 1.25 MB

总结,理论上在 64位系统上跑 32位程序,会有额外的开销,本来 32 位程序虚拟内存只有 2GB 可用,运行在 64 位系统上时会更快的暴露这个短板。想了解更多的同学可以去查阅一下 WoW64(windows on windows64)相关内容

分析 Windows 虚拟内存的利器,VMMap

上面介绍了那么多理论,实际上我们该如何分析应用的虚拟内存呢?

官方为我们提供了一款工具 vmmap

内存区域含义

vmmap.png

  • Total::总的分配过的虚拟内存

  • Free:可用的虚拟内存

  • Image:exe dll 占用的虚拟内存

  • Private data:进程私有的堆占用的内存

  • Stack:线程栈占用的虚拟内存

vmmap_help.png

我们也可以打开 vmmap 点 help 进行查看每个区域的具体含义

CLI

vmmap_cli.png

除了 GUI,vmmap 也提供了 CLI 供我们在脚本中使用

如何解决 Win32 程序的虚拟内存瓶颈?

介绍了理论和工具,如何解决实际问题呢?

将 32位程序升级为 64位

虚拟内存在 64位程序上将不会成为瓶颈,但将现有程序改为 64位并不是一件容易的事,具体需要做什么就不再本文赘述了。

缩小冗余的预留空间(Reserved)

  • 减小线程栈分配空间,在上文得出结论,默认情况下,32位程序跑在64位系统上,每条线程需要开辟 1.25MB内存,那我们可以适当减小栈大小。如果是 java 程序可以通过JVM启动参数 Xss 来减少栈空间
  • 减少大的预留的堆空间,比如 java 程序在 JVM 启动的时候就会预留分配 XmX 大小的空间,如果是 1GB,就占用了一半的空间。

扩大进程虚拟内存空间

default_vm_size.png

  • 默认情况,进程虚拟内存大小 2GB

  • 如果 exe 做大地址空间标记且系统启动使用了特殊参数,可以将进程虚拟内存大小升至 3GB

下面讲具体该怎么做

  1. 在编译 exe 的时候需要指定 Linker 参数 LARGE_ADDRESS_AWARE 为 YES
  2. 需要用管理员模式打开 cmd,然后输入命令 bcdedit /set increaseuserva 3072,3072 表示 3GB

如何检查大地址空间模式是否生效?

  1. 确认 windows 系统是否通过 bcdedit 设置了参数,用管理员模式打开 cmd,输入 bcdedit,看列表中是否有 increaseuserva 3072,如果有就进行下一步
  2. 使用 dumpbin /headers 查看 exe 是否开启了大地址空间模式

check_large_adress.png

参考

《Windows Internal 6》《Windows Internal 7》 《程序员的自我修养》

hansimov.gitbook.io/csapp/part2…

docs.microsoft.com/en-us/windo…

news.mydrivers.com/1/571/57139…

www.zhihu.com/question/38…

stackoverflow.com/questions/1…

stackoverflow.com/questions/9…

docs.microsoft.com/en-us/cpp/b…

stackoverflow.com/questions/2…

docs.microsoft.com/en-us/archi…