1. 程式人生 > >深入理解計算機系統之虛擬存儲器

深入理解計算機系統之虛擬存儲器

fragment 策略 動態鏈接 字段 索引 ~~ cti 錯誤 個數

http://blog.csdn.net/al_xin/article/details/38590931

進程提供給應用程序的關鍵抽象:

  • 一個獨立的邏輯控制流,它提供一個假象,好像我們的程序獨占地使用處理器
  • 一個私有的地址空間,它提供一個假象,好像我們的程序獨占地使用存儲器系統.

虛擬存儲器

虛擬存儲器是硬件異常、硬件地址翻譯、主存、磁盤文件和內核軟件的完美交互,它為每個進程提供了一個大的、一致的和私有的地址空間。通過一個很清晰的機制,虛擬存儲器提供了三個重要的能力:

(1)它將主存看成是一個存儲在磁盤上的地址空間的高速緩存,在主存中只保存活動區域,並根據需要在磁盤和主存之間來回傳送數據,

通過這種方式,它高效地使用了主存。

(2)它為每個進程提供了一致的地址空間,從而簡化了存儲器管理。

(3)它保護了每個進程的地址空間不被其他進程破壞

物理和虛擬尋址

物理尋址

計算機系統的主存被組織成一個由M個連續的字節大小的單元組成的數組。每字節都有一個唯一的物理地址(Physical Address,PA)。第一個字節的地址為0,接下來的字節的地址為1,再下一個為2,依此類推。給定這種簡單的結構,CPU訪問存儲器的最自然的方式就是使用物理地址,我們把這種方式稱為物理尋址。

虛擬尋址

使用虛擬尋址時,CPU通過生成一個虛擬地址(Virtual Address,VA)來訪問主存,這個虛擬地址在被送到存儲器之前先轉換成適當的物理地址。將一個虛擬地址轉換為物理地址的任務叫做地址翻譯(address translation)。就像異常處理一樣,地址翻譯需要CPU硬件和操作系統之間的緊密合作。CPU芯片上叫做存儲器管理單元(Memory Management Unit,MMU)的專用硬件,利用存放在主存中的查詢表來動態翻譯虛擬地址,該表的內容是由操作系統管理。

地址空間

地址空間(adress space)是一個非整數地址的有序集合:{0,1,2,...}

如果地址空間中的整數是連續的,那麽我們說它是一個線性地址空間(linear address space)。在一個帶虛擬存儲器的系統中,CPU從一個有N = 2 ^ n個地址空間中生成虛擬地址,這個地址空間稱為虛擬地址空間(virtual address space):{0,1,2,3,...,N-1}

一個地址空間的大小是由表示最大地址所需要的倍數來描述的。例如,一個包含N=2^n個地址的虛擬地址空間叫做一個n位地址空間。現在系統典型地支持32位或者64位虛擬地址空間是。

一個系統還有一個物理地址空間(physical addresss space),它與系統中物理存儲器的M字節相對應:{0,1,2,...M-1}

M不要求是2的冪,但是為了簡化討論,我們假設M = 2 ^ m。

地址空間的概念是很重要的,因為它清楚地區分了數據對象(字節)和它們的屬性(地址)。一旦認識到了這種區別,那麽我們就可以將其推廣,允許每個數據對象有多個獨立的地址,其中每個地址都選自一個不同的地址空間(不連續的意思嗎?)。這就是虛擬存儲器的基本思想。主存中每個字節都有一個選自虛擬地址空間的虛擬地址和一個選自物理地址空間的物理地址。(這段沒怎麽看懂~~)

虛擬存儲器作為緩存的工具

概念上而言,虛擬存儲器(VM)被組織為一個由存放在磁盤上N個連續的字節大小的單元組成的數組。每個字節都有一個唯一的虛擬地址,這個唯一的虛擬地址是作為到數組的索引的。磁盤上的數組的內容被緩存在主存中。和存儲器層次結構中其他緩存一樣,磁盤(較低層)上的數據被分割成塊,這些塊作為磁盤和主存(較高層)之間的傳輸單元。VM系統通過將虛擬存儲器分割稱為虛擬頁(Vitual Page,VP)的大小固定的塊來處理這個問題。每個虛擬頁的大小為P = 2 ^ n字節。類似地,物理存儲器被分割為物理頁(Physical Page,PP),大小也為P字節(物理頁也稱為頁幀(page frame))。

