漫遊計算機系統之虛擬存儲器

漫遊計算機系統之虛擬存儲器

1. 背景

一個典型的計算機系統以下圖所示:
漫遊計算機系統之虛擬存儲器
直接讓應用使用硬件可能會致使濫用,而且應用須要處理複雜的硬件細節,容易出錯。因此咱們引入了操做系統來管理硬件資源,以下圖所示:
漫遊計算機系統之虛擬存儲器
操做系統爲了讓應用能更好更簡單地使用硬件資源,對硬件資源作了進一步抽象,以下圖所示:
漫遊計算機系統之虛擬存儲器html

2. 虛擬存儲器

虛擬存儲器把進程訪問的存儲設備抽象成一個巨大的字節數組,並對每一個字節作惟一的地址編碼。它提供了三個重要的功能:linux

  1. 將主存看作存儲在磁盤上的地址空間的高速緩存,從而提升了主存使用效率。
  2. 爲每一個進程提供一致的地址空間,簡化存儲器管理。
  3. 保護每一個進程的地址空間不被其餘進程破壞。

虛擬存儲器在幕後自動地工做,無需應用程序員干涉,既然如此,爲何咱們還須要去理解它呢?我想理解它能夠帶來如下幾點好處:c++

  1. 虛擬存儲器是計算機系統的核心。它涉及計算機系統的全部層面(硬件異常、彙編器、連接器、加載器、共享對象、文件、進程),理解它將幫助咱們更好地理解計算機系統是怎麼工做的。
  2. 虛擬存儲器的功能很是強大。它賦予應用程序強大的功能,好比加載一個文件到存儲器中,不須要任何顯示拷貝。
  3. 虛擬存儲器很是危險。對於編寫像c/c++程序,一個錯誤指針就可讓程序當即崩潰。理解它可讓咱們更好地避免錯誤;或者當錯誤發生時,更好地定位問題所在。

3. 虛擬尋址

進程看到是虛擬地址,可是信息是存在物理內存上的,那麼系統是如何用虛擬地址來獲取對應物理內存的字節信息的呢?簡單來講,能夠分爲三步:程序員

  1. CPU會把虛擬地址發送到MMU(Memory Management Unit)
  2. MMU把虛擬地址翻譯爲物理地址後傳送給主存
  3. 主存把物理地址對應的字節傳送給CPU

具體過程以下圖:
漫遊計算機系統之虛擬存儲器算法

3.1. 頁表

MMU是如何把虛擬地址翻譯爲物理地址的呢?
OS會把物理內存、虛擬內存分爲一樣大小的塊(linux默認爲4k),並稱之爲頁。同時爲每一個進程分配頁表,頁表是一個頁表條目(PTE)數組,其中每一個PTE記錄了虛擬頁與物理頁的映射關係。
漫遊計算機系統之虛擬存儲器
一個虛擬地址能夠分爲兩部分:虛擬頁號×××和虛擬頁偏移量VPO。因爲虛擬頁與物理頁是一樣大小,所以虛擬頁偏移量就是物理頁偏移量;虛擬頁號是頁表中PTE的索引,對應的PTE中存儲着物理頁號和有效位(表示頁面是否有對應物理頁),這樣MMU經過查詢PTE就能夠找到虛擬頁對應的物理頁,再加上虛擬頁偏移量就能夠獲得物理地址,以下圖:數組

漫遊計算機系統之虛擬存儲器

3.2. 多級頁表

若是每一個進程只有一個頁表(假設物理頁大小爲4k),那麼對於32位系統,須要佔用4M內存(每一個PTE是4字節);對於64位系統(實際只用了48位用來尋址),則須要256G內存,實在是太大了。爲了解決這個問題,咱們用多級頁表,以下圖:
漫遊計算機系統之虛擬存儲器
在多級頁表中,全部級別的頁表大小是同樣的,咱們以linux的4級頁表爲例,則最少要4個頁表,假設一個頁表4k,總共16k;隨着進程消耗內存的增加,第k級頁表數目隨之線性增加,因爲其餘級別的頁表數目遠遠小於k級頁表,所以總頁表消耗內存頁頁接近於線性增加。因爲進程實際佔用內存大小遠小於256T,所以頁表消耗內存遠小於一級頁表。緩存

