博文:https://chanjarster.github.io...git
原文:What every programmer should know about memory, Part 3: Virtual Memorygithub
4 Virtual Memory
虛擬內存(virtual memory)是處理器的一個子系統,它給每一個進程提供虛擬地址空間(virtual address space)。這讓每一個進程覺得本身在系統中是獨自一人。數據庫
wiki詞條:數組
虛擬內存的做用在於爲進程提供「看上去」連續的地址空間,這麼作的好處在於程序不須要處理內存碎片的問題了。
虛擬地址空間由CPU的Memory Management Unit(MMU)實現,操做系統必須填寫頁表數據結構(page table data structures,見wiki詞條),大多數CPU本身完成餘下的工做。安全
把虛擬地址(virtual address)做爲輸入交給MMU作翻譯。在32位系統中虛擬地址是32位的,在64位系統中是64位的。數據結構
4.1 Simplest Address Translation
MMU能夠逐頁(page)的將虛擬地址翻譯成物理地址的,和cache line同樣,虛擬地址被分割成多個部分,這些部分則被索引到不一樣的表(table)裏,這些表用來構造最終的物理地址。最簡單的模型則只擁有一個級別的表(only one level of tables)。架構
![](http://static.javashuo.com/static/loading.gif)
Figure 4.1: 1-Level Address Translationide
虛擬地址的結構:佈局
- 虛擬地址的頭部被用來在一個頁目錄(Page Directory)中選擇條目(entry),
- 頁目錄中存儲的是條目(entry),每一個條目可由操做系統單獨設置。
- 條目決定了物理內存頁的地址,即頁的物理地址
- 虛擬地址的尾部是頁內的偏移量
- 因此頁的物理地址+偏移量=物理地址
- 頁目錄的條目還包含一些輔助信息,好比訪問權限
頁目錄的存儲:post
- 頁目錄是存在內存裏的,
- 操縱系統爲其分配一段連續的物理內存空間,並將基地址(base address)存在一個特殊的寄存器裏
- 而條目在目錄裏就是一個數組(記住這是數組,這對於理解下面多級目錄,多級索引很重要)
先弄個速算表,下面會用得着:
- 29=512
- 210=512 * 2=1024=1K
- 220=1024 * 1024=1MB
拿x86系統,4MB頁舉例:
- 虛擬地址的偏移量部分佔用22位(能夠覆蓋4MB的空間)
- 目錄部分則還剩10位,便可以存放1024個條目
- 每一個條目存了10位的物理頁內存的基地址
- 10位+22位=32位,造成了完整的物理內存地址
4.2 Multi-Level Page Tables
多級頁表(page table),注意原文寫到這裏用頁表(page table)而不是頁目錄(page directory),這兩個其實是一個東西。
上面的例子拿4MB頁來舉例的,不過4MB頁表不常見,這是由於操做系統執行的不少操做是按照頁來對齊的,意思是每一個頁的基地址之間都差4MB的倍數,就算你要用1k內存也要申請了一個4MB的頁,這形成了大量的浪費。
真實世界裏32位機器大多用4kB頁,一樣多見於64位機器。
爲啥4kB頁,單級頁表不行:
- 虛擬地址偏移量佔12位
- 虛擬地址頁目錄部分佔20位(64位機器就是52位)
- 頁表條目數=220,就算每一個條目只佔4 bytes(32位)那整個頁表頁要佔4MB
- 而後每一個進程會擁有本身的頁表,那麼大量的物理內存就會被用在頁表上。
- 實際上不光是物理內存用量太大的問題,因頁表就是一個數組,須要連續的內存空間,到時候很難分配。
解決辦法是使用多級頁表。它們可以表明一個稀疏的巨大的頁表,能夠作到對沒有被使用的區域(原文沒有講區域是啥)不須要分配內存。這種形式跟爲緊湊,能夠爲許多進程提供頁表,同時又不對性能產生太大影響。
![](http://static.javashuo.com/static/loading.gif)
Figure 4.2: 4-Level Address Translation
上面是一個4級頁表:
- 虛擬地址被分割成5個部分,其中4個部分是不一樣頁表的索引
- 第4級頁表經過CPU裏的一個特殊目的的register來引用
- 第4級-第2級的頁表的內容是對下一級頁表引用(我以爲應該就是物理內存地址,由於前面講過頁表存在物理內存中的)
- 第1級頁表存儲的物理地址的一部分(應該就是去掉偏移量的那一部分)和輔助數據,好比訪問權限
- 因此整個造成了一個頁表樹(page table tree),稀疏又緊湊(sparse and compact)
獲得物理地址的步驟,Page tree walking:
- 先從register中獲得第4級頁表的地址,
-
拿到第4級頁表
- 拿虛擬地址中Level 4 Index取得頁表中的條目,這個條目裏存的是第3級頁表的地址
-
拿到第3級頁表
- 拿虛擬地址中Level 3 Index取得頁表中的條目,這個條目裏存的是第2級頁表的地址
- 如此反覆直到拿到第1級頁表裏的條目,這個條目裏存的是物理地址的高位部分
- 結合虛擬地址中的偏移量,獲得最終的物理地址
- Page tree walking在x8六、x86-64處理器裏是發生在硬件層面的
Page table tree尺寸對性能的影響:
- 每一個進程可能須要本身的page table tree,幾個進程共享樹的一部分是存在的,但這只是特例。
- 若是頁表樹所需內存越小,那就越有利於性能和擴展性(performance and scalability)
- 理想狀況下,把使用的內存在虛擬地址空間裏緊密的放在一塊兒,就可以讓page table tree佔用的空間小(單獨看這句沒有辦法明白,結合後面的內容看
- 舉例,4kB/頁,512條目/頁表,1頁表/每級,那麼能夠尋址2MB連續的地址空間(512*4kB=2MB)
- 舉例,4kB/頁,512條目/頁表,4-2級只有1個頁表,1級有512個頁表,那麼能夠尋址1GB連續的地址空間(512 512 4KB=1G)
Page table tree佈局:
- 假設全部內存都可以連續的被分配太過簡單了
- 好比,出於靈活性的考慮(flexibility),stack和heap分佔地址空間的兩端,因此極有可能有2個2級頁表,每一個二級頁表有一個1級頁表。
- 顯示中比上面這個更復雜,處於安全性考慮,不一樣的可執行部分(code、data、heap、stack、DSOs又稱共享庫)是被影射到隨機地址上的。因此進程所使用的不一樣內存區域是遍及整個虛擬地址空間的。因此一個進程不可能只有一兩個2級3級頁表的。
我的總結,前面講的對於多少連續的尋址空間,各級別頁表須要多少個是這麼計算的:
- 首先得知道前提,對於4-2級頁表,在同一頁表內,不一樣頁表條目不會指向同一個下一級頁表
- 對於1級頁表,不一樣頁表條目不會指向相同的物理地址(準確的說是物理地址去掉offset的部分)
- 對於4-2級頁表,每一個頁表條目指向一個下級頁表,即上級頁表條目數目=下級頁表數
- 假設如今是32位系統,每一個頁表至多保存29=512個頁表項
下面舉連續的2MB尋址空間(頁大小爲4kB):
- 2MB=210 210 2=221 bytes
- 因此須要:2MB / 4kB = 221 / 212 = 29個1級頁表條目
- 因此須要:29 / 29=1個一級頁表=1個2級頁表條目
- 因此前面說,4kB/頁,512條目/頁表,1頁表/每級,那麼能夠尋址2MB連續的地址空間
下面舉例連續的1GB尋址空間(頁大小爲4kB):
- 1GB=210 210 210=230 bytes
- 因此須要1級頁表條目:1GB / 4kB = 230 / 212=218個1級頁表條目
- 因此須要:218 / 29=29個1級頁表=29個2級頁表條目
- 因此須要:29 / 29=1個2級頁表
- 因此前面說,4kB/頁,512條目/頁表,4-2級只有1個頁表,1級有512個頁表,那麼能夠尋址1GB連續的地址空間(512 512 4KB=1G)
同理若是是連續的2GB尋址空間(頁大小爲4kB):
- 1GB=210 210 210 * 2=231 bytes
- 因此須要:1GB / 4kB = 231 / 212=219個1級頁表條目
- 因此須要:219 / 29=210個1級頁表=210個2級頁表條目
- 因此須要:210 / 29=2個二級頁表=2個3級頁表條目
4.3 Optimizing Page Table Access
- 全部頁表是存在main memory中的,操做系統負責構建和更新頁表
- 建立進程或更新頁表時CPU會收到通知
- 頁表被用來每一次解析虛擬地址到物理地址的工做,採用的方式是page tree walking
- 當解析虛擬地址的時候,每級都至少有一個頁表在page tree walking中被使用
- 因此每次解析虛擬地址要訪問4次內存,這很慢
TLB:
- 現代CPU將虛擬地址的計算結果保存在一個叫作TLB(Tranlsation Look-Aside Buffer)的cache中。
- TLB是一個很小的cache,並且速度極快
- 現代CPU提供多級TLB,級別越高尺寸越大同時越慢。也分爲數據和指令兩種,ITLB和DTLB。高層級TLB好比2LTLB一般是統一的。(和前一篇文章講的cache結構相似)
- 由於虛擬地址的offset不參與page tree walking,因此使用其他部分做爲cache的tag
- 經過軟件或硬件prefetch code/data會隱式的prefetch TLB條目,若是地址是在另外一個page上時
4.3.1 Caveats Of Using A TLB
講了幾種優化TLB cache flush的手段,不過沒有講現代CPU使用的是哪種。
我的認爲這段不用太仔細讀,只須要知道存在一種手段能夠最少範圍的flush TLB cache entry就好了。
4.3.2 Influencing TLB Performance
使用大頁:
- 頁尺寸越大,則頁表須要存儲的條目就越少,則須要作的虛擬地址->物理地址翻譯工做就越少,則須要TLB的條目就越少。有些x86/x86-64支持4kB、2MB、4MB的頁尺寸。
- 不過大頁存在問題,給大頁使用的內存區域必須是連續的。
- 若是物理內存的管理基本單位和虛擬內存頁同樣大的話,浪費的內存就會變多(由於內存申請是以頁爲單位的,無論你用多少,都會佔用1頁)。
- 2MB的頁對於x86-64系統來講也仍是太大了,若是要實現則必須用幾個小頁組成大頁來實現。若是小頁是4kB,那麼就意味着要在物理內存中連續地分配512個小頁。要作到這個比較困難,並且系統運行一段時間物理內存就會變得碎片化。
- Linux系統在操做系統啓動時遇險分配了一塊內存區域存放大頁(
hugetlbs
文件系統),固定數量的物理頁被保留給虛擬大頁使用。
- 因此大頁適合在如下場景:性能優先、資源充足、不怕配置繁瑣,好比數據庫應用。
提升虛擬頁最小尺寸(前面講的大頁是可選的)也會產生問題:
- 內存影射操做(好比加載應用程序)必須可以適配頁尺寸。比頁尺寸更小的映射是不容許的。
- 一個可執行程序的各個部分,在大多數架構中,的關係是固定的。
- 若是頁尺寸變得太大,以致於超出了可執行程序所適配的大小,那麼就沒法加載了。看下圖:
$ eu-readelf -l /bin/ls
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
...
LOAD 0x000000 0x0000000000400000 0x0000000000400000 0x0132ac 0x0132ac R E 0x200000
LOAD 0x0132b0 0x00000000006132b0 0x00000000006132b0 0x001a71 0x001a71 RW 0x200000
...
Figure 4.3: ELF Program Header Indicating Alignment Requirements
這是一個x86-64可執行二進制的頭,裏面規定了內存對齊單位是0x200000 = 2,097,152 = 2MB,若是頁尺寸比這個大就不行了。
另外一個使用大頁的影響是減小page table tree的層級,由於offset變大了,那麼剩下的留給頁表的部分就變少了,那麼page tree walking就更快了,那麼TLB missing所要產生的工做就變少了。
下面這段沒有看懂:
Beyond using large page sizes, it is possible to reduce the number of TLB entries needed by moving data which is used at the same time to fewer pages. This is similar to some optimizations for cache use we talked about above. Only now the alignment required is large. Given that the number of TLB entries is quite small this can be an important optimization.
4.4 Impact Of Virtualization
大體意思是現代虛擬化技術可以消解大部分因虛擬化致使的TLB性能損失,可是這個開銷不會徹底消失。