在任意時刻,虛擬頁面的集合都分為三個不相交的子集:

  • 未分配的:VM系統還未分配(或者創建)的頁。未分配的塊沒有任何數據和它們相關聯,因此也就不占用任何磁盤空間。(沒有調用malloc或者mmap的)
  • 緩存的:當前緩存在物理存儲中的已分配頁。(已經調用malloc和mmap的,在程序中正在引用的)
  • 未緩存的:沒有緩存在物理存儲器中的已分配頁。(已經調用malloc和mmap的,在程序中還沒有被引用的)
技術分享

頁表

同任何緩存一樣,虛擬存儲器系統必須有某種方法來判定一個虛擬頁是否存放在DRAM中的某個地方。如果是,系統還必須確定這個虛擬頁存放在哪個物理頁中。如果不命中,系統必須判斷這個虛擬頁存放在磁盤的哪個位置,在物理存儲器中選擇一個犧牲頁,並將虛擬頁從磁盤拷貝到DRAM中,替換這個犧牲頁。

這些功能是由許多軟硬件聯合提供的,包括操作系統軟件,MMU(存儲器管理單元)中地址翻譯硬件和一個存放在物理存儲器中叫做頁表(page table)的數據結構,頁表將虛擬頁映射到物理頁。頁表就是一個頁表條目(Page Table Entry,PTE)的數組。

技術分享

Linux虛擬存儲器系統

linux為每個進程維持了一個單獨的虛擬地址空間

技術分享

內核虛擬存儲器包含內核中的代碼和數據結構。內核虛擬存儲器的某些區域被映射到所有進程共享的物理頁面。例如,每個進程共享內核的代碼和全局數據結構

1、Linux虛擬存儲器區域(Windows下也有區域的概念)

Linux將虛擬存儲器組織成一些區域(也叫做段)的集合。一個區域(area)就是已經存在著的(已分配的)虛擬存儲器的連續片(chunk),這些頁是以某種方式相關聯的。例如,代碼段、數據段、堆、共享庫段,以及用戶棧都不同的區域。每個存在的虛擬頁面保存在某個區域中,而不屬於某個區域的虛擬頁是不存在的,並且不能被進程引用。區域的概念很重要,因為它允許虛擬地址空間有間隙。內核不用記錄那些不存在的虛擬頁,而這樣的頁也不占用存儲器。磁盤或者內核本身的任何額外資源

內核為系統中的每個進程維護一個單獨的任務結構(源代碼中的task_struct)。任務結構中的元素包含或者指向內核運行該進程所需要的所有信息(例如,PID,指向用戶棧的指針、可執行的目標文件的名字以及程序計數器)。

技術分享

task_struct中的一個條目指向mm_struct,它描述了虛擬存儲器中的當前狀態。其中pgd指向第一級頁表(頁全局目錄)的基址,而mmap指向一個vm_area_struct(區域結構)的鏈表,其中每個vm_area_structs都描述了當前虛擬地址空間的一個區域(area)。當內核運行這個進程時,它就將pgd存放在CR3控制寄存器中。

一個具體區域結構包含下面的字段:

  • vm_start:指向這個區域的起始處。
  • vm_end:指向這個區域的結束處。
  • vm_prot:描述這個區域的內包含的所有頁的讀寫許可權限。
  • vm_flags:描述這個區域內頁面是與其他進程共享的,還是這個進程私有的(還描述了其他一些信息)。
  • vm_next:指向鏈表中下一個區域結構。