4. 進程內存佈局

從上述小結,咱們知道每一個進程都有一個獨立的虛擬存儲器空間,那麼其佈局是否有規律呢?咱們以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列,各列含義以下:多線程

  1. 此段虛擬地址空間起始地址-結束地址。
  2. 此段虛擬地址空間的屬性。每種屬性用一個字段表示,r表示可讀,w表示可寫,x表示可執行,p和s共用一個字段,互斥關係,p表示私有段,s表示共享段,若是沒有相應權限,則用’-’代替。
  3. 對有名映射,表示此段虛擬存儲器起始地址在文件中以頁爲單位的偏移。對匿名映射,它等於0或者vm_start/PAGE_SIZE。
  4. 映射文件所屬設備號。對匿名映射來講,由於沒有文件在磁盤上,因此沒有設備號,始終爲00:00。對有名映射來講,是映射的文件所在設備的設備號
  5. 映射文件所屬節點號。對匿名映射來講,由於沒有文件在磁盤上,因此沒有節點號,始終爲00:00。對有名映射來講,是映射的文件的節點號
  6. 對有名來講,是映射的文件名。對匿名映射來講,是此段虛擬存儲器在進程中的角色。[stack]表示在進程中做爲棧使用,[heap]表示堆。其他狀況則無顯示

5. 缺頁異常處理

假如MMU在嘗試翻譯某個虛擬地址A時,沒有對應的物理地址,則會觸發了一個缺頁異常。這個異常會致使控制轉移到內核的缺頁異常處理程序,處理程序隨後執行以下步驟:

  1. 虛擬地址是合法的嗎?即虛擬地址是否在已分配的段的地址範圍內。若是找不到就會觸發段錯誤。
  2. 試圖進行的虛擬地址訪問是合法的嗎?即權限是否符合所在段的權限。若是沒有權限就會觸發保護異常。
  3. 分配物理頁,更新頁表。缺頁處理程序返回時,CPU會從新執行引發缺頁的指令。

經過執行如下兩種的任意一種命令可查看某個進程的缺頁中斷信息
ps -o majflt,minflt -C program_name
ps -o majflt,minflt -p pid
majflt和minor這兩個數值表示一個進程自啓動以來所發生的缺頁中斷的次數。
其中majflt與minflt的不一樣是,majflt表示須要讀寫磁盤,多是內存對應頁面在磁盤中須要load到物理內存中,也多是此時物理內存不足,須要淘汰部分物理頁面至磁盤中。

6. 內存映射

linux經過將虛擬內地段與一個磁盤上的文件關聯起來,以初始化這個虛擬存儲器段的內容,這個過程稱之爲內存映射(memory mapping)。內存映射有兩種:

  1. 有名文件映射:一個段能夠映射到一個普通磁盤文件的連續部分,例如一個可執行文件。
  2. 匿名文件映射:一個段也能夠映射到一個匿名文件,匿名文件由內核建立,包含的是全二進制零。

###6.1 共享對象
內存映射可讓咱們簡單高效地把程序和數據加載到虛擬存儲器空間中。在實際中,許多進程會映射同一個文件到內存中,好比glic動態庫,若是物理內存中存在多份,那就是極端的浪費。咱們能夠經過共享對象技術來消除浪費。
漫遊計算機系統之虛擬存儲器
對於私有對象,咱們能夠用寫時拷貝技術來共享物理內存頁。
漫遊計算機系統之虛擬存儲器

6.2 思考題

  1. 可否經過動態庫的全局變量在進程間傳遞信息?
  2. 進程A使用動態庫S,更新S版本,再啓動使用S的進程B後,進程A是否還能正常訪問S的函數?

7. 動態內存分配

類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方式。

7.1. mmap

