這是Java建設者的第74篇優質長文程序員
本文的思惟導圖算法
主存(RAM) 是一件很是重要的資源,必需要認真對待內存。雖然目前大多數內存的增加速度要比 IBM 7094 要快的多,可是,程序大小的增加要比內存的增加還快不少。無論存儲器有多大,程序大小的增加速度比內存容量的增加速度要快的多。下面咱們就來探討一下操做系統是如何建立內存並管理他們的。
通過多年的研究發現,科學家提出了一種 分層存儲器體系(memory hierarchy),下面是分層體系的分類
位於頂層的存儲器速度最快,可是相對容量最小,成本很是高。層級結構向下,其訪問速度會變慢,可是容量會變大,相對造價也就越便宜。(因此我的感受相對存儲容量來講,訪問速度是更重要的)
操做系統中管理內存層次結構的部分稱爲內存管理器(memory manager),它的主要工做是有效的管理內存,記錄哪些內存是正在使用的,在進程須要時分配內存以及在進程完成時回收內存。全部現代操做系統都提供內存管理。
下面咱們會對不一樣的內存管理模型進行探討,從簡單到複雜,因爲最低級別的緩存是由硬件進行管理的,因此咱們主要探討主存模型和如何對主存進行管理。
無存儲器抽象編程
最簡單的存儲器抽象是無存儲器。早期大型計算機(20 世紀 60 年代以前),小型計算機(20 世紀 70 年代以前)和我的計算機(20 世紀 80 年代以前)都沒有存儲器抽象。每個程序都直接訪問物理內存。當一個程序執行以下命令:數組
MOV REGISTER1, 1000
計算機會把位置爲 1000 的物理內存中的內容移到 REGISTER1 中。所以呈現給程序員的內存模型就是物理內存,內存地址從 0 開始到內存地址的最大值中,每一個地址中都會包含一個 8 位位數的內存單元。
因此這種狀況下的計算機不可能會有兩個應用程序同時在內存中。若是第一個程序向內存地址 2000 的這個位置寫入了一個值,那麼此值將會替換第二個程序 2000 位置上的值,因此,同時運行兩個應用程序是行不通的,兩個程序會馬上崩潰。
不過即便存儲器模型就是物理內存,仍是存在一些可變體的。下面展現了三種變體
在上圖 a 中,操做系統位於 RAM(Random Access Memory) 的底部,或像是圖 b 同樣位於 ROM(Read-Only Memory) 頂部;而在圖 c 中,設備驅動程序位於頂端的 ROM 中,而操做系統位於底部的 RAM 中。圖 a 的模型之前用在大型機和小型機上,但如今已經不多使用了;圖 b 中的模型通常用於掌上電腦或者是嵌入式系統中。第三種模型就應用在早期我的計算機中了。ROM 系統中的一部分紅爲 BIOS (Basic Input Output System)。模型 a 和 c 的缺點是用戶程序中的錯誤可能會破壞操做系統,可能會致使災難性的後果。
按照這種方式組織系統時,一般同一個時刻只能有一個進程正在運行。一旦用戶鍵入了一個命令,操做系統就把須要的程序從磁盤複製到內存中並執行;當進程運行結束後,操做系統在用戶終端顯示提示符並等待新的命令。收到新的命令後,它把新的程序裝入內存,覆蓋前一個程序。
在沒有存儲器抽象的系統中實現並行性一種方式是使用多線程來編程。因爲同一進程中的多線程內部共享同一內存映像,那麼實現並行也就不是問題了。可是這種方式卻並無被普遍採納,由於人們一般但願可以在同一時間內運行沒有關聯的程序,而這正是線程抽象所不能提供的。
運行多個程序瀏覽器
可是,即使沒有存儲器抽象,同時運行多個程序也是有可能的。操做系統只須要把當前內存中全部內容保存到磁盤文件中,而後再把程序讀入內存便可。只要某一時刻內存只有一個程序在運行,就不會有衝突的狀況發生。
在額外特殊硬件的幫助下,即便沒有交換功能,也能夠並行的運行多個程序。IBM 360 的早期模型就是這樣解決的
System/360是 IBM 在1964年4月7日,推出的劃時代的大型電腦,這一系列是世界上首個指令集可兼容計算機。緩存
在 IBM 360 中,內存被劃分爲 2KB 的區域塊,每塊區域被分配一個 4 位的保護鍵,保護鍵存儲在 CPU 的特殊寄存器(SFR)中。一個內存爲 1 MB 的機器只須要 512 個這樣的 4 位寄存器,容量總共爲 256 字節 (這個會算吧) PSW(Program Status Word, 程序狀態字) 中有一個 4 位碼。一個運行中的進程若是訪問鍵與其 PSW 中保存的碼不一樣,360 硬件會捕獲這種狀況。由於只有操做系統能夠修改保護鍵,這樣就能夠防止進程之間、用戶進程和操做系統之間的干擾。
這種解決方式是有一個缺陷。以下所示,假設有兩個程序,每一個大小各爲 16 KB
從圖上能夠看出,這是兩個不一樣的 16KB 程序的裝載過程,a 程序首先會跳轉到地址 24,那裏是一條 MOV 指令,然而 b 程序會首先跳轉到地址 28,地址 28 是一條 CMP 指令。這是兩個程序被前後加載到內存中的狀況,假如這兩個程序被同時加載到內存中而且從 0 地址處開始執行,內存的狀態就如上面 c 圖所示,程序裝載完成開始運行,第一個程序首先從 0 地址處開始運行,執行 JMP 24 指令,而後依次執行後面的指令(許多指令沒有畫出),一段時間後第一個程序執行完畢,而後開始執行第二個程序。第二個程序的第一條指令是 28,這條指令會使程序跳轉到第一個程序的 ADD 處,而不是事先設定好的跳轉指令 CMP,因爲這種不正確訪問,可能會形成程序崩潰。
上面兩個程序的執行過程當中有一個核心問題,那就是都引用了絕對物理地址,這不是咱們想要看到的。咱們想要的是每個程序都會引用一個私有的本地地址。IBM 360 在第二個程序裝載到內存中的時候會使用一種稱爲 靜態重定位(static relocation) 的技術來修改它。它的工做流程以下:當一個程序被加載到 16384 地址時,常數 16384 被加到每個程序地址上(因此 JMP 28會變爲JMP 16412 )。雖然這個機制在不出錯誤的狀況下是可行的,但這不是一種通用的解決辦法,同時會減慢裝載速度。更近一步來說,它須要全部可執行程序中的額外信息,以指示哪些包含(可重定位)地址,哪些不包含(可重定位)地址。畢竟,上圖 b 中的 JMP 28 能夠被重定向(被修改),而相似 MOV REGISTER1,28 會把數字 28 移到 REGISTER 中則不會重定向。因此,裝載器(loader)須要必定的能力來辨別地址和常數。
一種存儲器抽象:地址空間服務器
把物理內存暴露給進程會有幾個主要的缺點:第一個問題是,若是用戶程序能夠尋址內存的每一個字節,它們就能夠很容易的破壞操做系統,從而使系統中止運行(除非使用 IBM 360 那種 lock-and-key 模式或者特殊的硬件進行保護)。即便在只有一個用戶進程運行的狀況下,這個問題也存在。
第二點是,這種模型想要運行多個程序是很困難的(若是隻有一個 CPU 那就是順序執行)。在我的計算機上,通常會打開不少應用程序,好比輸入法、電子郵件、瀏覽器,這些進程在不一樣時刻會有一個進程正在運行,其餘應用程序能夠經過鼠標來喚醒。在系統中沒有物理內存的狀況下很難實現。
地址空間的概念網絡
若是要使多個應用程序同時運行在內存中,必需要解決兩個問題:保護和 重定位。咱們來看 IBM 360 是如何解決的:第一種解決方式是用保護密鑰標記內存塊,並將執行過程的密鑰與提取的每一個存儲字的密鑰進行比較。這種方式只能解決第一種問題(破壞操做系統),可是不能解決多進程在內存中同時運行的問題。
還有一種更好的方式是創造一個存儲器抽象:地址空間(the address space)。就像進程的概念建立了一種抽象的 CPU 來運行程序,地址空間也建立了一種抽象內存供程序使用。地址空間是進程能夠用來尋址內存的地址集。每一個進程都有它本身的地址空間,獨立於其餘進程的地址空間,可是某些進程會但願能夠共享地址空間。
基址寄存器和變址寄存器數據結構
最簡單的辦法是使用動態重定位(dynamic relocation)技術,它就是經過一種簡單的方式將每一個進程的地址空間映射到物理內存的不一樣區域。從 CDC 6600(世界上最先的超級計算機)到 Intel 8088(原始 IBM PC 的核心)所使用的經典辦法是給每一個 CPU 配置兩個特殊硬件寄存器,一般叫作基址寄存器(basic register)和變址寄存器(limit register)。當使用基址寄存器和變址寄存器時,程序會裝載到內存中的連續位置而且在裝載期間無需重定位。當一個進程運行時,程序的起始物理地址裝載到基址寄存器中,程序的長度則裝載到變址寄存器中。在上圖 c 中,當一個程序運行時,裝載到這些硬件寄存器中的基址和變址寄存器的值分別是 0 和 16384。當第二個程序運行時,這些值分別是 16384 和 16384。若是第三個 16 KB 的程序直接裝載到第二個程序的地址之上而且運行,這時基址寄存器和變址寄存器的值會是 32768 和 16384。那麼咱們能夠總結下多線程
若是計算機的物理內存足夠大來容納全部的進程,那麼以前說起的方案或多或少是可行的。可是實際上,全部進程須要的 RAM 總容量要遠遠高於內存的容量。在 Windows、OS X、或者 Linux 系統中,在計算機完成啓動(Boot)後,大約有 50 - 100 個進程隨之啓動。例如,當一個 Windows 應用程序被安裝後,它一般會發出命令,以便在後續系統啓動時,將啓動一個進程,這個進程除了檢查應用程序的更新外不作任何操做。一個簡單的應用程序可能會佔用 5 - 10MB 的內存。其餘後臺進程會檢查電子郵件、網絡鏈接以及許多其餘諸如此類的任務。這一切都會發生在第一個用戶啓動以前。現在,像是 Photoshop 這樣的重要用戶應用程序僅僅須要 500 MB 來啓動,可是一旦它們開始處理數據就須要許多 GB 來處理。從結果上來看,將全部進程始終保持在內存中須要大量內存,若是內存不足,則沒法完成。
因此針對上面內存不足的問題,提出了兩種處理方式:最簡單的一種方式就是交換(swapping)技術,即把一個進程完整的調入內存,而後再內存中運行一段時間,再把它放回磁盤。空閒進程會存儲在磁盤中,因此這些進程在沒有運行時不會佔用太多內存。另一種策略叫作虛擬內存(virtual memory),虛擬內存技術可以容許應用程序部分的運行在內存中。下面咱們首先先探討一下交換
交換過程
下面是一個交換過程
剛開始的時候,只有進程 A 在內存中,而後從建立進程 B 和進程 C 或者從磁盤中把它們換入內存,而後在圖 d 中,A 被換出內存到磁盤中,最後 A 從新進來。由於圖 g 中的進程 A 如今到了不一樣的位置,因此在裝載過程當中須要被從新定位,或者在交換程序時經過軟件來執行;或者在程序執行期間經過硬件來重定位。基址寄存器和變址寄存器就適用於這種狀況。
交換在內存建立了多個 空閒區(hole),內存會把全部的空閒區儘量向下移動合併成爲一個大的空閒區。這項技術稱爲內存緊縮(memory compaction)。可是這項技術一般不會使用,由於這項技術回消耗不少 CPU 時間。例如,在一個 16GB 內存的機器上每 8ns 複製 8 字節,它緊縮所有的內存大約要花費 16s。
有一個值得注意的問題是,當進程被建立或者換入內存時應該爲它分配多大的內存。若是進程被建立後它的大小是固定的而且再也不改變,那麼分配策略就比較簡單:操做系統會準確的按其須要的大小進行分配。
可是若是進程的 data segment 可以自動增加,例如,經過動態分配堆中的內存,確定會出現問題。這裏仍是再提一下什麼是 data segment 吧。從邏輯層面操做系統把數據分紅不一樣的段(不一樣的區域)來存儲:
_TEXT segment dword public use32 'CODE'
_TEXT ends
_DATA segment dword public use32 'DATA'
_DATA ends
_BSS segment dword public use32 'BSS'
_BSS ends
段定義( segment ) 是用來區分或者劃分範圍區域的意思。彙編語言的 segment 僞指令表示段定義的起始,ends 僞指令表示段定義的結束。段定義是一段連續的內存空間 **因此內存針對自動增加的區域,會有三種處理方式** * 若是一個進程與空閒區相鄰,那麼可把該空閒區分配給進程以供其增大。 * 若是進程相鄰的是另外一個進程,就會有兩種處理方式:要麼把須要增加的進程移動到一個內存中空閒區足夠大的區域,要麼把一個或多個進程交換出去,已變成生成一個大的空閒區。 * 若是一個進程在內存中不能增加,並且磁盤上的交換區也滿了,那麼這個進程只有掛起一些空閒空間(或者能夠結束該進程) * ![](https://s4.51cto.com/images/blog/202012/01/cbe51de981391bfb3f6c849785d83b4b.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=) 上面只針對單個或者一小部分須要增加的進程採用的方式,若是大部分進程都要在運行時增加,爲了減小因內存區域不夠而引發的進程交換和移動所產生的開銷,一種可用的方法是,在換入或移動進程時爲它分配一些額外的內存。然而,當進程被換出到磁盤上時,應該只交換實際上使用的內存,將額外的內存交換也是一種浪費,下面是一種爲兩個進程分配了增加空間的內存配置。 ![](https://s4.51cto.com/images/blog/202012/01/8c61bd2b883dcab87f6981c4abf945e9.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=) 若是進程有兩個可增加的段,例如,供變量動態分配和釋放的做爲堆(全局變量)使用的一個數據段(data segment),以及存放局部變量與返回地址的一個堆棧段(stack segment),就如圖 b 所示。在圖中能夠看到所示進程的堆棧段在進程所佔內存的頂端向下增加,緊接着在程序段後的數據段向上增加。當增加預留的內存區域不夠了,處理方式就如上面的流程圖(data segment 自動增加的三種處理方式)同樣了。 **空閒內存管理** 在進行內存動態分配時,操做系統必須對其進行管理。大體上說,有兩種監控內存使用的方式 位圖(bitmap) 空閒列表(free lists) 下面咱們就來探討一下這兩種使用方式 **使用位圖的存儲管理** 使用位圖方法時,內存可能被劃分爲小到幾個字或大到幾千字節的分配單元。每一個分配單元對應於位圖中的一位,0 表示空閒, 1 表示佔用(或者相反)。一塊內存區域和其對應的位圖以下 ![](https://s4.51cto.com/images/blog/202012/01/66ff6fb3240d0811d78381ceacd1135e.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=) 圖 a 表示一段有 5 個進程和 3 個空閒區的內存,刻度爲內存分配單元,陰影區表示空閒(在位圖中用 0 表示);圖 b 表示對應的位圖;圖 c 表示用鏈表表示一樣的信息 分配單元的大小是一個重要的設計因素,分配單位越小,位圖越大。然而,即便只有 4 字節的分配單元,32 位的內存也僅僅只須要位圖中的 1 位。32n 位的內存須要 n 位的位圖,因此1 個位圖只佔用了 1/32 的內存。若是選擇更大的內存單元,位圖應該要更小。若是進程的大小不是分配單元的整數倍,那麼在最後一個分配單元中會有大量的內存被浪費。 位圖提供了一種簡單的方法在固定大小的內存中跟蹤內存的使用狀況,由於位圖的大小取決於內存和分配單元的大小。這種方法有一個問題是,當決定爲把具備 k 個分配單元的進程放入內存時,內容管理器(memory manager) 必須搜索位圖,在位圖中找出可以運行 k 個連續 0 位的串。在位圖中找出制定長度的連續 0 串是一個很耗時的操做,這是位圖的缺點。(能夠簡單理解爲在雜亂無章的數組中,找出具備一大長串空閒的數組單元) **使用鏈表進行管理** 另外一種記錄內存使用狀況的方法是,維護一個記錄已分配內存段和空閒內存段的鏈表,段會包含進程或者是兩個進程的空閒區域。可用上面的圖 c 來表示內存的使用狀況。鏈表中的每一項均可以表明一個 空閒區(H) 或者是進程(P)的起始標誌,長度和下一個鏈表項的位置。 在這個例子中,段鏈表(segment list)是按照地址排序的。這種方式的優勢是,當進程終止或被交換時,更新列表很簡單。一個終止進程一般有兩個鄰居(除了內存的頂部和底部外)。相鄰的多是進程也多是空閒區,它們有四種組合方式。 ![](https://s4.51cto.com/images/blog/202012/01/52319d8b199c74bfd528c5ae6c308fa4.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=) 當按照地址順序在鏈表中存放進程和空閒區時,有幾種算法能夠爲建立的進程(或者從磁盤中換入的進程)分配內存。咱們先假設內存管理器知道應該分配多少內存,最簡單的算法是使用 首次適配(first fit)。內存管理器會沿着段列表進行掃描,直到找個一個足夠大的空閒區爲止。除非空閒區大小和要分配的空間大小同樣,不然將空閒區分爲兩部分,一部分供進程使用;一部分生成新的空閒區。首次適配算法是一種速度很快的算法,由於它會盡量的搜索鏈表。 首次適配的一個小的變體是 下次適配(next fit)。它和首次匹配的工做方式相同,只有一個不一樣之處那就是下次適配在每次找到合適的空閒區時就會記錄當時的位置,以便下次尋找空閒區時從上次結束的地方開始搜索,而不是像首次匹配算法那樣每次都會從頭開始搜索。Bays(1997) 證實了下次算法的性能略低於首次匹配算法。 另一個著名的而且普遍使用的算法是 最佳適配(best fit)。最佳適配會從頭至尾尋找整個鏈表,找出可以容納進程的最小空閒區。最佳適配算法會試圖找出最接近實際須要的空閒區,以最好的匹配請求和可用空閒區,而不是先一次拆分一個之後可能會用到的大的空閒區。好比如今咱們須要一個大小爲 2 的塊,那麼首次匹配算法會把這個塊分配在位置 5 的空閒區,而最佳適配算法會把該塊分配在位置爲 18 的空閒區,以下 ![](https://s4.51cto.com/images/blog/202012/01/98ae0e06d08e8bbb834bb81e1f8ca869.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=) 那麼最佳適配算法的性能如何呢?最佳適配會遍歷整個鏈表,因此最佳適配算法的性能要比首次匹配算法差。可是使人想不到的是,最佳適配算法要比首次匹配和下次匹配算法浪費更多的內存,由於它會產生大量無用的小緩衝區,首次匹配算法生成的空閒區會更大一些。 最佳適配的空閒區會分裂出不少很是小的緩衝區,爲了不這一問題,能夠考慮使用 最差適配(worst fit) 算法。即老是分配最大的內存區域(因此你如今明白爲何最佳適配算法會分裂出不少小緩衝區了吧),使新分配的空閒區比較大從而能夠繼續使用。仿真程序代表最差適配算法也不是一個好主意。 若是爲進程和空閒區維護各自獨立的鏈表,那麼這四個算法的速度都能獲得提升。這樣,這四種算法的目標都是爲了檢查空閒區而不是進程。但這種分配速度的提升的一個不可避免的代價是增長複雜度和減慢內存釋放速度,由於必須將一個回收的段從進程鏈表中刪除並插入空閒鏈表區。 若是進程和空閒區使用不一樣的鏈表,那麼能夠按照大小對空閒區鏈表排序,以便提升最佳適配算法的速度。在使用最佳適配算法搜索由小到大排列的空閒區鏈表時,只要找到一個合適的空閒區,則這個空閒區就是能容納這個做業的最小空閒區,所以是最佳匹配。由於空閒區鏈表以單鏈表形式組織,因此不須要進一步搜索。空閒區鏈表按大小排序時,首次適配算法與最佳適配算法同樣快,而下次適配算法在這裏毫無心義。 另外一種分配算法是 快速適配(quick fit) 算法,它爲那些經常使用大小的空閒區維護單獨的鏈表。例如,有一個 n 項的表,該表的第一項是指向大小爲 4 KB 的空閒區鏈表表頭指針,第二項是指向大小爲 8 KB 的空閒區鏈表表頭指針,第三項是指向大小爲 12 KB 的空閒區鏈表表頭指針,以此類推。好比 21 KB 這樣的空閒區既能夠放在 20 KB 的鏈表中,也能夠放在一個專門存放大小比較特別的空閒區鏈表中。 快速匹配算法尋找一個指定代銷的空閒區也是十分快速的,但它和全部將空閒區按大小排序的方案同樣,都有一個共同的缺點,即在一個進程終止或被換出時,尋找它的相鄰塊並查看是否能夠合併的過程都是很是耗時的。若是不進行合併,內存將會很快分裂出大量進程沒法利用的小空閒區。 **虛擬內存** 儘管基址寄存器和變址寄存器用來建立地址空間的抽象,可是這有一個其餘的問題須要解決:管理軟件的不斷增大(managing bloatware)。雖然內存的大小增加迅速,可是軟件的大小增加的要比內存還要快。在 1980 年的時候,許多大學用一臺 4 MB 的 VAX 計算機運行分時操做系統,供十幾個用戶同時運行。如今微軟公司推薦的 64 位 Windows 8 系統至少須要 2 GB 內存,而許多多媒體的潮流則進一步推進了對內存的需求。 這一發展的結果是,須要運行的程序每每大到內存沒法容納,並且必然須要系統可以支持多個程序同時運行,即便內存能夠知足其中單獨一個程序的需求,可是從整體上來看內存仍然知足不了日益增加的軟件的需求(感受和xxx和xxx 的矛盾很類似)。而交換技術並非一個頗有效的方案,在一些中小應用程序尚可以使用交換,若是應用程序過大,難道還要每次交換幾 GB 的內存?這顯然是不合適的,一個典型的 SATA 磁盤的峯值傳輸速度高達幾百兆/秒,這意味着須要好幾秒才能換出或者換入一個 1 GB 的程序。 SATA(Serial ATA)硬盤,又稱串口硬盤,是將來 PC 機硬盤的趨勢,已基本取代了傳統的 PATA 硬盤。 那麼還有沒有一種有效的方式來應對呢?有,那就是使用 虛擬內存(virtual memory),虛擬內存的基本思想是,每一個程序都有本身的地址空間,這個地址空間被劃分爲多個稱爲頁面(page)的塊。每一頁都是連續的地址範圍。這些頁被映射到物理內存,但並非全部的頁都必須在內存中才能運行程序。當程序引用到一部分在物理內存中的地址空間時,硬件會馬上執行必要的映射。當程序引用到一部分不在物理內存中的地址空間時,由操做系統負責將缺失的部分裝入物理內存並從新執行失敗的指令。 在某種意義上來講,虛擬地址是對基址寄存器和變址寄存器的一種概述。8088 有分離的基址寄存器(但不是變址寄存器)用於放入 text 和 data 。 使用虛擬內存,能夠將整個地址空間以很小的單位映射到物理內存中,而不是僅僅針對 text 和 data 區進行重定位。下面咱們會探討虛擬內存是如何實現的。 虛擬內存很適合在多道程序設計系統中使用,許多程序的片斷同時保存在內存中,當一個程序等待它的一部分讀入內存時,能夠把 CPU 交給另外一個進程使用。 **分頁** 大部分使用虛擬內存的系統中都會使用一種 分頁(paging) 技術。在任何一臺計算機上,程序會引用使用一組內存地址。當程序執行
MOV REG,1000
這條指令時,它會把內存地址爲 1000 的內存單元的內容複製到 REG 中(或者相反,這取決於計算機)。地址能夠經過索引、基址寄存器、段寄存器或其餘方式產生。 這些程序生成的地址被稱爲 虛擬地址(virtual addresses) 並造成虛擬地址空間(virtual address space),在沒有虛擬內存的計算機上,系統直接將虛擬地址送到內存中線上,讀寫操做都使用一樣地址的物理內存。在使用虛擬內存時,虛擬地址不會直接發送到內存總線上。相反,會使用 MMU(Memory Management Unit) 內存管理單元把虛擬地址映射爲物理內存地址,像下圖這樣 ![](https://s4.51cto.com/images/blog/202012/01/1807322cea1799c9f9dcec0b272dca54.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=) 下面這幅圖展現了這種映射是如何工做的 ![](https://s4.51cto.com/images/blog/202012/01/a81d70f0276e9ebf4f857565c93809bb.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=) 頁表給出虛擬地址與物理內存地址之間的映射關係。每一頁起始於 4096 的倍數位置,結束於 4095 的位置,因此 4K 到 8K 實際爲 4096 - 8191 ,8K - 12K 就是 8192 - 12287 在這個例子中,咱們可能有一個 16 位地址的計算機,地址從 0 - 64 K - 1,這些是虛擬地址。然而只有 32 KB 的物理地址。因此雖然能夠編寫 64 KB 的程序,可是程序沒法所有調入內存運行,在磁盤上必須有一個最多 64 KB 的程序核心映像的完整副本,以保證程序片斷在須要時被調入內存。 **存在映射的頁如何映射** 虛擬地址空間由固定大小的單元組成,這種固定大小的單元稱爲 頁(pages)。而相對的,物理內存中也有固定大小的物理單元,稱爲 頁框(page frames)。頁和頁框的大小同樣。在上面這個例子中,頁的大小爲 4KB ,可是實際的使用過程當中頁的大小範圍多是 512 字節 - 1G 字節的大小。對應於 64 KB 的虛擬地址空間和 32 KB 的物理內存,可獲得 16 個虛擬頁面和 8 個頁框。RAM 和磁盤之間的交換老是以整個頁爲單元進行交換的。 程序試圖訪問地址時,例如執行下面這條指令
MOV REG, 0
會將虛擬地址 0 送到 MMU。MMU 看到虛擬地址落在頁面 0 (0 - 4095),根據其映射結果,這一頁面對應的頁框 2 (8192 - 12287),所以 MMU 把地址變換爲 8192 ,並把地址 8192 送到總線上。內存對 MMU 一無所知,它只看到一個對 8192 地址的讀寫請求並執行它。MMU 從而有效的把全部虛擬地址 0 - 4095 映射到了 8192 - 12287 的物理地址。一樣的,指令
MOV REG, 8192
也被有效的轉換爲
MOV REG, 24576
虛擬地址 8192(在虛擬頁 2 中)被映射到物理地址 24576(在物理頁框 6 中)上。 經過恰當的設置 MMU,能夠把 16 個虛擬頁面映射到 8 個頁框中的任何一個。可是這並無解決虛擬地址空間比物理內存大的問題。 上圖中有 8 個物理頁框,因而只有 8 個虛擬頁被映射到了物理內存中,在上圖中用 X 號表示的其餘頁面沒有被映射。在實際的硬件中,會使用一個 在/不在(Present/absent bit)位記錄頁面在內存中的實際存在狀況。 **未映射的頁如何映射** 當程序訪問一個未映射的頁面,如執行指令
MOV REG, 32780
將會發生什麼狀況呢?虛擬頁面 8 (從 32768 開始)的第 12 個字節所對應的物理地址是什麼?MMU 注意到該頁面沒有被映射(在圖中用 X 號表示),因而 CPU 會陷入(trap)到操做系統中。這個陷入稱爲 缺頁中斷(page fault) 或者是 缺頁錯誤。操做系統會選擇一個不多使用的頁並把它的內容寫入磁盤(若是它不在磁盤上)。隨後把須要訪問的頁面讀到剛纔回收的頁框中,修改映射關係,而後從新啓動引發陷入的指令。有點不太好理解,舉個例子來看一下。 例如,若是操做系統決定放棄頁框 1,那麼它將把虛擬機頁面 8 裝入物理地址 4096,並對 MMU 映射作兩處修改。首先,它要將虛擬頁中的 1 表項標記爲未映射,使之後任何對虛擬地址 4096 - 8191 的訪問都將致使陷入。隨後把虛擬頁面 8 的表項的叉號改成 1,所以在引發陷阱的指令從新啓動時,它將把虛擬地址 32780 映射爲物理地址(4096 + 12)。 下面查看一下 MMU 的內部構造以便了解它們是如何工做的,以及瞭解爲何咱們選用的頁大小都是 2 的整數次冪。下圖咱們能夠看到一個虛擬地址的例子 ![](https://s4.51cto.com/images/blog/202012/01/9925850b0831ceb2c42ded3adbb474aa.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=) 虛擬地址 8196 (二進制 0010000000000100)用上面的頁表映射圖所示的 MMU 映射機制進行映射,輸入的 16 位虛擬地址被分爲 4 位的頁號和 12 位的偏移量。4 位的頁號能夠表示 16 個頁面,12 位的偏移能夠爲一頁內的所有 4096 個字節。 可用頁號做爲頁表(page table) 的索引,以得出對應於該虛擬頁面的頁框號。若是在/不在位則是 0 ,則引發一個操做系統陷入。若是該位是 1,則將在頁表中查到的頁框號複製到輸出寄存器的高 3 位中,再加上輸入虛擬地址中的低 12 位偏移量。如此就構成了 15 位的物理地址。輸出寄存器的內容隨即被做爲物理地址送到總線。 **頁表** 在上面這個簡單的例子中,虛擬地址到物理地址的映射能夠總結以下:虛擬地址被分爲虛擬頁號(高位部分)和偏移量(低位部分)。例如,對於 16 位地址和 4 KB 的頁面大小,高 4 位能夠指定 16 個虛擬頁面中的一頁,而低 12 位接着肯定了所選頁面中的偏移量(0-4095)。 虛擬頁號可做爲頁表的索引用來找到虛擬頁中的內容。由頁表項能夠找到頁框號(若是有的話)。而後把頁框號拼接到偏移量的高位端,以替換掉虛擬頁號,造成物理地址。 ![](https://s4.51cto.com/images/blog/202012/01/0607ed45fa4a8737059332e7a3aa7d30.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=) 所以,頁表的目的是把虛擬頁映射到頁框中。從數學上說,頁表是一個函數,它的參數是虛擬頁號,結果是物理頁框號。 ![](https://s4.51cto.com/images/blog/202012/01/79b85df7b8ce033a193beebb0b7606aa.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=) 經過這個函數能夠把虛擬地址中的虛擬頁轉換爲頁框,從而造成物理地址。 **頁表項的結構** 下面咱們探討一下頁表項的具體結構,上面你知道了頁表項的大體構成,是由頁框號和在/不在位構成的,如今咱們來具體探討一下頁表項的構成 ![](https://s4.51cto.com/images/blog/202012/01/9360d2d5cf4bd2f7893375ff724fbd7a.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=) 頁表項的結構是與機器相關的,可是不一樣機器上的頁表項大體相同。上面是一個頁表項的構成,不一樣計算機的頁表項可能不一樣,可是通常來講都是 32 位的。頁表項中最重要的字段就是頁框號(Page frame number)。畢竟,頁表到頁框最重要的一步操做就是要把此值映射過去。下一個比較重要的就是在/不在位,若是此位上的值是 1,那麼頁表項是有效的而且可以被使用。若是此值是 0 的話,則表示該頁表項對應的虛擬頁面不在內存中,訪問該頁面會引發一個缺頁異常(page fault)。 保護位(Protection) 告訴咱們哪種訪問是容許的,啥意思呢?最簡單的表示形式是這個域只有一位,0 表示可讀可寫,1 表示的是隻讀。 修改位(Modified) 和 訪問位(Referenced) 會跟蹤頁面的使用狀況。當一個頁面被寫入時,硬件會自動的設置修改位。修改位在頁面從新分配頁框時頗有用。若是一個頁面已經被修改過(即它是 髒 的),則必須把它寫回磁盤。若是一個頁面沒有被修改過(即它是 乾淨的),那麼從新分配時這個頁框會被直接丟棄,由於磁盤上的副本仍然是有效的。這個位有時也叫作 髒位(dirty bit),由於它反映了頁面的狀態。 訪問位(Referenced) 在頁面被訪問時被設置,無論是讀仍是寫。這個值可以幫助操做系統在發生缺頁中斷時選擇要淘汰的頁。再也不使用的頁要比正在使用的頁更適合被淘汰。這個位在後面要討論的頁面置換算法中做用很大。 最後一位用於禁止該頁面被高速緩存,這個功能對於映射到設備寄存器仍是內存中起到了關鍵做用。經過這一位能夠禁用高速緩存。具備獨立的 I/O 空間而不是用內存映射 I/O 的機器來講,並不須要這一位。 在深刻討論下面問題以前,須要強調一下:虛擬內存本質上是用來創造一個地址空間的抽象,能夠把它理解成爲進程是對 CPU 的抽象,虛擬內存的實現,本質是將虛擬地址空間分解成頁,並將每一項映射到物理內存的某個頁框。由於咱們的重點是如何管理這個虛擬內存的抽象。 **加速分頁過程** 到如今咱們已經虛擬內存(virtual memory) 和 分頁(paging) 的基礎,如今咱們能夠把目光放在具體的實現上面了。在任何帶有分頁的系統中,都會須要面臨下面這兩個主要問題: 虛擬地址到物理地址的映射速度必需要快 若是虛擬地址空間足夠大,那麼頁表也會足夠大 第一個問題是因爲每次訪問內存都須要進行虛擬地址到物理地址的映射,全部的指令最終都來自於內存,而且不少指令也會訪問內存中的操做數。 操做數:操做數是計算機指令中的一個組成部分,它規定了指令中進行數字運算的量 。操做數指出指令執行的操做所須要數據的來源。操做數是彙編指令的一個字段。好比,MOV、ADD 等。 所以,每條指令可能會屢次訪問頁表,若是執行一條指令須要 1 ns,那麼頁表查詢須要在 0.2 ns 以內完成,以免映射成爲一個主要性能瓶頸。 第二個問題是全部的現代操做系統都會使用至少 32 位的虛擬地址,而且 64 位正在變得愈來愈廣泛。假設頁大小爲 4 KB,32 位的地址空間將近有 100 萬頁,而 64 位地址空間簡直多到沒法想象。 對大並且快速的頁映射的須要成爲構建計算機的一個很是重要的約束。就像上面頁表中的圖同樣,每個表項對應一個虛擬頁面,虛擬頁號做爲索引。在啓動一個進程時,操做系統會把保存在內存中進程頁表讀副本放入寄存器中。 最後一句話是否是很差理解?還記得頁表是什麼嗎?它是虛擬地址到內存地址的映射頁表。頁表是虛擬地址轉換的關鍵組成部分,它是訪問內存中數據所必需的。在進程啓動時,執行不少次虛擬地址到物理地址的轉換,會把物理地址的副本從內存中讀入到寄存器中,再執行這一轉換過程。 因此,在進程的運行過程當中,沒必要再爲頁表而訪問內存。使用這種方法的優點是簡單並且映射過程當中不須要訪問內存。缺點是 頁表太大時,代價高昂,並且每次上下文切換的時候都必須裝載整個頁表,這樣會形成性能的下降。鑑於此,咱們討論一下加速分頁機制和處理大的虛擬地址空間的實現方案 **轉換檢測緩衝區** 咱們首先先來一塊兒探討一下加速分頁的問題。大部分優化方案都是從內存中的頁表開始的。這種設計對效率有着巨大的影響。考慮一下,例如,假設一條 1 字節的指令要把一個寄存器中的數據複製到另外一個寄存器。在不分頁的狀況下,這條指令只訪問一次內存,即從內存取出指令。有了分頁機制後,會由於要訪問頁表而須要更多的內存訪問。因爲執行速度一般被 CPU 從內存中取指令和數據的速度所限制,這樣的話,兩次訪問才能實現一次的訪問效果,因此內存訪問的性能會降低一半。在這種狀況下,根本不會採用分頁機制。 什麼是 1 字節的指令?咱們以 8085 微處理器爲例來講明一下,在 8085 微處理中,一共有 3 種字節指令,它們分別是 1-byte(1 字節)、2-byte(2 字節)、3-byte(3 字節),咱們分別來講一下 1-byte:1 字節的操做數和操做碼共同以 1 字節表示;操做數是內部寄存器,並被編碼到指令中;指令須要一個存儲位置來將單個寄存器存儲在存儲位置中。沒有操做數的指令也是 1-byte 指令。 例如:MOV B,C 、LDAX B、NOP、HLT(這塊不明白的讀者能夠自行查閱) 2-byte: 2 字節包括:第一個字節指定的操做碼;第二個字節指定操做數;指令須要兩個存儲器位置才能存儲在存儲器中。 例如 MVI B, 26 H、IN 56 H 3-byte: 在 3 字節指令中,第一個字節指定操做碼;後面兩個字節指定 16 位的地址;第二個字節保存低位地址;第三個字節保存 高位地址。指令須要三個存儲器位置才能將單個字節存儲在存儲器中。 例如 LDA 2050 H、JMP 2085 H 大多數程序老是對少許頁面進行屢次訪問,而不是對大量頁面進行少許訪問。所以,只有不多的頁面可以被再次訪問,而其餘的頁表項不多被訪問。 頁表項通常也被稱爲 Page Table Entry(PTE)。 基於這種設想,提出了一種方案,即從硬件方面來解決這個問題,爲計算機設置一個小型的硬件設備,可以將虛擬地址直接映射到物理地址,而沒必要再訪問頁表。這種設備被稱爲轉換檢測緩衝區(Translation Lookaside Buffer, TLB),有時又被稱爲 相聯存儲器(associate memory) 。 ![](https://s4.51cto.com/images/blog/202012/01/fbf7c2b8208fbd781a2e465547e21606.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=) TLB 一般位於 MMU 中,包含少許的表項,每一個表項都記錄了頁面的相關信息,除了虛擬頁號外,其餘表項都和頁表是一一對應的 ![](https://s4.51cto.com/images/blog/202012/01/00f2e82e429d27966241d6f8d51fc1fb.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=) 是否是你到如今仍是有點不理解什麼是 TLB,TLB 其實就是一種內存緩存,用於減小訪問內存所須要的時間,它就是 MMU 的一部分,TLB 會將虛擬地址到物理地址的轉換存儲起來,一般能夠稱爲地址翻譯緩存(address-translation cache)。TLB 一般位於 CPU 和 CPU 緩存之間,它與 CPU 緩存是不一樣的緩存級別。下面咱們來看一下 TLB 是如何工做的。 當一個 MMU 中的虛擬地址須要進行轉換時,硬件首先檢查虛擬頁號與 TLB 中全部表項進行並行匹配,判斷虛擬頁是否在 TLB 中。若是找到了有效匹配項,而且要進行的訪問操做沒有違反保護位的話,則將頁框號直接從 TLB 中取出而不用再直接訪問頁表。若是虛擬頁在 TLB 中可是違反了保護位的權限的話(好比只容許讀可是是一個寫指令),則會生成一個保護錯誤(protection fault) 返回。 上面探討的是虛擬地址在 TLB 中的狀況,那麼若是虛擬地址再也不 TLB 中該怎麼辦?若是 MMU 檢測到沒有有效的匹配項,就會進行正常的頁表查找,而後從 TLB 中逐出一個表項而後把從頁表中找到的項放在 TLB 中。當一個表項被從 TLB 中清除出,將修改位複製到內存中頁表項,除了訪問位以外,其餘位保持不變。當頁表項從頁表裝入 TLB 中時,全部的值都來自於內存。 ![](https://s4.51cto.com/images/blog/202012/01/57817678bff16a093a1ca16eca4057ff.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=) **軟件 TLB 管理** 直到如今,咱們假設每臺電腦都有能夠被硬件識別的頁表,外加一個 TLB。在這個設計中,TLB 管理和處理 TLB 錯誤徹底由硬件來完成。僅僅當頁面不在內存中時,纔會發生操做系統的陷入(trap)。 在之前,咱們上面的假設一般是正確的。可是,許多現代的 RISC 機器,包括 SPARC、MIPS 和 HP PA,幾乎全部的頁面管理都是在軟件中完成的。 精簡指令集計算機或 RISC 是一種計算機指令集,它使計算機的微處理器的每條指令(CPI)週期比複雜指令集計算機(CISC)少 在這些計算機上,TLB 條目由操做系統顯示加載。當發生 TLB 訪問丟失時,再也不是由 MMU 到頁表中查找並取出須要的頁表項,而是生成一個 TLB 失效並將問題交給操做系統解決。操做系統必須找到該頁,把它從 TLB 中移除(移除頁表中的一項),而後把新找到的頁放在 TLB 中,最後再執行先前出錯的指令。然而,全部這些操做都必須經過少許指令完成,由於 TLB 丟失的發生率要比出錯率高不少。 ![](https://s4.51cto.com/images/blog/202012/01/65bf4d7a3fe435a39d9b8464c65cf320.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=) 不管是用硬件仍是用軟件來處理 TLB 失效,常見的方式都是找到頁表並執行索引操做以定位到將要訪問的頁面,在軟件中進行搜索的問題是保存頁表的頁可能不在 TLB 中,這將在處理過程當中致使其餘 TLB 錯誤。改善方法是能夠在內存中的固定位置維護一個大的 TLB 表項的高速緩存來減小 TLB 失效。經過首先檢查軟件的高速緩存,操做系統 可以有效的減小 TLB 失效問題。 TLB 軟件管理會有兩種 TLB 失效問題,當一個頁訪問在內存中而不在 TLB 中時,將產生 軟失效(soft miss),那麼此時要作的就是把頁表更新到 TLB 中(咱們上面探討的過程),而不會產生磁盤 I/O,處理僅僅須要一些機器指令在幾納秒的時間內完成。然而,當頁自己不在內存中時,將會產生硬失效(hard miss),那麼此時就須要從磁盤中進行頁表提取,硬失效的處理時間一般是軟失效的百萬倍。在頁表結構中查找映射的過程稱爲 頁表遍歷(page table walk)。 ![](https://s4.51cto.com/images/blog/202012/01/748e6c1980c25c33e03193f4770c7d0d.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=) 上面的這兩種狀況都是理想狀況下出現的現象,可是在實際應用過程當中狀況會更加複雜,未命中的狀況可能既不是硬失效又不是軟失效。一些未命中可能更軟或更硬(偷笑)。好比,若是頁表遍歷的過程當中沒有找到所須要的頁,那麼此時會出現三種狀況: 所需的頁面就在內存中,可是卻沒有記錄在進程的頁表中,這種狀況多是由其餘進程從磁盤掉入內存,這種狀況只須要把頁正確映射就能夠了,而不須要在從硬盤調入,這是一種軟失效,稱爲 次要缺頁錯誤(minor page fault)。 基於上述狀況,若是須要從硬盤直接調入頁面,這就是嚴重缺頁錯誤(major page falut)。 還有一種狀況是,程序可能訪問了一個非法地址,根本無需向 TLB 中增長映射。此時,操做系統會報告一個 段錯誤(segmentation fault) 來終止程序。只有第三種缺頁屬於程序錯誤,其餘缺頁狀況都會被硬件或操做系統以下降程序性能爲代價來修復 **針對大內存的頁表** 還記得咱們討論的是什麼問題嗎?(捂臉),可能討論的太多你有所不知道了,我再提醒你一下,上面加速分頁過程討論的是虛擬地址到物理地址的映射速度必需要快的問題,還有一個問題是 若是虛擬地址空間足夠大,那麼頁表也會足夠大的問題,如何處理巨大的虛擬地址空間,下面展開咱們的討論。 **多級頁表** 第一種方案是使用多級頁表(multi),下面是一個例子 ![](https://s4.51cto.com/images/blog/202012/01/efdbaa777a75278477bf1d02cd4ddd49.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=) 32 位的虛擬地址被劃分爲 10 位的 PT1 域,10 位的 PT2 域,還有 12 位的 Offset 域。由於偏移量是 12 位,因此頁面大小是 4KB,公有 2^20 次方個頁面。 引入多級頁表的緣由是避免把所有頁表一直保存在內存中。不須要的頁表就不該該保留。 多級頁表是一種分頁方案,它由兩個或多個層次的分頁表組成,也稱爲分層分頁。級別1(level 1)頁面表的條目是指向級別 2(level 2) 頁面表的指針,級別2頁面表的條目是指向級別 3(level 3) 頁面表的指針,依此類推。最後一級頁表存儲的是實際的信息。 下面是一個二級頁表的工做過程 ![](https://s4.51cto.com/images/blog/202012/01/0ff94313351c5e7f3a01e41282ff2d31.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=) 在最左邊是頂級頁表,它有 1024 個表項,對應於 10 位的 PT1 域。當一個虛擬地址被送到 MMU 時,MMU 首先提取 PT1 域並把該值做爲訪問頂級頁表的索引。由於整個 4 GB (即 32 位)虛擬地址已經按 4 KB 大小分塊,因此頂級頁表中的 1024 個表項的每個都表示 4M 的塊地址範圍。 由索引頂級頁表獲得的表項中含有二級頁表的地址或頁框號。頂級頁表的表項 0 指向程序正文的頁表,表項 1 指向含有數據的頁表,表項 1023 指向堆棧的頁表,其餘的項(用陰影表示)表示沒有使用。如今把 PT2 域做爲訪問選定的二級頁表的索引,以便找到虛擬頁面的對應頁框號。 **倒排頁表** 針對分頁層級結構中不斷增長的替代方法是使用 倒排頁表(inverted page tables)。採用這種解決方案的有 PowerPC、UltraSPARC 和 Itanium。在這種設計中,實際內存中的每一個頁框對應一個表項,而不是每一個虛擬頁面對應一個表項。 雖然倒排頁表節省了大量的空間,可是它也有本身的缺陷:那就是從虛擬地址到物理地址的轉換會變得很困難。當進程 n 訪問虛擬頁面 p 時,硬件不能再經過把 p 看成指向頁表的一個索引來查找物理頁。而是必須搜索整個倒排表來查找某個表項。另外,搜索必須對每個內存訪問操做都執行一次,而不是在發生缺頁中斷時執行。 解決這一問題的方式時使用 TLB。當發生 TLB 失效時,須要用軟件搜索整個倒排頁表。一個可行的方式是創建一個散列表,用虛擬地址來散列。當前全部內存中的具備相同散列值的虛擬頁面被連接在一塊兒。以下圖所示 ![](https://s4.51cto.com/images/blog/202012/01/0dfcd3c0db907458c9d74424b0cb61b5.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=) 若是散列表中的槽數與機器中物理頁面數同樣多,那麼散列表的衝突鏈的長度將會是 1 個表項的長度,這將會大大提升映射速度。一旦頁框被找到,新的(虛擬頁號,物理頁框號)就會被裝在到 TLB 中。 **頁面置換算法** 當發生缺頁異常時,操做系統會選擇一個頁面進行換出從而爲新進來的頁面騰出空間。若是要換出的頁面在內存中已經被修改,那麼必須將其寫到磁盤中以使磁盤副本保持最新狀態。若是頁面沒有被修改過,而且磁盤中的副本也已是最新的,那麼就不須要進行重寫。那麼就直接使用調入的頁面覆蓋須要移除的頁面就能夠了。 當發生缺頁中斷時,雖然能夠隨機的選擇一個頁面進行置換,可是若是每次都選擇一個不經常使用的頁面會提高系統的性能。若是一個常用的頁面被換出,那麼這個頁面在短期內又可能被重複使用,那麼就可能會形成額外的性能開銷。在關於頁面的主題上有不少頁面置換算法(page replacement algorithms),這些已經從理論上和實踐上獲得了證實。 須要指出的是,頁面置換問題在計算機的其餘領域中也會出現。例如,多數計算機把最近使用過的 32 字節或者 64 字節的存儲塊保存在一個或多個高速緩存中。當緩存滿的時候,一些塊就被選擇和移除。這些塊的移除除了花費時間較短外,這個問題同頁面置換問題徹底同樣。之因此花費時間較短,是由於丟掉的高速緩存能夠從內存中獲取,而內存沒有尋找磁道的時間也不存在旋轉延遲。 第二個例子是 Web 服務器。服務器會在內存中緩存一些常用到的 Web 頁面。然而,當緩存滿了而且已經引用了新的頁面,那麼必須決定退出哪一個 Web 頁面。在高速緩存中的 Web 頁面不會被修改。所以磁盤中的 Web 頁面常常是最新的,一樣的考慮也適用在虛擬內存中。在虛擬系統中,內存中的頁面可能會修改也可能不會修改。 下面咱們就來探討一下有哪些頁面置換算法。 **最優頁面置換算法** 最優的頁面置換算法很容易描述但在實際狀況下很難實現。它的工做流程以下:在缺頁中斷髮生時,這些頁面之一將在下一條指令(包含該指令的頁面)上被引用。其餘頁面則可能要到 十、100 或者 1000 條指令後纔會被訪問。每一個頁面均可以用在該頁首次被訪問前所要執行的指令數做爲標記。 最優化的頁面算法代表應該標記最大的頁面。若是一個頁面在 800 萬條指令內不會被使用,另一個頁面在 600 萬條指令內不會被使用,則置換前一個頁面,從而把須要調入這個頁面而發生的缺頁中斷推遲。計算機也像人類同樣,會把不肯意作的事情儘量的日後拖。 這個算法最大的問題時沒法實現。當缺頁中斷髮生時,操做系統沒法知道各個頁面的下一次將在何時被訪問。這種算法在實際過程當中根本不會使用。 **最近未使用頁面置換算法** 爲了可以讓操做系統收集頁面使用信息,大部分使用虛擬地址的計算機都有兩個狀態位,R 和 M,來和每一個頁面進行關聯。每當引用頁面(讀入或寫入)時都設置 R,寫入(即修改)頁面時設置 M,這些位包含在每一個頁表項中,就像下面所示 ![](https://s4.51cto.com/images/blog/202012/01/c4a992f2332373fe710fdf4d118e8bc3.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=) 由於每次訪問時都會更新這些位,所以由硬件來設置它們很是重要。一旦某個位被設置爲 1,就會一直保持 1 直到操做系統下次來修改此位。 若是硬件沒有這些位,那麼能夠使用操做系統的缺頁中斷和時鐘中斷機制來進行模擬。當啓動一個進程時,將其全部的頁面都標記爲不在內存;一旦訪問任何一個頁面就會引起一次缺頁中斷,此時操做系統就能夠設置 R 位(在它的內部表中),修改頁表項使其指向正確的頁面,並設置爲 READ ONLY 模式,而後從新啓動引發缺頁中斷的指令。若是頁面隨後被修改,就會發生另外一個缺頁異常。從而容許操做系統設置 M 位並把頁面的模式設置爲 READ/WRITE。 能夠用 R 位和 M 位來構造一個簡單的頁面置換算法:當啓動一個進程時,操做系統將其全部頁面的兩個位都設置爲 0。R 位按期的被清零(在每一個時鐘中斷)。用來將最近未引用的頁面和已引用的頁面分開。 當出現缺頁中斷後,操做系統會檢查全部的頁面,並根據它們的 R 位和 M 位將當前值分爲四類: * 第 0 類:沒有引用 R,沒有修改 M * 第 1 類:沒有引用 R,已修改 M * 第 2 類:引用 R ,沒有修改 M * 第 3 類:已被訪問 R,已被修改 M 儘管看起來好像沒法實現第一類頁面,可是當第三類頁面的 R 位被時鐘中斷清除時,它們就會發生。時鐘中斷不會清除 M 位,由於須要這個信息才能知道是否寫回磁盤中。清除 R 但不清除 M 會致使出現一類頁面。 NRU(Not Recently Used) 算法從編號最小的非空類中隨機刪除一個頁面。此算法隱含的思想是,在一個時鐘內(約 20 ms)淘汰一個已修改可是沒有被訪問的頁面要比一個大量引用的未修改頁面好,NRU 的主要優勢是易於理解而且可以有效的實現。 **先進先出頁面置換算法** 另外一種開銷較小的方式是使用 FIFO(First-In,First-Out) 算法,這種類型的數據結構也適用在頁面置換算法中。由操做系統維護一個全部在當前內存中的頁面的鏈表,最先進入的放在表頭,最新進入的頁面放在表尾。在發生缺頁異常時,會把頭部的頁移除而且把新的頁添加到表尾。 還記得缺頁異常何時發生嗎?咱們知道應用程序訪問內存會進行虛擬地址到物理地址的映射,缺頁異常就發生在虛擬地址沒法映射到物理地址的時候。由於實際的物理地址要比虛擬地址小不少(參考上面的虛擬地址和物理地址映射圖),因此缺頁常常會發生。 先進先出頁面多是最簡單的頁面替換算法了。在這種算法中,操做系統會跟蹤鏈表中內存中的全部頁。下面咱們舉個例子看一下(這個算法我剛開始看的時候有點懵逼,後來纔看懂,我仍是很菜) ![](https://s4.51cto.com/images/blog/202012/01/f915ec9428a3073768b47082bb743769.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=) * 初始化的時候,沒有任何頁面,因此第一次的時候會檢查頁面 1 是否位於鏈表中,沒有在鏈表中,那麼就是 MISS,頁面1 進入鏈表,鏈表的先進先出的方向如圖所示。 * 相似的,第二次會先檢查頁面 2 是否位於鏈表中,沒有在鏈表中,那麼頁面 2 進入鏈表,狀態爲 MISS,依次類推。 * 咱們來看第四次,此時的鏈表爲 1 2 3,第四次會檢查頁面 2 是否位於鏈表中,通過檢索後,發現 2 在鏈表中,那麼狀態就是 HIT,並不會再進行入隊和出隊操做,第五次也是同樣的。 * 下面來看第六次,此時的鏈表仍是 1 2 3,由於以前沒有執行進入鏈表操做,頁面 5 會首先進行檢查,發現鏈表中沒有頁面 5 ,則執行頁面 5 的進入鏈表操做,頁面 2 執行出鏈表的操做,執行完成後的鏈表順序爲 2 3 5。 **第二次機會頁面置換算法** 咱們上面學到的 FIFO 鏈表頁面有個缺陷,那就是出鏈和入鏈並不會進行 check 檢查,這樣就會容易把常用的頁面置換出去,爲了不這一問題,咱們對該算法作一個簡單的修改:咱們檢查最老頁面的 R 位,若是是 0 ,那麼這個頁面就是最老的並且沒有被使用,那麼這個頁面就會被馬上換出。若是 R 位是 1,那麼就清除此位,此頁面會被放在鏈表的尾部,修改它的裝入時間就像剛放進來的同樣。而後繼續搜索。 這種算法叫作 第二次機會(second chance)算法,就像下面這樣,咱們看到頁面 A 到 H 保留在鏈表中,並按到達內存的時間排序。 ![](https://s4.51cto.com/images/blog/202012/01/e949e5430ec0fa0fa0b57311cb47cc2b.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=) 假設缺頁異常發生在時刻 20 處,這時最老的頁面是 A ,它是在 0 時刻到達的。若是 A 的 R 位是 0,那麼它將被淘汰出內存,或者把它寫回磁盤(若是它已經被修改過),或者只是簡單的放棄(若是它是未被修改過)。另外一方面,若是它的 R 位已經設置了,則將 A 放到鏈表的尾部而且從新設置裝入時間爲當前時刻(20 處),而後清除 R 位。而後從 B 頁面開始繼續搜索合適的頁面。 尋找第二次機會的是在最近的時鐘間隔中未被訪問過的頁面。若是全部的頁面都被訪問過,該算法就會被簡化爲單純的 FIFO 算法。具體來講,假設圖 a 中全部頁面都設置了 R 位。操做系統將頁面依次移到鏈表末尾,每次都在添加到末尾時清除 R 位。最後,算法又會回到頁面 A,此時的 R 位已經被清除,那麼頁面 A 就會被執行出鏈處理,所以算法可以正常結束。 **時鐘頁面置換算法** 即便上面提到的第二次頁面置換算法也是一種比較合理的算法,但它常常要在鏈表中移動頁面,既下降了效率,並且這種算法也不是必須的。一種比較好的方式是把全部的頁面都保存在一個相似鐘面的環形鏈表中,一個錶針指向最老的頁面。以下圖所示 ![](https://s4.51cto.com/images/blog/202012/01/38ab286dec3cb2981df6898b0587c38e.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=) 當缺頁錯誤出現時,算法首先檢查錶針指向的頁面,若是它的 R 位是 0 就淘汰該頁面,並把新的頁面插入到這個位置,而後把錶針向前移動一位;若是 R 位是 1 就清除 R 位並把錶針前移一個位置。重複這個過程直到找到了一個 R 位爲 0 的頁面位置。瞭解這個算法的工做方式,就明白爲何它被稱爲 時鐘(clokc)算法了。 **最近最少使用頁面置換算法** 最近最少使用頁面置換算法的一個解釋會是下面這樣:在前面幾條指令中頻繁使用的頁面和可能在後面的幾條指令中被使用。反過來講,已經好久沒有使用的頁面有可能在將來一段時間內仍不會被使用。這個思想揭示了一個能夠實現的算法:在缺頁中斷時,置換未使用時間最長的頁面。這個策略稱爲 LRU(Least Recently Used) ,最近最少使用頁面置換算法。 雖然 LRU 在理論上是能夠實現的,可是從長遠看來代價比較高。爲了徹底實現 LRU,會在內存中維護一個全部頁面的鏈表,最頻繁使用的頁位於表頭,最近最少使用的頁位於表尾。困難的是在每次內存引用時更新整個鏈表。在鏈表中找到一個頁面,刪除它,而後把它移動到表頭是一個很是耗時的操做,即便使用硬件來實現也是同樣的費時。 然而,還有其餘方法能夠經過硬件實現 LRU。讓咱們首先考慮最簡單的方式。這個方法要求硬件有一個 64 位的計數器,它在每條指令執行完成後自動加 1,每一個頁表必須有一個足夠容納這個計數器值的域。在每次訪問內存後,將當前的值保存到被訪問頁面的頁表項中。一旦發生缺頁異常,操做系統就檢查全部頁表項中計數器的值,找到值最小的一個頁面,這個頁面就是最少使用的頁面。 **用軟件模擬 LRU** 儘管上面的 LRU 算法在原則上是能夠實現的,可是不多有機器可以擁有那些特殊的硬件。上面是硬件的實現方式,那麼如今考慮要用軟件來實現 LRU 。一種能夠實現的方案是 NFU(Not Frequently Used,最不經常使用)算法。它須要一個軟件計數器來和每一個頁面關聯,初始化的時候是 0 。在每一個時鐘中斷時,操做系統會瀏覽內存中的全部頁,會將每一個頁面的 R 位(0 或 1)加到它的計數器上。這個計數器大致上跟蹤了各個頁面訪問的頻繁程度。當缺頁異常出現時,則置換計數器值最小的頁面。 NFU 最主要的問題是它不會忘記任何東西,想一下是否是這樣?例如,在一個屢次(掃描)的編譯器中,在第一遍掃描中頻繁使用的頁面會在後續的掃描中也有較高的計數。事實上,若是第一次掃描的執行時間剛好是各次掃描中最長的,那麼後續遍歷的頁面的統計次數總會比第一次頁面的統計次數小。結果是操做系統將置換有用的頁面而不是再也不使用的頁面。 幸運的是隻須要對 NFU 作一個簡單的修改就可讓它模擬 LRU,這個修改有兩個步驟 * 首先,在 R 位被添加進來以前先把計數器右移一位; * 第二步,R 位被添加到最左邊的位而不是最右邊的位。 修改之後的算法稱爲 老化(aging) 算法,下圖解釋了老化算法是如何工做的。 ![](https://s4.51cto.com/images/blog/202012/01/d1663b1bfa1ec29af984edbfd1435b6f.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=) 咱們假設在第一個時鐘週期內頁面 0 - 5 的 R 位依次是 1,0,1,0,1,1,(也就是頁面 0 是 1,頁面 1 是 0,頁面 2 是 1 這樣類推)。也就是說,在 0 個時鐘週期到 1 個時鐘週期之間,0,2,4,5 都被引用了,從而把它們的 R 位設置爲 1,剩下的設置爲 0 。在相關的六個計數器被右移以後 R 位被添加到 左側 ,就像上圖中的 a。剩下的四列顯示了接下來的四個時鐘週期內的六個計數器變化。 CPU正在以某個頻率前進,該頻率的週期稱爲時鐘滴答或時鐘週期。一個 100Mhz 的處理器每秒將接收100,000,000個時鐘滴答。 當缺頁異常出現時,將置換(就是移除)計數器值最小的頁面。若是一個頁面在前面 4 個時鐘週期內都沒有被訪問過,那麼它的計數器應該會有四個連續的 0 ,所以它的值確定要比前面 3 個時鐘週期內都沒有被訪問過的頁面的計數器小。 這個算法與 LRU 算法有兩個重要的區別:看一下上圖中的 e,第三列和第五列 ![](https://s4.51cto.com/images/blog/202012/01/fc085cb7de185d81c4e6535d6824bd0e.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=) 它們在兩個時鐘週期內都沒有被訪問過,在此以前的時鐘週期內都引用了兩個頁面。根據 LRU 算法,若是須要置換的話,那麼應該在這兩個頁面中選擇一個。那麼問題來了,我萌應該選擇哪一個?如今的問題是咱們不知道時鐘週期 1 到時鐘週期 2 內它們中哪一個頁面是後被訪問到的。由於在每一個時鐘週期內只記錄了一位,因此沒法區分在一個時鐘週期內哪一個頁面最先被引用,哪一個頁面是最後被引用的。所以,咱們能作的就是置換頁面3,由於頁面 3 在週期 0 - 1 內都沒有被訪問過,而頁面 5 卻被引用過。 LRU 與老化以前的第 2 個區別是,在老化期間,計數器具備有限數量的位(這個例子中是 8 位),這就限制了以往的訪問記錄。若是兩個頁面的計數器都是 0 ,那麼咱們能夠隨便選擇一個進行置換。實際上,有可能其中一個頁面的訪問次數實在 9 個時鐘週期之前,而另一個頁面是在 1000 個時鐘週期以前,可是咱們卻沒法看到這些。在實際過程當中,若是時鐘週期是 20 ms,8 位通常是夠用的。因此咱們常常拿 20 ms 來舉例。 **工做集頁面置換算法** 在最單純的分頁系統中,剛啓動進程時,在內存中並無頁面。此時若是 CPU 嘗試匹配第一條指令,就會獲得一個缺頁異常,使操做系統裝入含有第一條指令的頁面。其餘的錯誤好比 全局變量和 堆棧 引發的缺頁異常一般會緊接着發生。一段時間之後,進程須要的大部分頁面都在內存中了,此時進程開始在較少的缺頁異常環境中運行。這個策略稱爲 請求調頁(demand paging),由於頁面是根據須要被調入的,而不是預先調入的。 在一個大的地址空間中系統的讀全部的頁面,將會形成不少缺頁異常,所以會致使沒有足夠的內存來容納這些頁面。不過幸運的是,大部分進程不是這樣工做的,它們都會以局部性方式(locality of reference) 來訪問,這意味着在執行的任何階段,程序只引用其中的一小部分。 一個進程當前正在使用的頁面的集合稱爲它的 工做集(working set),若是整個工做集都在內存中,那麼進程在運行到下一運行階段(例如,編譯器的下一遍掃面)以前,不會產生不少缺頁中斷。若是內存過小從而沒法容納整個工做集,那麼進程的運行過程當中會產生大量的缺頁中斷,會致使運行速度也會變得緩慢。由於一般只須要幾納秒就能執行一條指令,而一般須要十毫秒才能從磁盤上讀入一個頁面。若是一個程序每 10 ms 只能執行一到兩條指令,那麼它將須要很長時間才能運行完。若是隻是執行幾條指令就會產生中斷,那麼就稱做這個程序產生了 顛簸(thrashing)。 在多道程序的系統中,一般會把進程移到磁盤上(即從內存中移走全部的頁面),這樣可讓其餘進程有機會佔用 CPU 。有一個問題是,當進程想要再次把以前調回磁盤的頁面調回內存怎麼辦?從技術的角度上來說,並不須要作什麼,此進程會一直產生缺頁中斷直到它的工做集 被調回內存。而後,每次裝入一個進程須要 20、100 甚至 1000 次缺頁中斷,速度顯然太慢了,而且因爲 CPU 須要幾毫秒時間處理一個缺頁中斷,所以由至關多的 CPU 時間也被浪費了。 所以,很多分頁系統中都會設法跟蹤進程的工做集,確保這些工做集在進程運行時被調入內存。這個方法叫作 工做集模式(working set model)。它被設計用來減小缺頁中斷的次數的。在進程運行前首先裝入工做集頁面的這一個過程被稱爲 預先調頁(prepaging),工做集是隨着時間來變化的。 根據研究代表,大多數程序並非均勻的訪問地址空間的,而訪問每每是集中於一小部分頁面。一次內存訪問可能會取出一條指令,也可能會取出數據,或者是存儲數據。在任一時刻 t,都存在一個集合,它包含所喲歐最近 k 次內存訪問所訪問過的頁面。這個集合 w(k,t) 就是工做集。由於最近 k = 1次訪問確定會訪問最近 k > 1 次訪問所訪問過的頁面,因此 w(k,t) 是 k 的單調遞減函數。隨着 k 的增大,w(k,t) 是不會無限變大的,由於程序不可能訪問比所能容納頁面數量上限還多的頁面。 ![](https://s4.51cto.com/images/blog/202012/01/a3ece6f02a12cf2bf355832d115bcd99.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=) 事實上大多數應用程序只會任意訪問一小部分頁面集合,可是這個集合會隨着時間而緩慢變化,因此爲何一開始曲線會快速上升而 k 較大時上升緩慢。爲了實現工做集模型,操做系統必須跟蹤哪些頁面在工做集中。一個進程從它開始執行到當前所實際使用的 CPU 時間總數一般稱做 當前實際運行時間。進程的工做集能夠被稱爲在過去的 t 秒實際運行時間中它所訪問過的頁面集合。 下面來簡單描述一下工做集的頁面置換算法,基本思路就是找出一個不在工做集中的頁面並淘汰它。下面是一部分機器頁表 ![](https://s4.51cto.com/images/blog/202012/01/17c0c832d5b56c3c14992e631d367cb7.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=) 由於只有那些在內存中的頁面才能夠做爲候選者被淘汰,因此該算法忽略了那些不在內存中的頁面。每一個表項至少包含兩條信息:上次使用該頁面的近似時間和 R(訪問)位。空白的矩形表示該算法不須要其餘字段,例如頁框數量、保護位、修改位。 算法的工做流程以下,假設硬件要設置 R 和 M 位。一樣的,在每一個時鐘週期內,一個週期性的時鐘中斷會使軟件清除 Referenced(引用)位。在每一個缺頁異常,頁表會被掃描以找出一個合適的頁面把它置換。 隨着每一個頁表項的處理,都須要檢查 R 位。若是 R 位是 1,那麼就會將當前時間寫入頁表項的 上次使用時間域,表示的意思就是缺頁異常發生時頁面正在被使用。由於頁面在當前時鐘週期內被訪問過,那麼它應該出如今工做集中而不是被刪除(假設 t 是橫跨了多個時鐘週期)。 若是 R 位是 0 ,那麼在當前的時鐘週期內這個頁面沒有被訪問過,應該做爲被刪除的對象。爲了查看是否應該將其刪除,會計算其使用期限(當前虛擬時間 - 上次使用時間),來用這個時間和 t 進行對比。若是使用期限大於 t,那麼這個頁面就再也不工做集中,而使用新的頁面來替換它。而後繼續掃描更新剩下的表項。 然而,若是 R 位是 0 可是使用期限小於等於 t,那麼此頁應該在工做集中。此時就會把頁面臨時保存起來,可是會記生存時間最長(即上次使用時間的最小值)的頁面。若是掃描完整個頁表卻沒有找到適合被置換的頁面,也就意味着全部的頁面都在工做集中。在這種狀況下,若是找到了一個或者多個 R = 0 的頁面,就淘汰生存時間最長的頁面。最壞的狀況下是,在當前時鐘週期內,全部的頁面都被訪問過了(也就是都有 R = 1),所以就隨機選擇一個頁面淘汰,若是有的話最好選一個未被訪問的頁面,也就是乾淨的頁面。 **工做集時鐘頁面置換算法** 當缺頁異常發生後,須要掃描整個頁表才能肯定被淘汰的頁面,所以基本工做集算法仍是比較浪費時間的。一個對基本工做集算法的提高是基於時鐘算法可是卻使用工做集的信息,這種算法稱爲WSClock(工做集時鐘)。因爲它的實現簡單而且具備高性能,所以在實踐中被普遍應用。 與時鐘算法同樣,所需的數據結構是一個以頁框爲元素的循環列表,就像下面這樣 ![](https://s4.51cto.com/images/blog/202012/01/2ab8d0655072e6061969550767350935.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=) 工做集時鐘頁面置換算法的操做:a) 和 b) 給出 R = 1 時所發生的情形;c) 和 d) 給出 R = 0 的例子 最初的時候,該表是空的。當裝入第一個頁面後,把它加載到該表中。隨着更多的頁面的加入,它們造成一個環形結構。每一個表項包含來自基本工做集算法的上次使用時間,以及 R 位(已標明)和 M 位(未標明)。 與時鐘算法同樣,在每一個缺頁異常時,首先檢查指針指向的頁面。若是 R 位被是設置爲 1,該頁面在當前時鐘週期內就被使用過,那麼該頁面就不適合被淘汰。而後把該頁面的 R 位置爲 0,指針指向下一個頁面,並重復該算法。該事件序列化後的狀態參見圖 b。 如今考慮指針指向的頁面 R = 0 時會發生什麼,參見圖 c,若是頁面的使用期限大於 t 而且頁面爲被訪問過,那麼這個頁面就不會在工做集中,而且在磁盤上會有一個此頁面的副本。申請從新調入一個新的頁面,並把新的頁面放在其中,如圖 d 所示。另外一方面,若是頁面被修改過,就不能從新申請頁面,由於這個頁面在磁盤上沒有有效的副本。爲了不因爲調度寫磁盤操做引發的進程切換,指針繼續向前走,算法繼續對下一個頁面進行操做。畢竟,有可能存在一個老的,沒有被修改過的頁面能夠當即使用。 原則上來講,全部的頁面都有可能由於磁盤I/O 在某個時鐘週期內被調度。爲了下降磁盤阻塞,須要設置一個限制,即最大隻容許寫回 n 個頁面。一旦達到該限制,就不容許調度新的寫操做。 那麼就有個問題,指針會繞一圈回到原點的,若是回到原點,它的起始點會發生什麼?這裏有兩種狀況: * 至少調度了一次寫操做 * 沒有調度過寫操做 在第一種狀況中,指針僅僅是不停的移動,尋找一個未被修改過的頁面。因爲已經調度了一個或者多個寫操做,最終會有某個寫操做完成,它的頁面會被標記爲未修改。置換遇到的第一個未被修改過的頁面,這個頁面不必定是第一個被調度寫操做的頁面,由於硬盤驅動程序爲了優化性能可能會把寫操做重排序。 對於第二種狀況,全部的頁面都在工做集中,不然將至少調度了一個寫操做。因爲缺少額外的信息,最簡單的方法就是置換一個未被修改的頁面來使用,掃描中須要記錄未被修改的頁面的位置,若是不存在未被修改的頁面,就選定當前頁面並把它寫回磁盤。 **頁面置換算法小結** 咱們到如今已經研究了各類頁面置換算法,如今咱們來一個簡單的總結,算法的總結概括以下 ![](https://s4.51cto.com/images/blog/202012/01/3935977cba7e858a2f07d7b816706d1a.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=) * 最優算法在當前頁面中置換最後要訪問的頁面。不幸的是,沒有辦法來斷定哪一個頁面是最後一個要訪問的,所以實際上該算法不能使用。然而,它能夠做爲衡量其餘算法的標準。 * NRU 算法根據 R 位和 M 位的狀態將頁面氛圍四類。從編號最小的類別中隨機選擇一個頁面。NRU 算法易於實現,可是性能不是很好。存在更好的算法。 * FIFO 會跟蹤頁面加載進入內存中的順序,並把頁面放入一個鏈表中。有可能刪除存在時間最長可是還在使用的頁面,所以這個算法也不是一個很好的選擇。 * 第二次機會算法是對 FIFO 的一個修改,它會在刪除頁面以前檢查這個頁面是否仍在使用。若是頁面正在使用,就會進行保留。這個改進大大提升了性能。 * 時鐘 算法是第二次機會算法的另一種實現形式,時鐘算法和第二次算法的性能差很少,可是會花費更少的時間來執行算法。 * LRU 算法是一個很是優秀的算法,可是沒有特殊的硬件(TLB)很難實現。若是沒有硬件,就不能使用 LRU 算法。 * NFU 算法是一種近似於 LRU 的算法,它的性能不是很是好。 * 老化 算法是一種更接近 LRU 算法的實現,而且能夠更好的實現,所以是一個很好的選擇 * 最後兩種算法都使用了工做集算法。工做集算法提供了合理的性能開銷,可是它的實現比較複雜。WSClock 是另一種變體,它不只可以提供良好的性能,並且能夠高效地實現。 總之,最好的算法是老化算法和WSClock算法。他們分別是基於 LRU 和工做集算法。他們都具備良好的性能而且可以被有效的實現。還存在其餘一些好的算法,但實際上這兩個多是最重要的。