存儲器映射(Windows下也有類似的機制,名叫內存映射) Linux(以及其他一些形式的Unix)通過將一個虛擬存儲器區域與一個磁盤上的對象(object)關聯起來,以初始化這個虛擬存儲器區域的內容,這個過程稱為存儲器映射(memory mapping)。虛擬存儲器區域可以映射到兩種類型的對象的一種: (1)Unix文件上的普通文件:一個區域可以映射到一個普通磁盤文件的連續部分,例如一個可執行目標文件。文件區(section)被分成頁大小的片,每一片包含一個虛擬頁面的初始化內容。因為按需進行頁面高度,所以這些虛擬頁面沒有實際進行物理存儲器,直到CPU第一次引用到頁面(即發射一個虛擬地址,落在地址空間這個頁面的範圍之內)。如果區域文件區要大,那麽就用零來填充這個區域的余下部分。 (2)匿名文件:一個區域也可以映射到一個匿名文件,匿名文件是由內核創建的,包含的全是二進制零。CPU第一次引用這樣一個區域內的虛擬頁面時,內核就在物理存儲器中找到一個合適的犧牲頁面,如果該頁面被修改過,就將這個頁面換出來,用二進制零覆蓋犧牲頁面並更新頁表,將這個頁面標記為是駐留在存儲器中的。註意在磁盤和存儲器之間沒有實際的數據傳送。因為這個原因,映射到匿名文件的區域中的頁面有時也叫做請求二進制零的頁(demand-zero page)。 無論在哪種情況下,一旦一個虛擬頁面被初始化了, 它就在一個由內核維護的專門的交換文件(swap file)之間換來換去。交換文件也叫做交換空間(swap space)或者交換區域(swap area)。需要意識到的很重要的一點,在任何時刻,交換空間都限制著當前運行著的進程能夠分配的虛擬頁面的總數再看共享對象 一個對象可以被映射到虛擬存儲的一個區域,要麽作為共享對象,要麽作為私有對象。如果一個進程將一個共享對象映射到它的虛擬地址空間的一個區域內,那麽這個進程對這個區域的任何寫操作,對於那些也把這個共享對象映射到它們虛擬存儲器的其他進程而言也是可見的。而且,這此變化也會反映在磁盤上的原始對象中。(IPC的一種方式) 另一方面,對一個映射到私有對象的區域做的改變,對於其他進程來說是不可見的,並且進程對這個區域所做的任何寫操作都不會反映在磁盤上的對象中。一個映射到共享對象的虛擬存儲器區域叫做共享區域。類似地,也有私有區域。

共享對象的關鍵點在於即使對象被映射到了多個共享區域,物理存儲器也只需要存放共享對象的一個拷貝。

一個共享對象(註意,物理頁面不一定是連續的。)

技術分享

私有對象是使用一種叫做寫時拷貝(copy-on-write)的巧妙技術被映射到虛擬存儲器中的。對於每個映射私有對象的進程,相應私有區域的頁表條目都被標記為只讀,並且區域結構被標記為私有的寫時拷貝。

技術分享

再看fork函數

當fork函數被當前進程調用時,內核為新進程創建各種數據結構,並分配給它一個唯一的PID。為了給這個新進程創建虛擬存儲器,它創建了當前進程的mm_struct、區域結構和頁表的原樣拷貝。它將兩個進程中的每個頁面都為標記只讀,並將兩個進程中的每個區域結構都標記為私有的寫時拷貝。

當fork在新進程中返回時,新進程現在的虛擬存儲器剛好和調用fork時存在的虛擬存儲器相同。當這兩個進程中的任一個後來進行寫操作時,寫時拷貝機制就會創建新頁面,因此,也就為每個進程保持了私有地址空間的抽象概念。

再看execve函數

假設運行在當前進程中的程序執行了如下的調用:

execve("a.out",NULL,NULL) ;

execve函數在當前進程中加載並運行包含在可執行目標文件a.out中的程序,用a.out程序有效地替代了當前程序。加載並運行a.out需要以下幾個步驟:

  • 刪除已存在的用戶區域。刪除當前進程虛擬地址用戶部分中的已存在的區域結構。
  • 映射私有區域。為新程序的文本、數據、bss和棧區域創建新的區域結構。所有這些新的區域都是私有的、寫時拷貝的。文本和數據區域被映射為a.out文件中的文本和數據區。bss區域是請求二進制零的,映射到匿名文件,其大小包含在a.out中。棧和堆區域也是請求二進制零的。
  • 映射共享區域。如果a.out程序與共享對象(或目標)鏈接,比如標準C庫libc.so,那麽這些對象都是動態鏈接到這個程序的,然後再映射到用戶虛擬地址空間中的共享區域內。
  • 設置程序計數器(PC)。execve做的最後一件事情就是設置當前進程上下文中的程序計數器,使之指向文本區域的入口點。