對於大內存,malloc會直接調用系統函數mmap分配內存,以物理頁爲最小單位作對齊。free會直接調用系統函數munmap釋放內存。

7.2. brk

進程有一個指針指向堆的頂部的地址,經過系統函數brk能夠改變這個指針的位置,從而改變堆的大小(堆能夠擴大也能夠收縮)。當已有的堆不能分配內存時,brk會擴大堆來分配動態內存。當頂部的內存被釋放,切釋放內存大於128k,brk就會收縮堆,以下圖:
漫遊計算機系統之虛擬存儲器
從上面的堆分配釋放方式,咱們知道實際上不少小內存申請後是不會立刻釋放給OS,爲了將這些內存重複利用,內存分配器須要由一個算法,下面介紹下ptmalloc是如何處理的。

7.3. ptmalloc

ptmalloc經過chunk的數據結構來組織每一個內存單元。當咱們使用malloc分配獲得一塊內存的時候,這塊內存就會經過chunk的形式被記錄到glibc上而且管理起來。你能夠把它想象成本身寫內存池的時候的一個內存數據結構。chunk的結構能夠分爲使用中的chunk和空閒的chunk。使用中的chunk和空閒的chunk數據結構基本項同,可是會有一些設計上的小技巧,巧妙的節省了內存。
使用中的chunk:
漫遊計算機系統之虛擬存儲器

  1. chunk指針指向chunk開始的地址;mem指針指向用戶內存塊開始的地址。
  2. p=0時,表示前一個chunk爲空閒,prev_size纔有效
  3. p=1時,表示前一個chunk正在使用,prev_size無效 p主要用於內存塊的合併操做
  4. ptmalloc 分配的第一個塊老是將p設爲1, 以防止程序引用到不存在的區域
  5. M=1 爲mmap映射區域分配;M=0爲heap區域分配
  6. A=1 爲非主分區分配;A=0 爲主分區分配

空閒的chunk結構會複用User data來保存雙向鏈表指針。

漫遊計算機系統之虛擬存儲器
ptmalloc一共維護了128bin。每一個bins都維護了大小相近的雙向鏈表的chunk。
經過上圖這個bins的列表就能看出,當用戶調用malloc的時候,能很快找到用戶須要分配的內存大小是否在維護的bin上,若是在某一個bin上,就能夠經過雙向鏈表去查找合適的chunk內存塊給用戶使用。

  1. fast bins。fast bins是bins的高速緩衝區,大約有10個定長隊列。當用戶釋放一塊不大於max_fast(默認值64)的chunk(通常小內存)的時候,會默認會被放到fast bins上。當用戶下次須要申請內存的時候首先會到fast bins上尋找是否有合適的chunk,而後纔會到bins上空閒的chunk。ptmalloc會遍歷fast bin,看是否有合適的chunk須要合併到bins上。
  2. unsorted bin。是bins的一個緩衝區。當用戶釋放的內存大於max_fast或者fast bins合併後的chunk都會進入unsorted bin上。當用戶malloc的時候,先會到unsorted bin上查找是否有合適的bin,若是沒有合適的bin,ptmalloc會將unsorted bin上的chunk放入bins上,而後到bins上查找合適的空閒chunk。
  3. small bins和large bins。small bins和large bins是真正用來放置chunk雙向鏈表的。每一個bin之間相差8個字節,而且經過上面的這個列表,能夠快速定位到合適大小的空閒chunk。前64個爲small bins,定長;後64個爲large bins,非定長。
  4. Top chunk。並非全部的chunk都會被放到bins上。top chunk至關於分配區的頂部空閒內存,當bins上都不能知足內存分配要求的時候,就會來top chunk上分配。
  5. mmaped chunk。當分配的內存很是大(大於分配閥值,默認128K)的時候,須要被mmap映射,則會放到mmaped chunk上,當釋放mmaped chunk上的內存的時候會直接交還給操做系統。

