unix虛擬存儲器詳解

       昨晚和舍友星光夜談到12點多,今天一大早就要睡覺。你要問談的什麼這麼來勁,我只能說既不是美女也不是電影,而是Linux下面的虛存管理機制!尷尬我們是不是很用功,哈哈哈。今天抽時間來對unix下面的虛存機制總結一下,就當溫故而知新吧!

       大家可能經常聽說什麼段頁式內存管理,虛擬內存,虛地址等,然後沒學過操作系統的朋友聽着一片暈乎!今天我們就來深扒一下unix系統下的內存管理機制得意

       首先很多朋友會問,爲什麼要有虛擬存儲器呢?比如我單片機寫程序的時候就直接用物理內存地址啊,沒見得什麼不妥的。確實,對應某個單一程序而言,在保證內存空間大小滿足的情況下,確實不需要什麼虛擬內存機制,直接用就OK了。但是對於一個多進程運行的系統而言,虛擬內存機制實在是必不可少的。我們知道一個系統的進程是與其他進程共享CPU和內存資源的,但是共享內存會形成一些問題。如果沒有虛存機制,我們來看一下下面兩種情況:

1.當多個進程需要的空間大於內存容量,那麼它們當中必然會有某些進程無法運行,超過空間的進程就會崩潰了。

2.當A進程不小心寫了B進程使用的地址空間時,那麼B進程在執行時行爲會無法預測,太危險了。

一直這樣做也會使得物理存儲器特別容易損壞。

       爲了更好地有效管理存儲器並且不出錯,現在的主流系統(unix/windows)都提供了虛擬內存管理機制,將邏輯存儲地址與實際物理地址區分開來。並且虛存爲每個進程都提供一個獨立的,同等大小的私有地址空間。

一. 虛擬尋址











圖中的MMU是CPU芯片中用於將虛擬地址轉換成物理地址的地址翻譯器。

首先忽略高級緩存L1,我們只看處理器,MMU,還有物理存儲器。

處理器只管尋址虛擬地址,交給MMU,MMU翻譯成物理地址,到物理存儲器中取指令或數據。

早期的PC和現在的數字信號處理器比如DSP,單片機等用的還是直接物理尋址,沒有虛存機制。不過目前的計算機系統大都提供虛存機制。


二. 地址空間

如果是虛存尋址,那麼虛存空間可以完全和物理存儲在邏輯上獨立開來,容量也可以不受實際物理存儲器的大小限制。那麼實際虛存容量究竟有多大呢?以前在本科上課的時候聽老師說過是4G,當時就鬱悶過,如果一個進程所需空間大於4G怎麼辦,而且4G也不大啊。。。然後發現老師說的並不準確,確實虛存的大小要CPU地址總線長度有關係,如果是32位的CPU,那麼2的32次方是4G。如果是64位的CPU(目前PC基本都是64位的,32位絕種了),那麼2的64次方是4G*4G(太大了,天文數字)。。。因爲太大了,遠遠超過目前實際所需,所以CPU生產廠商限制了地址位長


三.虛擬存儲器結構

概念上講,虛擬存儲器(VM)被組織爲一個由存放在磁盤(注意是磁盤!!!生氣)上的N個連續的字節大小的單元組成的數組。每個字節都有一個唯一的虛擬地址,這個唯一的虛擬地址是作爲到數組的索引。

虛擬存儲器以虛擬頁爲單位,每個頁有P個字節。

物理內存也是以頁爲單位區別的,每個頁也是P個字節,不過是叫物理頁。

虛擬存儲機制是用物理內存做爲緩存的,下面我們來看一下:












任意時刻,虛擬頁只能有三種狀態:

1.未分配:VM系統還未分配的頁。意思是沒有任何數據,還處於無用狀態,也不佔用任何磁盤空間。

2.已緩存到物理存儲器中。

3.已分配,但爲緩存到物理存儲器中。

值得注意的是,虛擬頁與物理頁並不是按照順序對應的,可以說基本不可能按照順序對應,也沒有這個必要。任何兩個進程的物理頁可能都是交替分佈在內存中的,我們無需考慮。


下面我們來談談頁表(PT:page table),頁表是用來表示每個虛擬頁的實際狀態。到底這塊頁是分配了沒有,如果分配了緩存了沒有,緩存在哪裏?這些信息就記錄在頁表中,所以頁表非常的重要。頁表將虛擬頁映射到物理頁。每次地址翻譯器(MMU)轉換成物理地址時,都會讀取頁表。操作系統負責維護頁表的內容,以及在磁盤與DRAM之間來回傳送頁。下面我們結合圖來看一下:














頁表的每個頁表項(PTE)由一個有效位和一個N位地址字段組成。

1)當有效位是1時,表示爲已緩存。後面是物理頁地址。

2)當有效位是0時,後面字段非0,則爲已分配,未緩存。

3)當有效位是0時,後面字段是0,則爲爲分配。


下面我們結合CPU送出虛擬地址到MMU,然後看MMU是如何構造出物理地址的。

有三種情況值得我們注意:

1.頁命中情況