下一次調度這個進程時,它將從這個入口點開始執行。Linux將根據需要換入代碼和數據頁面。 技術分享 使用mmap函數的用戶級存儲器映射 [cpp] view plaincopy
  1. #include<unistd.h>
  2. #include<sys/mman.h>
  3. void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offset) ;
  4. //返回:若成功時則為指向映射區域的指針,若出錯則為MAP_FAILED(-1)

mmap函數要求內核創建一個新的虛擬存儲器區域是,最好是從地址start開始的一個區域,並將文件描述符fd指定的對象的一個連續的片(chunk)映射到這個新區域。連續的對象片大小為length字節,從距文件開始處偏移量為offset字節的地方開始。start地址僅僅是一個暗示,通常被定義為NULL。


技術分享







[cpp] view plaincopy
  1. munmap函數刪除虛擬存儲器的區域:
  2. #include<unistd.h>
  3. #include<sys/mman.h>
  4. int munmap(void *start,size_t length);
  5. //返回:若成功則為0,若出錯則為-1

1、需要額外的虛擬存儲器時,使用一種動態存儲器分配器(dynamic memory allocator)。一個動態存儲器分配器維護著一個進程的虛擬存儲器區域,稱為堆(heap)。在大多數的unix系統中,堆是一個請求二進制0的區域;對於每個進程,內核維護著一個變量brk,它指向堆的頂部。

技術分享

2、分配器將堆視為一組不同大小的塊(block)的集合來維護。每個塊就是一個連續的虛擬存儲器組塊(chunk),要麽是已分配的,要麽是未分配的。

1)顯式分配器(explicit allocator):如通過malloc,free或C++中通過new,delete來分配和釋放一個塊。

2)隱式分配器(implicit allocator):也叫做垃圾收集器(garbage collector)。自動釋放未使用的已分配的塊的過程叫做垃圾回收(garbage collection)。

3、malloc不初始化它返回的存儲器,calloc是一個基於malloc的包裝(wrapper)函數,它將分配的存儲器初始化為0。想要改變一個以前已分配的塊的大小,可以使用realloc函數。

4、分配器必須對齊塊,使得它們可以保存任何類型的數據對象。在大多數系統中,以8字節邊界對齊。

不修改已分配的塊:分配器只能操作或者改變空閑塊。一旦被分配,就不允許修改或者移動它。

5、碎片(fragmentation)

有內部碎片(internal)和外部碎片(external)。

外部碎片:在一個已分配塊比有效載荷在時發生的。(如對齊要求,分配最小值限制等)

外部碎片:當空閑存儲器合計起來足夠滿足一個分配請求,但是沒有一個單獨的空閑塊足夠大可以來處理這個請求時發生的。

6、隱式空間鏈表

技術分享

技術分享

放置分配的塊的策略有:首次適配(first fit),下一次適配(next fit),和最佳適配(best fit)。

如果空閑塊已經最大程度的合並,而仍然不能生成一個足夠大的塊,來滿足要求的話,分配器就會向內核請求額外的堆存儲器,要麽是通過調用nmap,要麽是通過調用sbrk函數;分配器都會將額外的(增加的)存儲器轉化成一個大的空閑塊,將這個塊插入到空閑鏈表中,然後將被請求的塊放置在這個新的空閑塊中。

7、書中對分配器的設計舉了一個小例子,10.9.12節。

8、一種流行的減少分配時間的方法,稱為分離存儲(segregated storage),維護多個空閑鏈表,其中每個鏈表中的塊有大致相等的大小。

關於“簡單分離存儲”、“分離適配”、“夥伴系統”等概念,10.9.14節進行了敘述。

垃圾回收

1、垃圾收集器將存儲器視為一張有向可達圖(reachability graph)。

技術分享

2、Mark%Sweep垃圾收集器由標記(mark)階段和清除(sweep)階段組成。標記階段標記出根節點的所有可達的和已分配的後繼,而後面的清除階段釋放每個被標記的已分配塊。典型地,塊頭部中空閑的低位中的一位來表示這個塊是否被標記了。

技術分享

Note that the arrows in this example denote memory references, and not free list pointers.

3、在10.11中,講述了與存儲器相關的錯誤,值得一讀。

如:指針的算術運算是以它們指向的對象的大小為單位來進行的。

深入理解計算機系統之虛擬存儲器