Windows 內存管理知識總結

語言: CN / TW / HK

我正在參加「掘金·啟航計劃」

工作中遇到了 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》 《程序員的自我修養》

https://hansimov.gitbook.io/csapp/part2/ch09-virtual-memory

https://docs.microsoft.com/en-us/windows-hardware/drivers/gettingstarted/virtual-address-spaces

https://news.mydrivers.com/1/571/571392.htm

https://www.zhihu.com/question/382484336

https://stackoverflow.com/questions/16264118/how-jvm-stack-heap-and-threads-are-mapped-to-physical-memory-or-operation-syste

https://stackoverflow.com/questions/9560993/how-do-you-disable-aslr-address-space-layout-randomization-on-windows-7-x64

https://docs.microsoft.com/en-us/cpp/build/reference/dynamicbase-use-address-space-layout-randomization?redirectedfrom=MSDN&view=msvc-170

https://stackoverflow.com/questions/2440434/whats-the-difference-between-reserved-and-committed-memory

https://docs.microsoft.com/en-us/archive/blogs/markrussinovich/pushing-the-limits-of-windows-virtual-memory