引言:前面連續幾章講述的文件系統是存儲系統的外存管理的一種抽象,而虛擬內存則是存儲系統的內存管理的一種抽象。其實這兩種原理有類似地地方,固然也就有不一樣的地方。同時這二者也屬於操做系統內核的範疇。 html
虛擬內存又叫虛擬存儲器(Virtual Memory),虛擬內存是計算機系統內存管理的一種技術。mysql
咱們都知道,進程運行前必須將程序加載到內存中,而根據Parkinson定律「存儲有多大,程序就會有多長」,因此如何有效的管理內存一直是計算機須要解決的問題,也所以提出了不少簡單高效的方案,例如虛擬內存是其中之一。因此虛擬內存是由於內存不足而提出來的,而目前這種技術也已經廣泛應用在大多數操做系統中,具體實現起來可能稍有不一樣。程序員
虛擬內存簡單的定義就是把進程在內存和磁盤之間換進換出。如何換?頁面置換。固然這只是比較常見的方法,也還有其餘方法或者幾種方法的組合。算法
在虛擬內存技術提出以前,其實已有另外一種更簡單更直接的技術:交換技術。sql
交換技術,就是把各個進程完整地調入內存,運行一段時間後,而後再放回到磁盤上。緩存
而虛擬內存,只需把進程的一部份內容存放在內存中,並且也能保證進程的正常運行。ide
這兩種方式雖然加載的對象大小不相同,可是都須要進程的換進換出。同時,進程的堆棧是實時變化的,那麼該如何管理內存空間呢?函數
其中,操做系統的內核進程是常駐在內存,通常固定在內存空間地址最低端,如上圖。32位系統中,通常狀況下固定大小1GB爲系統區,而其餘3GB爲用戶區。post
內存是動態分配的,那麼如何記錄當前內存的使用狀況?有如下兩種方式。性能
在位圖法中,內存被劃分爲不少個單元,每一個單元對應於位圖中的某個數據位,0表示空閒,1表示佔用。以下圖顯示了部分的內存和相應的位圖。
分配單元的大小是一個很重要的設計問題,分配單元越小,位圖越大。位圖法,簡單;可是查找一串K個連續的0,比較複雜和緩慢。
對內存的管理創建一個鏈表來管理已分配和空閒的內存空間。如上圖的(c),P 表明進程Process,H表示空閒Hole,接下來兩個字段依次表示起始地址和長度,最後一個字段是指向下一個節點的指針。在這個例子中,鏈表是按照地址從低到高排序的。這樣作的好處是當一個進程運行結束或被置換出去時,能夠很方便地來更新鏈表。
當一個新的程序加載進來的時候又該如何找到空閒的地址空間?一般的分配算法有最早匹配法、下次匹配法、最佳匹配法、最壞匹配法和快速匹配法。這些算法特別是前面幾個都有些相似,咱們簡單介紹下。
最早匹配法的基本思路是:假如進程的大小M,從鏈表的首節點開始查找,每一個空閒塊的長和M比較,是否大於或等於它,直到找到第一個符合要求的節點。
下次匹配法是在上次查找的結果基礎下繼續查找直到匹配成功。
最佳匹配法須要遍歷全部節點,找到能裝得下的最小空閒塊。而最壞匹配法則相反,找能裝得下的最大的空閒塊。事實上,這兩種都有比較並且遍歷全部鏈表,效果不佳。
快速匹配法和其餘不太同樣,基本思路是:對於一些經常使用的請求大小例如2K、4K、8K等,爲它們分別設置鏈表。這樣查找匹配很是快,可是若是程序結束時或者被置換後,可能須要合併左鄰右舍等操做操做複雜,若是不能合併則可能造成小空洞碎片。
對於這些碎片或者說一些不連續的小黑洞,能夠採用內存緊縮(memory compation)技術:把全部的進程都儘量地往內存地址的低端移動,相應地,那些空閒的小分區就會往高端移動,從而在地址高端造成一個較大的空閒區。
上面的交換技術是把程序做爲一個總體放入內存。但若是程序太大了,超出了空閒內存的容量,沒辦法裝入,該怎麼辦?事實上大部分多是這種狀況。
當時人們一般採用的解決方案是覆蓋技術,即:把程序劃分紅若干個部分,每一個部分叫作覆蓋塊(overlay),而後把那些當前須要用到的指令和數據的覆蓋塊保存在內存中,其餘放外存,運行一段時候後,在內外存之間再交換所需的覆蓋塊。
雖然覆蓋塊的交換是由操做系統完成,可是覆蓋塊的劃分最開始由程序員來手工完成,這是一項很是複雜的工做,費時費力。後不久人們又想到了一種辦法,能夠把工做交給計算機完成。
這種方法叫作虛擬存儲器(Virtual Memory)(Fotheringham,1961),咱們通常稱做虛擬內存。它的基本思路是:程序的代碼、數據和棧的總大小能夠超過實際可用的物理內存的大小。操做系統把當前須要的那些部分保留在內存中,而把其他部分保持在磁盤上。而後在再須要的時候,再把各個程序片斷在內存和磁盤之間來回交換。
大部分虛擬存儲系統採用的是一種稱爲分頁(paging)的技術。這種方式叫作虛擬頁式存儲管理。
由程序產生的地址稱爲虛擬地址(virtual address),它們構成了一個虛擬地址空間(virtual address space)。虛擬地址也叫作線性地址(linear address)。
若是計算機沒有使用虛擬存儲機制,那麼虛擬地址就是最終的物理地址,它被直接放在地址總線上,從而能夠對相應地址的內存單元進行讀寫操做。若是計算機使用了虛擬存儲機制,那麼虛擬地址不是直接放在地址總線上,而是被送到存儲管理單元(Memory Management Unit,MMU),由它負責把虛擬地址映射爲物理地址。MMU通常集成在CPU芯片內部,從邏輯上講,它能夠是單獨的一個芯片。
物理內存空間劃分爲固定大小的內存塊,稱爲物理頁面,或者是頁框(page frame)。
虛擬地址空間也劃分紅大小相同的塊,稱爲虛擬頁面,或者簡稱頁面(page)。
頁面和頁框的大小一般是同樣的,要求是2的整數次冪,通常在512字節到1G字節之間。程序在換入換出的時候是以頁面爲單位。
MMU能夠完成虛擬地址到物理地址的映射,可是咱們知道,虛擬地址空間是遠遠大於物理地址空間即內存空間的,因此也就不能保證全部虛擬地址能找到對應的物理地址,即沒法完成映射。此時,MMU會引起一個缺頁中斷(page fault),把這個問題交給操做系統處理。操做系統從內存中挑選一個使用很少的物理頁面,把它的內容寫回到磁盤,從而騰出了一個空閒頁面,而後把引起缺頁中斷的那個虛擬頁面裝入該空閒頁面中,並對地址映射進行更新。最後回到被中斷的指令從新開始。
下面咱們來看看MMU的內部結構,瞭解一下它的工做原理。舉例:頁面大小4KB、虛擬地址空間是64KB、物理內存是32KB,所以可獲得16個虛擬頁面和8個物理頁面。以下圖所示,虛擬地址8196(二進制是0010 0000 0000 0100),輸入的16位虛擬地址被劃分爲兩部分:4位的頁號和12位的偏移量。4位的頁號能夠表示16個頁面,12位的偏移量能夠尋址4096個字節。
在進行地址映射時,使用虛擬頁面號做爲索引去訪問頁表(page table),從而獲得相應的物理地址。若是有效位(頁表最低位)爲0,則產生缺頁中斷,陷入操做系統中;不然將頁表查到的物理頁面號加上偏移量,就獲得了15位的物理地址。
如上所述,虛擬地址被分爲虛擬頁面號(高位)和偏移量(低位)兩部分。高4位指定虛擬頁面號,也能夠是3位、5位或其餘,不一樣的劃分表示不一樣的頁面大小。
頁表的用途是將虛擬頁面映射爲相應的物理頁面。
上述例子中只是16位的虛擬地址空間,那麼32位的虛擬地址空間爲4GB,64位的地址空間可達16EB(雖然咱們廣泛不用這麼多位),若是將頁面映射放在一個頁表中,那麼頁表項將很是龐大。此外因爲每一個進程都有本身的虛擬地址空間,所以每一個進程也有本身的頁表。這樣,頁表的數量和規模將更加龐大。有沒有一種大而快速的頁面映射解決方案呢?分級。
多級頁表的基本思路是:雖然進程的虛擬地址空間很大,可是當進程在運行時,並不會用到全部的虛擬地址,因此不必把全部的頁表項都保存在內存中。
以下圖,一個典型的二級頁表的例子,虛擬地址爲32位,頁面大小4KB。虛擬頁面號爲20位,分紅兩級,最高10位表示頁目錄,中間10位爲頁表,從而造成10+10+12的二級頁表。 二級頁表也能夠擴展爲三級、四級或更多級。64位處理器典型地劃分爲9+9+9+9+12的四級頁表。更多的級別帶來了更多的靈活性,但算法的複雜性也會更高。
頁目錄項和頁表項具備相同的結構,但不一樣的CPU對頁表項的具體安排會有所不一樣,咱們討論一些共性。以下圖,給出了一個頁表項的示例。頁表項的長度因機器而異,通常使用的32位即4字節。
物理頁面號------最重要的就是物理頁面號,頁映射的目的就是找到這個值。
有效位------1表示該表項是有效的,可使用;0則表示該表項對應的虛擬頁面如今不在內存中,訪問該頁面會引發一個缺頁中斷。
保護位------指出一個頁容許什麼樣的方式訪問,最簡單的形式是隻有一位,0表示讀/寫,1表示只讀;更先進的方式是使用三位,各位分別表示是否啓用讀、寫、執行該頁面。
修改位------記錄頁面的使用狀況,在寫入一個頁時自動設置修改位。若是一個頁面已經被修改過(稱爲「髒頁面」),則必須把它寫會磁盤。若是沒有被修改過(稱爲「乾淨頁面」),能夠直接被覆蓋,由於它在磁盤上有備份。修改位也稱爲髒位,反映了頁面的當前狀態。
訪問位------不管是讀仍是寫,系統都會在該頁面被訪問時設置訪問位。用於頁面置換算法中,未被訪問的一般認爲是不常用的而被置換出去。頁面置換咱們稍後介紹。
禁止緩存位------禁止該頁面被高速緩存,對於映射到設備寄存器而不是常規內存的頁面很重要。具備獨立的I/O空間而不使用內存映射I/O的機器不須要這一位。高速緩存在下一章介紹。
從上面咱們能夠看出,每一次內存訪問,都須要兩次訪問頁表,而隨着頁表的增多,總體性能是會受很大影響的。後面人們發現絕大多數程序運行時,在任意一個階段都只會訪問一小部分的頁面,而非全部頁面。這就是訪問的局部性原理,或者說程序局部性原理。
人們利用這個特性爲計算機設計和增長了一種快速查找的硬件,即TLB(Translation lookaside Buffer)或者稱爲關聯存儲器(associative memory),用來存放最經常使用的頁表項。這種硬件設備能夠直接把虛擬地址映射到物理地址,而沒必要訪問內存,因此簡稱爲快表。TLB一般位於MMU中,只包含了少許的表項,書上說不超過64,網上有的說不超過256,總之很是少。
工做過程:當一個虛擬地址到來時,MMU首先會到TLB中查找,這個查找很是快,由於它是並行的方式,即同時與全部的頁表項進行比較。若是找到了,直接取出物理頁面號。不然若是權限夠的話將再去內存中查找所需的物理頁面號;而後,再將找到的物理頁面號所在的頁表項添加到TLB中,同時驅逐TLB中某一個頁表項;最後將被驅逐的頁表項的修改位複製到對應的內存中的頁表項。
上面咱們描述的硬件TLB,TLB的管理和TLB未命中時的處理都交由MMU硬件完成,只有當頁面不在內存中時纔會陷入到操做系統。
而在現代有些機器中,幾乎全部的頁面管理工做都是有軟件來完成,TLB表項也由操做系統負責載入。若是發生TLB未命中,MMU會產生一個TLB中斷,把問題交給操做系統。操做系統來對TLB進行頁面置換,可是這項工做必須用不多的命令完成,由於TLB未命中的頻率遠遠高於缺頁中斷的頻率。爲此,人們也設計出了一些方法來提升未命中的機率,例如在內存固定位置設置一個較大的緩衝區,存放最近經常使用的TLB表項;或者預測經常使用的頁面預先裝入TLB中。
前面咱們講述的頁表方案是經過進程的虛擬頁面號來組織的,用虛擬頁表號來做爲訪問頁表的索引。若是頁面大小4KB,32位尋址時,一個進程的頁表項個數是100萬。若是再按每一個頁表項長度是4字節,一個進程的頁表須要4MB的內存空間。這是32位,若是變成64位尋址,須要的內存顯然是個天文數字。
一種解決方案就是反置頁表(inverted page table),也稱做倒排頁表,根據內存的物理頁面號來組織頁表,用物理頁面號做爲訪問頁表的索引。有多少個物理頁面,就在頁表中設置多少個頁表項。而通常狀況下物理頁面遠遠小於虛擬頁面,因此這種方法節省了大量的內存空間。但同時也帶來一個問題,即從虛擬頁面號到物理頁面號的轉換變得複雜。必須搜索整個頁表。
擺脫這種困境就是使用TLB。由於TLB存放了咱們常常訪問的頁面。不過若是TLB未命中,則仍然須要對整個反置頁表進行搜索。爲了加快加快這個過程,人們又想到了一個辦法,使用虛擬地址創建一個哈希表。若是兩個虛擬頁面具備相同的哈希值,那麼它們就用鏈表連起來。(這種方法是否是似曾相識呢:Redis+MySQL,後面提到LRU方法亦是。)
以上對虛擬內存的頁式存儲管理的基本原理已經介紹完成,咱們再深刻了解幾個知識點。最後補充一下其餘的存儲管理方式。
虛擬內存的核心就是進程的換入換出,也就是缺頁中斷進行頁面置換。問題來了,如何選擇被置換的頁面?頁面的換進換出是須要開銷的,因此一個好的頁面算法就是儘可能減小頁面換進換出的次數。
最優算法------易於描述但沒法實現,通常用做目標或者說算法性能評價的依據。思路是:對於每個虛擬頁面,都計算出下一次訪問的時間,用指令數來計算,而後選擇等待時間最長的那個頁面。明顯的比較理想,虛擬頁面多並且下一次訪問也是不肯定的。
最近未使用算法------Not Recently Used,NRU。按頁表項中的訪問位和修改位的值對頁面進行分紅四類,對應四個值,值越小,越沒被使用過。而後在值最小的那類再隨機抽取一個頁面。
先進先出算法------First In First Out,FIFO。把最早訪問的頁面放在鏈表的首部,後面訪問的再依次排隊在鏈表的首部,最早的變成了尾部,選擇的就是鏈尾頁面。該算法可能會淘汰一些不經常使用的頁面,可是也存在淘汰一些經常使用只是暫時沒用的頁面。
第二次機會算法------針對FIFO進行了改進,根據FIFO獲得一個頁面時不是直接淘汰,而是再給一次機會,把它放在鏈表的首部。
時鐘算法------第二次機會須要移動鏈表節點,時鐘算法將鏈表變成環形,首尾相連。
最近最久未使用------Least Recently Used,LRU。選擇最久未被使用的頁面。最優算法的一個近似,它的理論依據是程序的局部性原理。若是某個頁面被訪問了,它頗有可能立刻被訪問;一樣若是它好久沒被訪問,那麼未來可能很長時間也不會被訪問。在LRU基礎上,利用頁表項中的訪問位和修改位這兩個值和二進制移位,獲得改進的算法,叫老化算法。減小了LRU鏈表的操做。
上面討論的算法是在一個進程內部,若是在相互競爭的進程之間如何分配呢?有兩種策略。
局部分配策略------爲每一個進程分配固定大小的內存空間。
全局分配策略------全部進程能夠動態分配內存空間。一般狀況下比局部策略更好,置換頁面的時候能夠考慮整個內存空間,減小缺頁發送的次數。
頁式存儲管理中,進程啓動之初,全部頁面都在外存,因此CPU去取第一個頁面時,會引起缺頁中斷。隨着是一系列的缺頁中斷,一段時間後,中斷次數會減小。這種策略就叫作請求調頁(demand paging),根據須要隨要隨調。
而前面咱們介紹過局部性原理,絕大多數進程實際訪問的頁面只是很小的一部分,咱們把一個進程當前正在使用的頁面集合叫作它的工做集(working set)。若是咱們在程序運行前就預先裝入它的頁面,這種技術叫作預先調頁(prepaging)。而裝入的頁面就是進程運行所需的工做集,這種方法叫作工做集模型。爲了實現工做集模型,操做系統必須知道哪些頁面屬於工做集,一種方法就是前面討論的老化算法。固然,隨着時間的變化,進程的工做集也會發生變化。通常工做集具備漸進式、緩慢變化的特色。
可是進程的工做集有時會發送劇烈的變更,它的運行可能進入一個調整期。若是分配給一個進程的物理頁面數太少,不能包含整個工做集,這是進程會形成不少的缺頁中斷,須要頻繁的在內存和外存之間置換頁面,從而使得進程的運行速度變慢,咱們把這種狀態稱做抖動(Denning)。同時咱們也應該要優化咱們的代碼儘可能減小和避免這種狀況的發生!
頁面大小在頁式存儲管理系統中是一個很是重要的參數,也是一個能夠自定義的參數。查看系統中頁的大小:
[root@localhost mysql]# getconf PAGESIZE
4096
內核是以頁面做爲內存管理的單位,內存分配時,每次分配都是頁面大小的整數倍。頁面越小,內碎片就會越少。分配的內存通常不會是頁面的整數倍,最後一個頁面剩下的空間叫作內碎片。頁面越小,同時頁表就越龐大,進程運行時系統開銷也會越大。
另外,在內存和磁盤之間傳送數據也是以頁面爲單位的。因此文件系統的邏輯塊大小最好和頁面大小保持一致。
大多數計算機使用的頁面大小在512字節到1M之間,典型的值是1KB、4KB和8KB。如今隨着內存容量愈來愈大,頁面大小也愈來愈大。
前面介紹都是跟分頁方式相關的虛擬內存。事實上,虛擬內存的調度方式有分頁式、段式、段頁式3種。只是如今大部分的操做系統使用了分頁式,並且理解了分頁式,再來看其餘兩種就很是容易了。
(1)段式存儲管理
分頁式存儲管理是一維的,而段式則是二維的。即分頁式存儲的虛擬地址從0到某一個最大地址,一個接一個。而段式存儲提供多個相互獨立的地址空間,稱爲段(segment);每一個段的內部都是從0到某一個最大值這樣一個線性地址,段的長度能夠動態變化。
分段有助於在幾個進程之間共享函數和數據,一個典型的例子就是共享庫(shared library)。頁式存儲管理也能夠實現共享庫,可是要複雜得多,實際上它們都是經過模擬分段來實現的。
在具體實現上,段式和頁式存儲系統是徹底不一樣的:頁面是定長的而段不是。因此,隨着程序的運行,段式存儲很容易造成外碎片(external fragmentation)或者叫作跳棋盤(checkerboarding)。外碎片一般比較小,沒法再裝入新的段,容易形成浪費。固然這個問題也能夠經過前面2.2節講過的緊縮技術來解決。
(2)段頁式存儲管理
Intel Pentinum支持16K個段,每一個段最多能夠容納4GB的虛擬地址空間。操做系統能夠對其進行設置,使他支持純頁式、純段式或者段頁式存儲管理。大多數操做系統如Linux和Windows都採用純頁式存儲管理。
Pentinum虛擬存儲器的核心是兩張表,局部描述符(Local Descriptor Table,LDT)和全局描述符(Global Descriptor Table,GDT)。每一個進程都有本身的LDT,可是GDT只有一個,爲計算機上全部進程共享。LDT描述的是每一個程序本身的段,包括代碼段、數據段、棧段等,而GDT描述的是系統的段,包括操做系統自己。
上圖是Pentinum代碼段描述符的結構,數據段略有不一樣。本文再也不鋪開描述了,有興趣能夠參考:分段機制與GDT|LDT。
參考資料:
《操做系統設計與實現》第三版 上冊。
《深刻理解LINUX內核》第三版。