最近在看《程序員的自我修養》。
在171頁,有這樣的一段話:
比如我們拿前面的程序“SectionMappping.elf"做例子,看看各個段的虛擬地址是怎么計算出來的。為什么VMA1的起始地址是0x080B99E8?而不是0x080B89E8或干脆是0x80B9000?
確實蠻有意思的一個問題,當時還沒有反應過來,所以繼續往下看:
VMA0的起始地址是0x08048000,長度是0x709E5,所以他的結束地址是0x080B89E5。而VMA1因為跟VMA0的最后一個虛擬頁面共享一個物理頁面,并且映射兩遍,所以它的虛擬地址應該是0x080B99E5,又因為段必須是4字節的倍數,則向上取整至0x080B99E8。
昨天晚上看到這里,完全沒看懂,反反復復糾結了半天,差點錯過最后一班回去的班車。
先來解釋一下什么是vma。
一個可執行文件被加載到進程的虛擬空間中時,需要一種映射關系,linux中將進程虛擬空間中的一個段叫做虛擬內存區域(Virtual Memory Area)。操作系通過給進程空間劃分出一個個VMA來管理進程的虛擬空間;基本原則是將相同權限屬性的,有相同影像文件的映射成一個VMA。
虛擬內存需要頁映射機制來和物理內存建立映射關系,而每個物理頁的大小為4096字節(32位),所以虛擬內存需要按4096進行對齊。
有了這些準備之后,我們回到書中的問題,VMA0的起始地址為0x08048000,長度為0x709E5,所以結束地址為:
0x08048000 + 0x709E5 = 0x080B89E5
0x080B89E5需要為4的倍數,所以取整為0x080B89E8。這個結果就是書上問題給出的第一個假設答案,但是是錯誤的,這是因為不同的VMA在虛擬內存中需要對應于不同的頁面,也就是段地址對齊。
那么,我們的答案就應該為0x80B9000,就是書上給出的第二個假設答案。但也是錯誤的,因為如果這樣映射,會造成物理內存的浪費,VMA0的結束地址為0x80B89E5,當映射到物理內存上時,從0x80B89E5開始到0x80B9000這部分內存,被浪費掉了。在極端情況下,每個VMA都可能浪費4095字節內存,這是非常劃不來的。
所以UNIX采取了一個很取巧的辦法:通過犧牲虛擬內存地址來換取物理內存的高效利用,讓各個段接壤部分共享一個物理頁面。在本例中,VMA0的最后一個頁面的起始地址為0x80B8000,結束地址為0x80B9000,如下圖:
VMA1從一個新的頁面開始映射,同時空出前面的0x709E5,起始地址為0x80B99E5,這里犧牲了從0x80B89E5到0x80B99E5的虛擬地址空間換取了物理內存的高效利用,這里VMA0的最后一個頁面和VMA1的第一個頁面映射了同一個物理頁面,(這個頁面的權限,只讀,可讀可寫等,應該是通過VMA來維護的,至于操作系統對于page是否有一些權限的控制,就不太清楚了)。
為了保證起始地址為4的倍數,需要將0x80B99E5向上取整為0x80B99E8,就是書上的答案了。
(原文時間2014-2-9)