7.3.1. 內存分配malloc流程

  1. 獲取分配區的鎖,防止多線程衝突。
  2. 計算出須要分配的內存的chunk實際大小。
  3. 判斷chunk的大小,若是小於max_fast(64b),則取fast bins上去查詢是否有適合的chunk,若是有則分配結束。
  4. chunk大小是否小於512B,若是是,則從small bins上去查找chunk,若是有合適的,則分配結束。
  5. 繼續從 unsorted bins上查找。若是unsorted bins上只有一個chunk而且大於待分配的chunk,則進行切割,而且剩餘的chunk繼續扔回unsorted bins;若是unsorted bins上有大小和待分配chunk相等的,則返回,並從unsorted bins刪除;若是unsorted bins中的某一chunk大小 屬於small bins的範圍,則放入small bins的頭部;若是unsorted bins中的某一chunk大小 屬於large bins的範圍,則找到合適的位置放入。
  6. 從large bins中查找,找到鏈表頭後,反向遍歷此鏈表,直到找到第一個大小 大於待分配的chunk,而後進行切割,若是有餘下的,則放入unsorted bin中去,分配則結束。
  7. 若是搜索fast bins和bins都沒有找到合適的chunk,那麼就須要操做top chunk來進行分配了(top chunk至關於分配區的剩餘內存空間)。判斷top chunk大小是否知足所需chunk的大小,若是是,則從top chunk中分出一塊來。
  8. 若是top chunk也不能知足需求,則須要擴大top chunk。主分區上,若是分配的內存小於分配閥值(默認128k),則直接使用brk()分配一塊內存;若是分配的內存大於分配閥值,則須要mmap來分配;非主分區上,則直接使用mmap來分配一塊內存。經過mmap分配的內存,就會放入mmap chunk上,mmap chunk上的內存會直接回收給操做系統。

7.3.2. 內存釋放free流程

  1. 獲取分配區的鎖,保證線程安全。
  2. 若是free的是空指針,則返回,什麼都不作。
  3. 判斷當前chunk是不是mmap映射區域映射的內存,若是是,則直接munmap()釋放這塊內存。前面的已使用chunk的數據結構中,咱們能夠看到有M來標識是不是mmap映射的內存。
  4. 判斷chunk是否與top chunk相鄰,若是相鄰,則直接和top chunk合併(和top chunk相鄰至關於和分配區中的空閒內存塊相鄰)。轉到步驟8
  5. 若是chunk的大小大於max_fast(64b),則放入unsorted bin,而且檢查是否有合併,有合併狀況而且和top chunk相鄰,則轉到步驟8;沒有合併狀況則free。
  6. 若是chunk的大小小於 max_fast(64b),則直接放入fast bin,fast bin並無改變chunk的狀態。沒有合併狀況,則free;有合併狀況,轉到步驟7
  7. 在fast bin,若是當前chunk的下一個chunk也是空閒的,則將這兩個chunk合併,放入unsorted bin上面。合併後的大小若是大於64KB,會觸發進行fast bins的合併操做,fast bins中的chunk將被遍歷,並與相鄰的空閒chunk進行合併,合併後的chunk會被放到unsorted bin中,fast bin會變爲空。合併後的chunk和topchunk相鄰,則會合併到topchunk中。轉到步驟8
  8. 判斷top chunk的大小是否大於mmap收縮閾值(默認爲128KB),若是是的話,對於主分配區,則會試圖歸還top chunk中的一部分給操做系統。free結束。

7.4. 內存碎片

形成堆利用率低的主要緣由是碎片,當雖然有未使用的內存但不能用來知足分配請求時,就會發生這種現象。有兩種形式的碎片:

  1. 內部碎片:已分配塊比有效載荷大。每每在實現內存池或者本身管理內存時會存在內部碎片。
  2. 外部碎片:空閒內存合起來能夠知足分配要求,可是沒有一個單獨的空閒塊足夠大能夠知足分配請求。目前一些好的第三方分配器,如tcmalloc、jemalloc能夠很好地解決外部碎片問題。

7.5. 思考題

####提問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;
}
相關文章
相關標籤/搜索