MMU接受CPU傳來的虛擬地址後,將它作爲一個索引,可以將地址的前幾位做簡單運算變爲虛擬頁,從而找到PT中的PTE2,有效位爲1,表示已緩存,所以用PTE2中的物理存儲器地址構造出該字的物理地址。具體的構造過程很簡單:PTE2中的地址是物理頁的首地址,只要將它加上CPU傳來的虛擬地址在虛擬頁中的偏移量就可以了。


2.缺頁情況

如果DRAM主存不命中稱爲缺頁。即該頁還未被緩存。比如CPU引用VP0中的字,那麼在頁表中有效位爲0,未緩存,則會觸發一個缺頁異常。缺頁異常調用內核中的缺頁異常處理程序,該程序根據一定的算法選出一個犧牲頁,比如PP2中的VP2。當VP2發現已經被修改過了,那麼內核就會將它拷貝回磁盤。並將PTE5修改有效位爲0,將後面的地址該爲磁盤的VP2地址。接下來將VP0拷貝到存儲器中的PP2中,然後修改PTE6。現在VP0已經在主存中了。接着將導致缺頁的虛擬地址重新發送給MMU,這樣就可以頁命中了。


3.分配頁面

當操作系統分配一個新的虛擬存儲頁時,通過在磁盤上創建空間,並更新頁表,使它指向磁盤上的這個新創建的頁面,從而分配新的虛擬頁。


有了虛擬存儲的頁機制,可以極大地

1)簡化鏈接

每個進程擁有相同的存儲格式(稍後我們會看到),不管代碼和數據實際存放在物理存儲器的何處。

文本區總是從虛地址的0x08048000處開始,棧總是從地址0xbfffffff開始,共享庫代碼總是從地址0x40000000開始,內核代碼和數據總是從地址xc0000000開始。這樣的一致性可以很大的簡化了鏈接器的設計和實現,這些可執行文件是獨立於物理存儲器中代碼和數據的最終位置的。

2)簡化共享

這個很容易理解了,比如很多程序都有相同的代碼,都同用一個內核代碼,這樣可以將各個進程中適當的虛擬頁面映射到相同的物理頁面上,達到共享的目標。

3)簡化加載

ELF可執行文件中的.text和.data借是相鄰的。爲了加載這些節到新創建的進程中,可以將頁表項指向目標文件中的適當位置,操作系統從進程虛存文本區分配一個連續的虛擬頁面區域。值得注意的是,加載器並不講磁盤中的代碼區拷貝到物理存儲器中,而只是簡單修改頁表指向磁盤文件特定區域。當文件執行時,再利用缺頁機制將其拷貝到主存中。這就是所謂的懶人模式,極大的提高的系統的效率。


再談頁表

目前爲止,我們只用一個進程的頁表進行分析,實際上系統中可能有多個進程,每個進程都有屬於自己的頁表。下面我們來算一下32位地址空間,4KB頁面,和一個4字節的PTE,我們需要4GB/4KB*4B = 4MB,一個進程就要用掉4MB的內存當頁表。這顯然是我們不能接受的,設想一下64位的地址空間需要多大的頁表。。。。幸好unix系統爲我們提供了多級頁表機制。即將頁表分成多級,我們已兩級頁表看一下具體的劃分。我們結合圖來看一下:
























第一級頁表中每個頁表項PTE負責4MB的組塊,這裏每個組塊都是由1024個連續的4KB頁面組成。如果一個組塊沒有被分配,那麼該表項爲0,如果組塊中有一個頁表分配,那麼第一級頁表中的該表項指向第二級頁表基地址。第二級頁表負責具體頁表的映射狀態。這樣算一下第一級頁表需要4KB,第二級頁表也只需要4KB。一般只有一級頁表才需要總是在主存中。VM可以在需要時創建第二級頁表,這樣減少了主存的壓力。只有最經常使用的第二級頁表才需要常駐內存中。


基本的地址翻譯過程已經講完了,不過需要告訴各位的是實際中系統的地址翻譯要比這個更加複雜,因爲加上了L1,L2,L3各級緩存,同時也有TLB頁表項緩存器,下面貼一下貌似是core-i7的地址翻譯過程:



































具體感興趣的同學可以去了解一下大笑。有了剛剛講的這些基礎,看起來應該不困難。


五.Linux虛擬存儲器區域

Linux將虛擬存儲器組織成一下段的集合。段是一些虛存中連續的組塊。有代碼段,數據段,堆,共享庫段,以及用戶棧等段。每個分配的虛擬頁總是存在在一個特定段中,不屬於某個段的虛擬頁是不存在的,而且不能被進程引用。

多說沒用,我來貼一下著名的linux進程虛擬存儲器:

































放這麼大的圖是讓大家看清楚一點,我調整了大小,整好一個web頁面可以看全(我用的是臺式機偷笑)。

說到這個圖,就不得不說一下linux內核中表示虛存的結構,瞭解了可以方便大家今後深入內核研究。

內核在系統中爲每個進程維護一個單獨的任務結構(tast_struct)。任務結構中的元素指向內核運行該進程所需的所有信息(如:PID等)。

