您好,登錄后才能下訂單哦!
一個典型的計算機系統如下圖所示:
直接讓應用使用硬件可能會導致濫用,并且應用需要處理復雜的硬件細節,容易出錯。所以我們引入了操作系統來管理硬件資源,如下圖所示:
操作系統為了讓應用能更好更簡單地使用硬件資源,對硬件資源做了進一步抽象,如下圖所示:
虛擬存儲器把進程訪問的存儲設備抽象成一個巨大的字節數組,并對每個字節做唯一的地址編碼。它提供了三個重要的功能:
虛擬存儲器在幕后自動地工作,無需應用程序員干涉,既然如此,為什么我們還需要去理解它呢?我想理解它可以帶來以下幾點好處:
進程看到是虛擬地址,但是信息是存在物理內存上的,那么系統是如何用虛擬地址來獲取對應物理內存的字節信息的呢?簡單來說,可以分為三步:
具體過程如下圖:
MMU是如何把虛擬地址翻譯為物理地址的呢?
OS會把物理內存、虛擬內存分為同樣大小的塊(linux默認為4k),并稱之為頁。同時為每個進程分配頁表,頁表是一個頁表條目(PTE)數組,其中每個PTE記錄了虛擬頁與物理頁的映射關系。
一個虛擬地址可以分為兩部分:虛擬頁號×××和虛擬頁偏移量VPO。由于虛擬頁與物理頁是同樣大小,因此虛擬頁偏移量就是物理頁偏移量;虛擬頁號是頁表中PTE的索引,對應的PTE中存儲著物理頁號和有效位(表示頁面是否有對應物理頁),這樣MMU通過查詢PTE就可以找到虛擬頁對應的物理頁,再加上虛擬頁偏移量就可以得到物理地址,如下圖:
如果每個進程只有一個頁表(假設物理頁大小為4k),那么對于32位系統,需要占用4M內存(每個PTE是4字節);對于64位系統(實際只用了48位用來尋址),則需要256G內存,實在是太大了。為了解決這個問題,我們用多級頁表,如下圖:
在多級頁表中,所有級別的頁表大小是一樣的,我們以linux的4級頁表為例,則最少要4個頁表,假設一個頁表4k,總共16k;隨著進程消耗內存的增長,第k級頁表數目隨之線性增長,由于其他級別的頁表數目遠遠小于k級頁表,因此總頁表消耗內存頁頁接近于線性增長。由于進程實際占用內存大小遠小于256T,因此頁表消耗內存遠小于一級頁表。
從上述小結,我們知道每個進程都有一個獨立的虛擬存儲器空間,那么其布局是否有規律呢?我們以linux下的64位進程舉例,見下圖:
linux將用戶虛擬存儲器組織成一些段的集合。一個段就是已分配的虛擬存儲器的連續片。只有存在于段的虛擬存儲器頁是可以被進程訪問的。
#include <stdlib.h>
int main()
{
char *p = (char*)malloc(1);
while(1);
return 0;
}
編譯上述代碼并運行,通過top獲取此進程PID后,我們可以打開/proc/PID/maps文件查看進程的內存布局:
00400000-00401000 r-xp 00000000 fd:01 723899 /home/wld/test/a.out
00600000-00601000 r--p 00000000 fd:01 723899 /home/wld/test/a.out
00601000-00602000 rw-p 00001000 fd:01 723899 /home/wld/test/a.out
0148c000-014ad000 rw-p 00000000 00:00 0 [heap]
7fb917267000-7fb917425000 r-xp 00000000 fd:01 1731435 /lib/x86_64-linux-gnu/libc-2.19.so
7fb917425000-7fb917625000 ---p 001be000 fd:01 1731435 /lib/x86_64-linux-gnu/libc-2.19.so
7fb917625000-7fb917629000 r--p 001be000 fd:01 1731435 /lib/x86_64-linux-gnu/libc-2.19.so
7fb917629000-7fb91762b000 rw-p 001c2000 fd:01 1731435 /lib/x86_64-linux-gnu/libc-2.19.so
7fb91762b000-7fb917630000 rw-p 00000000 00:00 0
7fb917630000-7fb917653000 r-xp 00000000 fd:01 1731443 /lib/x86_64-linux-gnu/ld-2.19.so
7fb917835000-7fb917838000 rw-p 00000000 00:00 0
7fb917850000-7fb917852000 rw-p 00000000 00:00 0
7fb917852000-7fb917853000 r--p 00022000 fd:01 1731443 /lib/x86_64-linux-gnu/ld-2.19.so
7fb917853000-7fb917854000 rw-p 00023000 fd:01 1731443 /lib/x86_64-linux-gnu/ld-2.19.so
7fb917854000-7fb917855000 rw-p 00000000 00:00 0
7ffe8b3e1000-7ffe8b402000 rw-p 00000000 00:00 0 [stack]
7ffe8b449000-7ffe8b44b000 r--p 00000000 00:00 0 [vvar]
7ffe8b44b000-7ffe8b44d000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
上面每一行表示一個段,每個段的有6列,各列含義如下:
假如MMU在嘗試翻譯某個虛擬地址A時,沒有對應的物理地址,則會觸發了一個缺頁異常。這個異常會導致控制轉移到內核的缺頁異常處理程序,處理程序隨后執行如下步驟:
通過執行以下兩種的任意一種命令可查看某個進程的缺頁中斷信息
ps -o majflt,minflt -C program_name
ps -o majflt,minflt -p pid
majflt和minor這兩個數值表示一個進程自啟動以來所發生的缺頁中斷的次數。
其中majflt與minflt的不同是,majflt表示需要讀寫磁盤,可能是內存對應頁面在磁盤中需要load到物理內存中,也可能是此時物理內存不足,需要淘汰部分物理頁面至磁盤中。
linux通過將虛擬內地段與一個磁盤上的文件關聯起來,以初始化這個虛擬存儲器段的內容,這個過程稱之為內存映射(memory mapping)。內存映射有兩種:
###6.1 共享對象
內存映射可以讓我們簡單高效地把程序和數據加載到虛擬存儲器空間中。在實際中,許多進程會映射同一個文件到內存中,比如glic動態庫,如果物理內存中存在多份,那就是極端的浪費。我們可以通過共享對象技術來消除浪費。
對于私有對象,我們可以用寫時拷貝技術來共享物理內存頁。
類unix操作系統下的動態內存分配器有很多,比如ptmalloc(linux默認),tcmalloc(google出品),jemalloc(FreeBSD、NetBSD和firefox默認)。這三種分配器的詳細介紹可以參考http://www.360doc.com/content/13/0915/09/8363527_314549128.shtml。
本文以ptmalloc為例介紹動態內存分配。在linux下os提供兩種動態內存分配brk和mmap。ptmalloc對于申請內存小于128k的采用brk方式,大于128k的采用mmap方式。
對于大內存,malloc會直接調用系統函數mmap分配內存,以物理頁為最小單位做對齊。free會直接調用系統函數munmap釋放內存。
進程有一個指針指向堆的頂部的地址,通過系統函數brk可以改變這個指針的位置,從而改變堆的大小(堆可以擴大也可以收縮)。當已有的堆不能分配內存時,brk會擴大堆來分配動態內存。當頂部的內存被釋放,切釋放內存大于128k,brk就會收縮堆,如下圖:
從上面的堆分配釋放方式,我們知道實際上很多小內存申請后是不會馬上釋放給OS,為了將這些內存重復利用,內存分配器需要由一個算法,下面介紹下ptmalloc是如何處理的。
ptmalloc通過chunk的數據結構來組織每個內存單元。當我們使用malloc分配得到一塊內存的時候,這塊內存就會通過chunk的形式被記錄到glibc上并且管理起來。你可以把它想象成自己寫內存池的時候的一個內存數據結構。chunk的結構可以分為使用中的chunk和空閑的chunk。使用中的chunk和空閑的chunk數據結構基本項同,但是會有一些設計上的小技巧,巧妙的節省了內存。
使用中的chunk:
空閑的chunk結構會復用User data來保存雙向鏈表指針。
ptmalloc一共維護了128bin。每個bins都維護了大小相近的雙向鏈表的chunk。
通過上圖這個bins的列表就能看出,當用戶調用malloc的時候,能很快找到用戶需要分配的內存大小是否在維護的bin上,如果在某一個bin上,就可以通過雙向鏈表去查找合適的chunk內存塊給用戶使用。
造成堆利用率低的主要原因是碎片,當雖然有未使用的內存但不能用來滿足分配請求時,就會發生這種現象。有兩種形式的碎片:
####提問1:請問下面代碼運行后,OS會立即分配1G物理內存嗎?
#include <cstdlib>
int main()
{
char *p = (char*)malloc(1024*1024*1024);
while(1);
return 0;
}
###提問2:請問下面代碼運行后,OS會分配多少物理內存?
#include <cstdlib>
#include <cstring>
int main()
{
const size_t MAX_LEN = 1024*1024*1024;
char *p = (char*)malloc(MAX_LEN);
memset(p, 0, MAX_LEN/2);
while(1);
return 0;
}
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。