Linux虛擬存儲器
虛擬存儲器
虛擬存儲器是硬件異常、硬件地址翻譯、主存、磁盤文件和內核軟件的完美交互,它為每個進程提供了一個大的、一致的和私有的地址空間。通過一個很清晰的機制,虛擬存儲器提供了三個重要的能力:
(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:指向鏈表中下一個區域結構。
共享對象的關鍵點在於即使對象被映射到了多個共享區域,物理存儲器也只需要存放共享對象的一個拷貝。
一個共享對象(註意,物理頁面不一定是連續的。)
私有對象是使用一種叫做寫時拷貝(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做的最後一件事情就是設置當前進程上下文中的程序計數器,使之指向文本區域的入口點。
- #include<unistd.h>
- #include<sys/mman.h>
- void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offset) ;
- //返回:若成功時則為指向映射區域的指針,若出錯則為MAP_FAILED(-1)
mmap函數要求內核創建一個新的虛擬存儲器區域是,最好是從地址start開始的一個區域,並將文件描述符fd指定的對象的一個連續的片(chunk)映射到這個新區域。連續的對象片大小為length字節,從距文件開始處偏移量為offset字節的地方開始。start地址僅僅是一個暗示,通常被定義為NULL。
[cpp] view plaincopy
- munmap函數刪除虛擬存儲器的區域:
- #include<unistd.h>
- #include<sys/mman.h>
- int munmap(void *start,size_t length);
- //返回:若成功則為0,若出錯則為-1
Linux虛擬存儲器