task_struct中的一個條目指向mm_struct。它描述了虛存中的當前狀態。我們最感興趣的是兩個字段:pgd和mmap,pgd指向頁面目錄表的基址,而mmap指向一個vm_area_structs的鏈表,其中每個vm_area_structs都描述了當前虛存空間中的一個區域。當內核運行這個進程時,它就將pgd存放在PDBR控制器中。

我在這裏就說vm_area_structs中每個元素代表的意思了,我們看個圖就一目瞭然。





















是不是很清楚大笑


六. 存儲器映射

Linux通過將一個虛擬存儲區域與一個磁盤上的對象關聯起來,已初始化這個虛擬存儲區域的內容,這個過程稱爲存儲器映射(memory mapping)。

Linux中有兩種映射方式:

1.文件系統中的普通文件:一個區域可以映射到一個普通磁盤文件的連續部分。文件被文成頁面大小的片,每一個片包含一個虛擬頁面的初始內容。因爲懶人模式進行頁調度,也就是要用的時候才換進主存中,所以初始化的虛擬頁面並沒有與主存有任何的交互。有人問,“萬一區域沒有頁面大怎麼辦”,那麼不足的部分就補0。


2.匿名文件:一個區域也可以映射到一個由內核創建的匿名文件上,匿名文件是由內核創建的,包含的全是二進制0。CPU第一次引用這樣一個區域內的虛擬頁面時,內核就在物理存儲器中找到一個合適的犧牲頁,如果該頁面被修改過了,就將這個頁面換出來,並修改這個犧牲頁的PTE,然後用0覆蓋犧牲頁面,並更新虛擬頁的PTE(有效位置1,後面地址更物理內存地址),表示這個頁面駐留在存儲器中的。注意和文件映射不同的是,在磁盤和存儲器之間沒有實際的數據傳送,因爲這個原因,所以被映射到匿名文件區域中的頁面,也可以被叫成請求二進制0的頁。


這段內容很重要!!!無論在哪種情況中,一旦一個虛擬頁面被初始化了,它就在一個由內核維護的專門的交換空間(swap)之間換來換去。在任何一個時刻,交換空間都限制着當前運行着的進程能夠分配的虛擬頁面總數。(內存容量+swap空間大小)/4KB = 最大能分配的虛擬頁面數。假如A進程malloc了1GB空間,B進程也malloc了1GB空間,如果內存大小爲1.5GB,swap爲1.5GB,那麼B進程在申請堆空間的時候需要講A進程中的一部分換出到swap(磁盤上)上面,然後申請1GB大小給B。再考慮極端一點的情況,如果swap大小爲300MB,那麼對不起,內存+swap才1.8G,肯定不夠A,B進程的總虛擬頁面數,結果可想而知,就是系統崩潰。。。。

如果大家對這部分有疑問的話,可以看一下這個帖子,是我以前問的,學校各種大神解答的非常完美,相信對你會有很大的幫助!存儲器映射問題解答


七. 共享對象與私有對象

關於這個共享機制其實不用我多囉嗦了,它的必要性大家都知道。不過我要說的是在linux中對私有對象的共享是採用一種叫copy on write的巧妙技術實現的,有必要說一下。比如:父進程fork一個子進程後,父進程中原來的段子進程也完全一樣。開始的時候子進程與父進程的某些虛地址對應相同的物理地址空間。當子進程對私有對象進行改變時,就會將原來私有對象先copy到別的物理地址上,再做修改。這個對父進程沒有任何影響。















根據上圖來理解一下上面的那段話吧,感覺會不會好一點。其實我說的只是個大概的實現,具體的實現如下:

一個私有對象在物理存儲器中只保存有私有對象的一份拷貝。比如,圖a中的情況,其中兩個進程將一個私有對象映射到它們虛擬存儲器的不同區域,但是共享這個對象同一個物理拷貝。對於每個映射私有對象的進程,相應私有區域的頁表條目都被標記爲只讀,並且區域結構被標記爲私有的寫時拷貝。只要沒有進程對它進行寫訪問,那麼這些進程繼續共享物理存儲器中對象的一個單獨拷貝。然而,只要一個進程試圖寫私有區域內的某個頁面,那麼這個寫操作就會觸發一個保護故障。

當故障處理程序注意到保護異常是由於進程試圖寫私有的寫時拷貝區域中的一個頁面而引起的,它會在物理存儲器中創建這個頁面的一個新拷貝,更新頁表目指向這個新拷貝,然後恢復這個頁面的可寫權限,如圖b所示。當故障處理程序返回時,CPU重新執行這個寫操作,現在在新創建的頁面上這個寫操作就可以正常執行了。

這種寫方式可以最充分地利用稀有的物理存儲器資源。


好啦,碼完了,大功告成。這裏幫助大家建立一系列的基本概念,並沒有深入內核分析,所以各位有時間的話一定極力推薦結合源碼來看看具體的虛存實現。大笑



Hello, my name is Linus. And I am your God!








發佈了65 篇原創文章 · 獲贊 75 · 訪問量 19萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章