一個典型的計算機系統以下圖所示:
直接讓應用使用硬件可能會致使濫用,而且應用須要處理複雜的硬件細節,容易出錯。因此咱們引入了操做系統來管理硬件資源,以下圖所示:
操做系統爲了讓應用能更好更簡單地使用硬件資源,對硬件資源作了進一步抽象,以下圖所示:html
虛擬存儲器把進程訪問的存儲設備抽象成一個巨大的字節數組,並對每一個字節作惟一的地址編碼。它提供了三個重要的功能:linux
虛擬存儲器在幕後自動地工做,無需應用程序員干涉,既然如此,爲何咱們還須要去理解它呢?我想理解它能夠帶來如下幾點好處:c++
進程看到是虛擬地址,可是信息是存在物理內存上的,那麼系統是如何用虛擬地址來獲取對應物理內存的字節信息的呢?簡單來講,能夠分爲三步:程序員
具體過程以下圖:算法
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; }