1. 程式人生 > >Linux虛擬存儲器

Linux虛擬存儲器

light 新的 大小 位或 -o view ear 不可 需要

虛擬存儲器

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

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

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

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

物理和虛擬尋址

物理尋址

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

虛擬尋址

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

技術分享圖片

MMU(memory management unit,存儲器管理單元),利用存放在主存中的查詢表來動態翻譯虛擬地址,該表的內容由OS管理。

地址空間

地址空間(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的,在程序中還沒有被引用的)
技術分享圖片

虛擬頁0和3還沒有被分配,因此在磁盤上還不存在。虛擬頁1、4和6被緩存在物理存儲器中。頁2、5和7已經被分配了,但是當前並未緩存在主存中,只存在於磁盤中。

頁表

同任何緩存一樣,虛擬存儲器系統必須有某種方法來判定一個虛擬頁是否存放在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:指向鏈表中下一個區域結構。
2、linux缺頁異常處理 假設MMU在試圖翻譯某個虛擬地址A時,觸發了一個缺頁。這個異常導致控制轉移到內核的缺頁處理程序,處理程序隨後就執行下面的步驟: 1)虛擬地址A是合法的嗎?換句話說,A在某個區域結構定義的區域內嗎?為了回答這個問題,缺頁處理程序搜索區域結構的鏈表,把A和每個區域結構中的vm_start和vm_end做比較。如果這個指令時不合法的,那麽缺頁處理程序就觸發一個段錯誤(說明這個虛擬地址還沒有映射),從而終止這個進程。這個情況在圖9-28中標識為“1” 2)試圖進行的存儲器訪問是否合法?換句話說,進程是否有讀、寫或者執行這個區域內頁面的權限?例如,這個缺頁是不是由一條試圖對這個代碼段裏的只讀頁面進行寫操作的存儲指令造成的?這個缺頁是不是因為一個運行在用戶模式中的進程試圖從內核虛擬存儲器中讀取字造成的?如果試圖進行的訪問時不合法的,那麽缺頁處理程序會觸發一個保護異常,從而終止這個進程。這種情況標識為"2" 3)此刻,內核知道了這個缺頁是由於對合法的虛擬地址進行合法的操作造成的。它是這樣來處理這個缺頁的:選擇一個犧牲頁面,如果這個犧牲頁面被修改過,那麽就將它交換出去,換入新的頁面並更新頁表。當缺頁處理程序返回時,CPU重新啟動引起缺頁的指令,這條指令將再次發送A到MMU。這次,MMU就能正常地翻譯A,而不會產生缺頁中斷了。 技術分享圖片 存儲器映射(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
虛擬存儲器是對主存的一個抽象。支持虛擬存儲器的處理器通過使用一種叫做虛擬尋址的間接形式來引用主存。處理器產生一個虛擬地址,在被發送到主存之前,這個地址被翻譯成一個物理地址。從虛擬地址空間到物理地址空間的地址翻譯要求硬件和軟件緊密合作。專門的硬件通過使用頁表來翻譯虛擬地址,而頁表的內容是由操作系統提供的。 虛擬存儲器提供三個重要的功能。第一,它在主存中自動緩存最近使用的存放磁盤上的虛擬地址空間的內容。虛擬存儲器緩存中的塊叫做頁。對磁盤上頁的引用會觸發缺頁,缺頁將控制轉移到操作系統中的一個缺頁處理程序。缺頁處理程序將頁面從磁盤拷貝到主存緩存,如果必要,將寫回被驅逐的頁。第二,虛擬存儲器簡化了存儲器管理,進而又簡化了鏈接、在進程間共享數據、進程的存儲器分配以及程序加載。最後,虛擬存儲器通過在每條頁表條目中加入保護位,從而簡化了存儲器保護。 地址翻譯的過程必須和系統中所有的硬件緩存的操作集成在一起。大多數頁表條目位於L1高速緩存中,但是一個稱為TLB的頁表條目的片上高速緩存,通常會消除訪問在L1上的頁表條目的開銷。 現代系統通過將虛擬存儲器片和磁盤上的文件片關聯起來,以初始化虛擬存儲器片,這個過程稱為存儲器映射。存儲器映射為共享數據、創建新的進程以及加載程序提供了一種高效的機制。應用可以使用mmap函數來手工地創建和刪除虛擬地址空間的區域。然而,大多數程序依賴於動態存儲器分配器,例如malloc,它管理虛擬地址空間區域內一個稱為堆的區域。動態存儲器分配器是一個感覺像系統級程序的應用級程序,它直接操作存儲器,而無需類型系統的很多幫助。分配器有兩種類型。顯式分配器要求應用顯式地釋放他們的存儲器塊。隱式分配器(垃圾收集器)自動釋放任何未使用的和不可達的塊。

Linux虛擬存儲器