【知識強化】第三章 內存管理 3.1 內存管理概念


其實內存它的作用就是用來存放數據。我們的程序本來是放在外存、放在磁盤當中的,但是磁盤的讀寫速度很慢,而CPU的處理速度又很快,所以如果CPU要執行這個程序,程序相關的數據都是從外存讀入的,那麽很顯然CPU的這個速度會被外存的速度給拖累。所以爲了緩和這個CPU和硬盤、外存之間的速度矛盾,所以我們必須先把我們要執行的、CPU要處理的這些程序數據把它放入內存裏。既然我們的內存是存放數據的,那麽我們的內存當中可能會存放很多很多數據,那操作系統是怎麽區分各個程序的數據是放在什麽地方的呢?那爲了區分這些數據存放的位置,就需要給內存進行一個地址的編號。就有點類似於說我們去住酒店的時候,怎么區分我們每個人住在哪個房間?其實很簡單,酒店的做法就是給每個房間編號,那我們的內存其實和這個酒店是一樣的,只不過酒店的這些房間裏你可以存的是人,而內存當中,它的這些“小房間”裏,它存的是一個一個的數據。那內存會被劃分成這樣一個一個的“小房間”,每個小房間就是一個存儲單元。那接下來在劃分的這些存儲單元之後,就需要給這些存儲單元進行一個編號。那內存的這個地址編號一般來説是從零開始的,然後依次遞增。並且每個地址會對應一個數據的存儲單元,也就是會對應一個“小房間”。那麽,這樣的一個存儲單元可以存放多少數據呢?這個具體得看計算機的編址方式。我們在操作系統這門課當中大部分遇到的情況是會告訴你說計算機按字節編址,按字節編址的意思就是一個地址它對應的是一個字節的數據,也就是說這樣的一個存儲單元,它可以存放一個字節,而一個字節它又由8個二進制位組成,也就是8個0101這樣組成。那在有的題目當中也有可能會告訴我們這個計算機是按字編址的,如果它告訴我們是按字編址的話,那麽就意味著一個地址它所對應的存儲單元可以存放一個字,而一個字的長度是多少個比特位?這個具體得看題目當中給出的條件。有的計算機當中字長是16位,那麽它一個字的大小就是16個比特。也有的計算機可能字長是32位,字長是64位等等。總之,我們需要根據題目給的條件來判斷一個字它占有多少個比特位。好的,那麽在這個部分我們為大家介紹了內存的一些最基本的知識。什麽叫做存儲單元,就是用於存放數據的最小單元。另外,每一個地址可以對應這樣的一個存儲單元。而一個存儲單元可以存儲多少數據,那具體要看這個計算機它是怎麽設計的。對於我們考研來説,我們就要看它題目給的條件到底是什麽。

那在內存管理這個章節當中,可能會有很多題目會涉及到這個數據的一些基本單位。而對於不考計組的同學來説可能對這些單位的描述是比較陌生的,所以我們在這個地方還需要再介紹一下一些常見的單位。比如說我們平時所説的一個手機,或者說一台電腦它有4GB內存,那除了GB之外,我們還經常看到什麽MB,KB這樣的單位。那所謂的1KB,其實就是2的10次方這麽多。而1MB,其實是2的20次方這麽多。而這裏的1GB,其實是2的30次方那麽多。所以這個地方4G其實它是一個數量,而B是一個數據的單位,這個大BByte指的是字節,小b小寫的b它指的是bit,是一個比特位,一個二進制位。一個Byte也就是一個大B等於8個小b,所以如果一個手機有4GB內存的話那麽就意味著這個手機的內存當中它可以存放4*2^30這麽多個字節的數據。所以如果這個手機或者這個電腦它是按字節編址的,那麽這個內存的地址空間就應該是4*2^30這麽多個存儲單元。每一個存儲單元可以存放一個字節。那我們知道,在計算機的世界當中,所有的這些數字其實都是用二進制0101這樣來表示的。包括我們的內存地址,其實也需要用二進制來表示。所以有的題目當中可能會告訴我們,內存的大小是多少。比如說內存大小是4GB,並且告訴我們這個內存是按字節編址的,題目可能會問我們到底需要多少個二進制位才能表示這麽多個存儲單元也就是2^32次方個存儲單元。那對於跨考的同學來說,一定要去了解一下二進制編碼還有二進制數和這個十進制數的一個轉換關係。對二進制比較熟悉的就可以很快速地反應出來這麽多個存儲單元肯定就需要32個二進制位來表示。所以如果手機的內存是4GB,並且它是按字節編址的,那麽對於這個手機來説它的地址至少需要用32個二進制位來表示。好的那麽再次提醒,對於跨考的同學來說,如果二進制和十進制的這個轉換不是很熟練的話,一定要下去練習。

在瞭解了內存的作用、內存的存儲單元、內存的地址這些概念之後,我們再結合之前我們提到過的一些基礎再給大家更進一步深入地講解一下指令工作的具體原理。這個知識點有助於大家對後面的那些內容的更深入的理解。那我們之前的學習當中提到過,其實我們用高級語言編程的代碼經過編譯之後,會形成與它對應的等價的一系列的機器語言指令。每一條指令就是讓CPU幹一件具體的事情。比如說我們用C語言寫的x=x+1;這樣一個很簡單的操作,經過編譯之後可能會形成這樣的三條與它對等的機器指令。那當這個程序運行的時候,系統會爲它建立相應的進程,而我們之前學到過一個進程在內存當中會有一片區域叫做程序段就是用於存放這個進程相關的那些代碼指令的。另外還有一個部分叫做數據段,數據段就是用來存放這個程序所處理的一些變量啊之類的數據。比如說我們這兒的x變量,它就是存放在所謂的數據段裏。那我們來看一下這三條指令分別代表著什麽呢?CPU在執行這幾條指令的時候首先它取出了指令1,然後指令1它發現由這樣的幾個部分組成。第一個部分紅色的這個部分叫做操作碼,就是指明了這條指令是要幹一件什麽事情。那這個地方的二進制碼我只是胡亂寫的,大家只需要理解它的原理就可以了。那我們假設這個什麽101100它代表的是讓CPU做一個數據傳送的事情。那後面這兩段數據又是指明了這個操作相關的一些必要的參數。比如說我們的指令1就是讓CPU從內存地址01001111把這個地方存放的數據把它取到對應十進制就是編號為3的這個寄存器當中。所以CPU在執行這個指令的時候,它就知道我現在要做的事情是要做數據的傳送。那怎麽傳送呢?我需要從地址為79的這個內存單元當中,把它裏面的數據取出來,然後把它放到編號為3的這個寄存器當中。所以指令1的執行就會導致編號為3的這個寄存器當中有了10這個數。把x的值放到了這個寄存器中。那在執行了指令1之後,CPU就會開始執行指令2。

同樣的,它會解析這個指令2到底是要幹一件什麽事情。根據它前面的這個部分,也就是所謂的操作碼,它可以判斷出這個指令是要做一個加法操作,加法運算。而怎麽加呢?CPU需要把編號為00000011也就是換成十進制的話也就是編號為3的這個寄存器當中的內容加上1,所以根據這條指令CPU會把這個寄存器當中的值從10加1,也就是變成11。

 

 

那再接下來它又執行的是第三條指令。這個指令同樣是一個數據傳送的指令。可以看到它的這個操作碼和第一個指令的操作碼是一樣的,就説明這兩條指令它們要幹的是同一個事情,是同一種指令。只不過它們的參數是不一樣的,大家可以對比著來看一下。那這個指令3是讓CPU幹這樣的一個事情。它需要把編號為3的這個寄存器當中的內容,把它寫回編號為01001111這個內存單元當中,所以CPU在執行第三條指令的時候,就會把這個寄存器當中的內容把它寫回x這個變量在內存當中存放的那個地址。因此這就完成了x=x+1;這樣的一個操作。當然剛才我們講的這三條指令只是我自己胡亂寫的,其實並不嚴謹。如果大家想要了解這些指令真正的什麽操作碼啊參數啊到底是什麽樣一種規範,那還需要學習計算機組成原理。但是對於不考那門課的同學來説,只要理解到這一步就差不多了。其實CPU在執行這些一條一條指令的過程當中,它就是在處理這些內存啊或者寄存器當中的數據,而怎麽處理這些數據,怎麽找到這些數據呢?它就是基於地址這個很重要的概念來進行的。我們的內存會有它自己的一些地址編址,同樣的我們的寄存器也會有一些它自己的地址編址。總之我們的程序經過編譯之後,會形成一系列等價的機器指令。在這個機器指令當中它會有一些相應的參數,告訴CPU你應該去哪些地址去讀數據,或者往哪些地址寫數據。那在剛才我們講的這個例子當中,我們默認了我們所提到的這個進程它是從0這個地址開始連續存放的。所以在它的這個指令當中,是直接指明了各個變量的存放位置。比如說x的存放地址,它就直接把它寫死在了這個指令裏。它是存放在79這個地址所對應的存儲單元裏的。那接下來我們要思考的一個問題是這樣的,如果我們的這個地址它不是從零開始存放的,而是從別的地址開始存放的,會不會導致我們的這個進程的運行出現一些問題呢?我們來具體看一下。

這個可執行文件在Windows系統當中就是.exe,這個可執行文件又可以稱作為裝入模塊。這個概念我們之後還會具體細聊。總之我們形成了這個裝入模塊,形成了這個可執行文件之後,就可以把這個可執行文件放入內存裏然後就開始執行這個程序了。不過需要注意的是,我們所形成的這個可執行文件,它的這些指令當中所指明的這些地址參數,其實指的是一個邏輯地址,一個相對地址。所謂的相對地址就是指,這個地址指的是它相對於這個進程的起始地址而言的地址。有點繞,不過其實並不難理解,在之前的那個例子當中,我們是默認了這個進程它相關的這些數據是從內存地址為零這個地方開始存放的。所以這條指令它是要進行x這個變量的初始化,並且它指明了x這個變量它存放的地址是79,它的初始值為10,所以CPU在執行這條指令的時候,它會往79這個地址所對應的內存單元裏寫入x的初始值10,那這是我們剛才提到的情況。我們的這個程序裝入模塊,它是從內存地址為零這個地方開始往後依次存放的,所以我們的指令當中指明的這些地址並不會出現什麽問題。

那接下來再來看另一種情況。假設我們的這個程序的裝入模塊,它裝入內存的時候,並不是從地址為零的地方開始的,而是從地址為100的這個地方開始的。那麽這就意味著操作系統給這個進程、給這個程序分配的地址空間其實是一百到279這麽多,所以如果是這種情況的話,這個程序的這些邏輯地址和它最終存放的物理地址就會出現對應不上的情況。比如説我們的指令零是要給x這個變量進行初始化,但是這個指令指明了x這個變量的值它是要寫到地址為79的那個內存單元當中的,所以如果CPU執行這條指令的話,

它就會把x的值10把它寫在上面的這個地方,79這個地址所對應的內存單元裏。而這上面的這一片內存空間,極有可能是分配給其他進程的,所以也就意味著本來是這個進程它自己的數據然而它強行往其他進程的那個地址空間裏去寫入了自己的數據。那這顯然是一個危險的並且應該被阻止的一種行爲。而事實上在這個例子當中,我們期待的x這個變量的正確的存放位置,應該是從它的這個起始位置開始往後79個單位這樣的一個內存單元裏,也就是179這個地址所對應的內存單元當中。如果x的值寫在這兒,那就是沒問題的。相信大家對邏輯地址和物理地址應該有一個比較直觀的體會了。總之我們的程序它編譯鏈接等等之後,所形成的這些指令當中一般來説使用的是邏輯地址,也就是相對地址。而這個程序最終被裝到內存的什麽位置,這個其實是我們沒辦法確定的,所以在內存管理這個章節當中有一個很重要的我們需要解決的問題就是如何把這些指令當中所指明的這些邏輯地址把它轉換為最終的物理地址、正確的物理地址。那這個小節當中我們會介紹三種策略來解決這個地址轉換的問題。這三種策略分別是絕對裝入、可重定位裝入(靜態重定位)和動態運行時裝入(動態重定位)。那我們會依次來看一下這三種策略是怎麽解決這個問題的。

首先來看第一種策略,絕對裝入。所謂的“絕對裝入”就是指,如果我們能夠在程序放入內存之前就知道這個程序會從哪個位置開始存放,那在這種情況下我們其實就可以讓編譯程序把各個變量存放的那些地址直接把它修改成一個正確的一個絕對地址。那還是以剛才的那個例子為例。比如說我們先前就已經知道了我們的那個裝入模塊它是要從地址為100的地方開始存放的,那麽按照之前我們的介紹來説,這個裝入模塊它裏面所使用到的這些地址都是相對地址,但是如果我們知道它是從100這個地址開始裝入的,

那其實在編譯的時候就可以由編譯器把它改爲正確的地址。比如按照之前的分析我們知道,x那個變量它正確的存放地址應該是179。所以接下來我們把這個裝入模塊從起始地址為100的這個地方開始裝入,那麽當這個程序運行的時候就可以把它的這些變量存放到一個正確的位置了,所以這是第一種方式。在編譯的那個時候,就把邏輯地址轉換成最終的物理地址。但是有一個前提就是我們需要知道我們的裝入模塊它會裝到內存的哪個位置,從什麽地方開始裝。所以這種方式的靈活性其實很差,它只適用於單道程序的環境,也就是早期的還沒有操作系統的那個階段,使用的就是這樣的一種方式。大家可以想一下,如果采用絕對裝入這種方式的話,那麽假設我的這個可執行文件此時要運行在另外一臺電腦當中,而另一臺電腦當中又不能讓它從100這個位置開始存放,那是不是就意味著這個程序換一臺電腦它就不能執行了,所以這種方式它的靈活性是特別低的。

第二種裝入方式叫做可重定位裝入,又叫靜態重定位方式。如果采用這種方式的話,那麽編譯、鏈接最終形成的這個裝入模塊這些指令當中使用的地址依然是從0開始的邏輯地址,也就是相對地址。而把這個地址重定位這個過程是留在了裝入模塊裝入內存的時候進行。比如說這個裝入模塊裝到內存裏之後,它的起始物理地址是100,那麽如果我們采用的是靜態重定位這種方式的話,就意味著在這個程序裝入內存的時候,我們同時還需要把這個程序當中所涉及的所有的這些和地址相關的參數都把它進行加100的操作。比如說指令0我們就需要把它加100,然後指令1也對79這個內存單元進行了操作,所以這個地址我們也需要把它加100。所以靜態重定位這種方式就是在我們的程序裝入內存的時候再進行這個地址的轉換。那這種方式的特點是我們給這個作業分配的這些地址空間必須是連續的,並且這個作業必須一次全部裝入內存。也就是說在它執行之前就必須給它分配它所需要的全部的內存空間。難道還可以只分配它所需要的部分空間嗎?那這個問題大家在學習了之後的虛擬存儲技術之後就會有更深入的了解。並且這個地方其實也不是特別重要。那靜態重定位這種方式它還有一個特點就是,在這個程序運行期間它是不可以移動的。這個很好理解,因爲我們的這些指令當中已經寫死了我們具體要操作那個物理地址到底是多少。如果這個程序這個進程相關的這一系列的數據發生了移動的話,那麽這個地址的指向又會發生錯誤。所以這是靜態重定位這種方式的一個局限性。

那最後我們來看一下現代的系統使用的這種地址轉換的機制,叫做動態重定位,又叫動態運行時裝入。那如果采用的是這種方式的話,程序經過編譯鏈接最後形成的裝入模塊當中,它這些指令所使用的其實也是邏輯地址也就是相對地址。並且這個可執行文件這個程序在裝入內存的時候,它們的這個指令當中所使用的同樣還是邏輯地址。如果一個系統支持這種動態重定位方式的話,那這個系統當中還需要設置一個專門的一個寄存器叫做重定位寄存器。重定位寄存器當中存放了這個進程,或者說這個作業它在內存當中的起始地址是多少,比如說我們的這個程序這個進程它是從起始地址為100的這個地方開始存放的,所以重定位寄存器當中我們就存放它的起始地址100。而當CPU在執行相應的這些指令的時候,比如說它在執行指令0的時候,這個指令0是讓他往地址為79的存儲單元當中寫入x這個變量的初始值10。CPU在對一個內存地址進行訪問的時候,它會做這樣的事情。它把邏輯地址和重定位寄存器當中存放的這個起始地址進行一個相加的操作,然後加出來的這個地址才是最終它可以訪問的地址。所以經過這樣的一步處理它就知道,指令0是讓它往地址為179的這個地方寫入數據10。那很顯然如果采用這種方式的話,我們想讓進程的數據在運行的過程當中發生移動是很方便的。比如說我們把這個進程的數據把它移到從200開始的話,那很簡單。我們只需要把重定位寄存器的值再修改成200就可以了,所以動態重定位方式有很多很多的優點。

它可以把程序分配到不連續的存儲區。那不連續的分配這個現在先不展開,經過后續的學習大家會有更深入的理解,這兒先簡單提一下。那這些內容現在還可能都看不懂,我們在學習了之后的虛擬存儲管理之后就可以對這個特性有更深入的理解了。那這個地方我們也暫時不展開,把這個點的理解往后挪一挪。

好的那么剛才我們介紹了內存的基本知識,介紹了內存的地址,介紹了什么叫邏輯地址什么叫物理地址,並且也介紹了三種裝入方式來解決了邏輯地址到物理地址的轉換這樣的一個過程。那接下來我們再從一個更宏觀更全局的這樣的一個角度再來看一下我們從寫程序到程序運行它所經歷的步驟。目標模塊文件在C語言里就是.o文件。並且這些目標模塊當中其實已經包含了這些代碼所對應的那些指令了,而這些指令的編址,都是一個邏輯地址也就是相對地址。每一個模塊的編址都是從邏輯地址0開始的。所以經過了編譯之后我們就把高級語言翻譯成了與它們等價的機器語言。只不過每一個模塊的邏輯地址的編址都是相互獨立的,都是從0開始的。那接下來的一步叫做鏈接。這一步做的事情就是把這些目標模塊都給組裝起來,形成一個完整的裝入模塊。而在Windows電腦當中,所謂的裝入模塊就是我們很熟悉的.exe文件,也就是可執行文件。把這些目標模塊鏈接起來之后,所形成的裝入模塊,就有一個完整的邏輯地址。當然在鏈接這一步,除了我們自己編寫的這些目標模塊需要鏈接以外,還需要把它們所調用到的一些庫函數比如說printf啊之類的這些函數,也給鏈接起來,把它形成一個完整的裝入模塊。那有了裝入模塊或者說有了這個可執行文件之后,我們就可以讓這個程序開始運行了。那程序要運行首先要做的事情就是我們剛才一直強調的那個過程,就是需要把這個裝入模塊裝入內存當中,並且當它裝入內存之后就確定了這個進程它所對應的實際的物理地址到底是多少。所以這就是我們從寫程序到程序運行的一個完整的流程。那之前我們一直強調的是,裝入這個步驟怎么完成,三種裝入的策略可以實現邏輯地址到物理地址的轉換。那接下來我們要介紹的是三種鏈接的方式,也就是這一步也有三種方法。

第一種鏈接方式叫做靜態鏈接,就是指在程序運行之前就把這些一個一個的目標模塊把它們鏈接成一個完整的可執行文件,也就是裝入模塊,之后便不再拆開,就是剛才我們所提到的這種方式。也就是說在形成了這個裝入模塊之后,就確定了這個裝入模塊的完整的邏輯地址。

那第二種鏈接方式叫做裝入式動態鏈接,就是說這些目標模塊不會先把它們鏈接起來,而是當這些目標模塊放入內存的時候才會進行鏈接這個動作。

也就是說采用這種方式的話,這個進程的完整的邏輯地址是一邊裝入一邊形成的。

那第三種方式叫做運行時動態鏈接,如果采用這種方式的話那么只有我們需要用到某一個模塊的時候才需要把這個模塊調入內存。比如說剛開始是main函數運行,那么我們就需要把目標模塊1先放到內存當中,然后執行的過程當中可能又發現main函數需要調用到a這個函數,所以我們需要把目標模塊2也把它放到內存當中,並且把它裝入的時候同時進行一個鏈接的工作。那如果說b這個函數在整個過程當中都用不到的話,那目標模塊3我們就可以不裝入內存。所以采用這種方式很顯然它的這個靈活性要更高,並且用這種方式可以提升對於內存的利用率。

而一個存儲單元可以存放多少數據,這個我們需要看這個計算機它到底是按字節編址還是按字編址。如果是按字節編址的話,那么一個存儲單元就是存放一個字節,也就是一個大B一個Byte。那內存地址其實就是給這些存儲單元的一個編號,CPU可以根據內存地址這個參數來找到正確的存儲單元。那之后我們又簡單地介紹了指令工作的原理。一條機器指令由操作碼和一些參數組成。操作碼給CPU指明了你現在需要干一些什么事情,而參數指明了你現在需要怎么干。而這個參數當中可能會包含地址參數,而一般來說這個指令中所包含的地址參數指的都是邏輯地址也就是相對地址。所以為了讓這個指令正常地工作,我們就需要完成從邏輯地址到物理地址的一個轉換。那為了完成邏輯地址到物理地址的轉換,我們又介紹了三種裝入方式,分別是絕對裝入、可重定位裝入和動態運行時裝入。其中可重定位裝入又稱作為靜態重定位,而動態運行時裝入又稱為動態重定位。這三種裝入方式是考研當中比較喜歡考查的內容。

那最后我們還介紹了從我們程序員寫程序到最后的程序運行需要經歷哪些步驟。首先是要編輯源代碼文件,然后源代碼文件經過編譯形成若干的目標模塊。目標模塊經過鏈接之后形成裝入模塊,最后再把裝入模塊裝入到內存。這個程序就可以開始正常地運行了。那我們還介紹了三種鏈接的方式分別是靜態鏈接、裝入時動態鏈接和運行時動態鏈接。其實經過剛才的講解我們能夠體會到,鏈接這一步就是要把各個目標模塊的那些邏輯地址,把它們組合起來形成一個完整的邏輯地址,所以鏈接這一步其實就是確定這個完整的邏輯地址這樣的一個步驟。而裝入這一步又是確定了最終的物理地址,這個小節的內容其實考查的頻率很低,只不過是為了讓大家更深入地理解之后的內容所以才進行了一些補充。

我們知道操作系統它作為系統資源的管理者,當然也需要對系統當中的各種軟硬件資源進行管理,包括內存這種資源。那么操作系統在管理內存的時候需要做一些什么事情呢?我們知道各種進程想要投入運行的時候,需要先把進程相關的一些數據放入到內存當中,就像這個樣子。那么內存當中,有的區域是已經被分配出去的,而有的區域是還在空閑的。操作系統應該怎么管理這些空閑或者非空閑的區域呢?另外,如果有一個新進程想要投入運行,那么這個進程相關的數據需要放入內存當中。但是如果內存當中有很多個地方都可以放入這個進程相關的數據,那這個數據應該放在什么位置呢?這也是操作系統需要回答的問題。第三,如果說有一個進程運行結束了,那么這個進程之前所占有的那些內存空間,應該怎么被回收呢?那所有的這些都是操作系統需要負責的問題。因此,內存管理的第一件事就是要操作系統來負責內存空間的分配與回收。那內存空間的分配與回收這個問題比較龐大,現在暫時不展開細聊,之后還會有專門的小節進行介紹。

計算機當中也經常會遇到實際的內存空間不夠所有的進程使用的問題。所以操作系統對內存進行管理,也需要提供某一種技術,從邏輯上對內存空間進行擴充,也就是實現所謂的虛擬性,把物理上很小的內存拓展為邏輯上很大的內存。那這個問題也暫時不展開細聊,之后還會有專門的小節進行介紹。

第三個需要實現的事情是地址轉換。為了讓編程人員編程更方便,程序員在寫程序的時候應該只需要關注指令、數據的邏輯地址。而邏輯地址到物理地址的轉換,或者說地址重定位這個過程應該由操作系統來負責進行,這樣的話程序員就不需要再關心底層那些復雜的硬件細節。所以內存管理的第三個功能就是應該實現地址轉換。就是把程序當中使用的邏輯地址,把它轉換成最終的物理地址。那么實現這個轉換的方法,咱們在上個小節已經介紹過,

就是用三種裝入方式分別是絕對裝入、可重定位裝入和動態運行時裝入。絕對裝入是在編譯的時候就產生了絕對地址或者說在程序員寫程序的時候直接就寫了絕對地址。那么這種裝入方式只在單道程序階段才使用。但是單道程序階段其實暫時還沒有產生操作系統,所以這個地址轉換其實是由編譯器來完成的,而不是由操作系統來完成的。那第二種方式叫做可重定位裝入,或者叫靜態重定位,就是指在裝入的時候把邏輯地址轉換為物理地址,那這個轉換的過程是由裝入程序負責進行的。那裝入程序也是操作系統的一部分。那這種方法一般來說是用於早期的多道批處理操作系統當中。那第三種裝入方式叫做動態運行時裝入或者叫動態重定位,就是運行的時候才把邏輯地址轉換為物理地址,當然這種轉換方式一般來說需要一個硬件——重定位寄存器的支持。而這種方式一般來說就是現代操作系統采用的方式,咱們之后在學習頁式存儲還有段式存儲的時候會大量地接觸這種動態運行時裝入的方式。所以說操作系統一般會用可重定位裝入和動態運行時裝入這兩種方式實現從邏輯地址到物理地址的轉換。而采用絕對裝入的那個時期暫時還沒有產生操作系統。那這就是內存管理需要實現的第三個功能——地址轉換。

第四個功能叫內存保護。就是指操作系統要保證各個進程在各自存儲空間內運行,互不干擾。

我們直接用一個圖讓大家更形象地理解。在內存當中一般來說會分為操作系統使用的內存區域還有普通的用戶程序使用的內存區域。那各個用戶進程都會被分配到各自的內存空間,比如說進程1使用的是這一塊內存區域,進程2使用的是這一塊內存區域。那如果說進程1想對操作系統的內存空間進行訪問的話,很顯然這個行為應該被阻止。如果進程1可以隨意地更改操作系統的數據,那么很明顯會影響整個系統的安全。另外如果進程1想要訪問其他進程的存儲空間的話,那么顯然這個行為也應該被阻止。如果進程1可以隨意地修改進程2的數據的話,那么顯然進程2的運行就會被影響,這樣也會導致系統不安全。所以進程1只能訪問進程1自己的那個內存空間,所以這就是內存保護想要實現的事情。讓各個進程只能訪問自己的那些內存空間,而不能訪問操作系統的也不能訪問別的進程的空間。那我們可以采用這樣的方式來進行內存保護,就是在CPU當中設置一對上限寄存器和下限寄存器,分別用來存儲這個進程的內存空間的上限和下限。那如果進程1的某一條指令想要訪問某一個內存單元的時候,CPU會根據指令當中想要訪問的那個內存地址和上下限寄存器的這兩個地址進行對比。只有在這兩個地址之間才允許進程1訪問,因爲只有這兩個地址之間的這個部分才屬於進程1的內存空間。那這是第一種方法,可以設置一對上下限寄存器。

第二種方法我們可以采用重定位寄存器和界地址寄存器來判斷此時是否有越界的嫌疑。那麽重定位寄存器又可以稱爲基址寄存器,界地址寄存器又稱爲限長寄存器。那重定位寄存器的概念咱們在上個小節已經接觸過,就是在動態運行時裝入那種方式裏,我們需要設置一個重定位寄存器,來記錄每一個進程的起始物理地址。界地址寄存器又可以稱爲限長寄存器,就是用來存放這個進程的最大邏輯長度的。比如說像進程1它的邏輯地址是0~179,所以界地址寄存器當中應該存放的是它的最大的邏輯地址也就是179。而重定位寄存器的話應該存放這個進程的起始物理地址,也就是100。那麽假如現在進程1想要訪問邏輯地址為80的那個內存單元的話,首先這個邏輯地址會和界地址寄存器當中的這個值進行一個對比。如果說沒有超過界地址寄存器當中保存的最大邏輯地址的話,那麽我們就認爲這個邏輯地址是合法的。如果超過了,那麽會拋出一個越界異常。那沒有越界的話,邏輯地址會和重定位寄存器的這個起始物理地址進行一個相加,最終就可以得到實際的想要訪問的物理地址也就是180。

那這個小節中我們學習了內存管理的整體框架。內存管理總共需要實現四個事情,內存空間的分配與回收,內存空間的擴充以實現虛擬性,另外還需要實現邏輯地址到物理地址的轉換。那么地址轉換一般來說有三種方式,就是上個小節學習的內容——絕對裝入、可重定位裝入和動態運行時裝入。其中絕對裝入這個階段其實是在早期的單道批處理階段才使用的,這個階段暫時還沒有操作系統產生。而可重定位裝入一般用於早期的多道批處理系統,現在的操作系統大多使用的是動態運行時裝入。另外呢內存管理還需要提供存儲保護的功能,就是要保證各個進程它們只在自己的內存空間內運行,不會越界訪問。那一般來說有兩種方式,第一種是設置上下限寄存器。第二種方式是利用重定位寄存器和界地址寄存器進行判斷。那么重定位寄存器又可以叫做基址寄存器,而界地址寄存器又可以叫做限長寄存器。這兩個別名大家也需要注意。那么本章之后內容還會介紹更多的內存空間的分配與回收,還有內存空間的擴充的一些相關策略。那這個小節的內容不算特別重要,只是為了讓大家對內存管理到底需要做什么形成一個大體的框架。

那在之前的小節中我們已經學習到了操作系統對內存進行管理需要實現這樣四個功能。那地址轉換和存儲保護是上個小節詳細介紹過的。那這個小節我們會介紹兩種實現內存空間的擴充的技術——覆蓋技術和交換技術,那虛擬存儲技術會在之後用更多的專門的視頻來進行講解。

一般來説都很少有低於100MB字節的這種程序。所以可想而知1MB字節的大小很多時候應該是不能滿足這些程序的運行的。那么后來人們為了解決這個問題就引入了覆蓋技術,就是解決程序大小超過物理內存總和的問題。比如說一個程序本來需要這么多的內存空間,但實際的內存大小又只有這么多。那怎么辦呢?覆蓋技術的思想就是要把程序分成多個段,或者理解為就是多個模塊。然后常用的段就需要常駐內存,不常用的段只有在需要的時候才需要調入內存。那內存當中會分一個“固定區”和若干個“覆蓋區”,常用的那些段需要放在固定區里,並且調入之后就不再調出,除非運行結束,這是固定區的特征。那不常用的段就可以放在“覆蓋區”里,只有需要的時候才需要調入內存,也就是調入內存的覆蓋區,然后用不到時候就可以調出內存。

A這個模塊會依次調用B模塊和C模塊。注意是依次調用,也就是說B模塊和C模塊只可能被A這個模塊在不同的時間段調用,不可能是同時訪問B和C這兩個模塊。另外由於B模塊和C模塊不可能同時被訪問,也就是說在同一個時間段內內存當中要么有B要么有C就可以了,不需要同時存在B和C這兩個模塊。所以我們可以讓B和C這兩個模塊共享一個覆蓋區,那這個覆蓋區的大小以B和C這兩個模塊當中更大的這個模塊為准,也就是10KB。因為如果我們把這個覆蓋區設為10KB的話,那既可以存的下C也可以存的下B。那同樣的,D、E、F這幾個模塊也不可能同時被使用。所以這幾個模塊也可以像上面一樣共享一個覆蓋區,覆蓋區1,那它的大小就是按它們之間最大的這個也就是D模塊的大小12KB來計算。所以如果說我們的程序有一個明顯的這種調用結構的話,那么我們可以根據它這種自身的邏輯結構,讓這些不可能被同時訪問的程序段共享一個覆蓋區。那只有其中的某一個模塊被使用的時候,那這個模塊才需要放到覆蓋區里。所以采用了覆蓋技術之后,在邏輯上看這個物理內存的大小是被拓展了的。不過這種技術也有一個很明顯的缺點,因為這個程序當中的這些調用結構操作其實系統肯定是不知道的,所以程序的這種調用結構必須由程序員來顯性地聲明,然后操作系統會根據程序員聲明的這種調用結構或者說覆蓋結構,來完成自動覆蓋。所以這種技術的缺點就是對用戶不透明,增加了用戶編程的負擔。因此,覆蓋技術現在已經很少使用了,它一般就只用於早期的操作系統中,現在已經退出了歷史的舞台。

所以其實采用這種技術(交換技術/對換技術)的時候,進程是在內存與磁盤或者說外存之間動態地調度的。那之前咱們其實已經提到過一個和交換技術息息相關的知識點,咱們在第二章講處理機調度的時候,講過一個處理機調度層次的概念,分為高級調度、中級調度和低級調度。那其中中級調度就是爲了實現交換技術而使用的一種調度策略。就是說本來我們的內存當中有很多進程正在並發地運行,那如果某一個時刻突然發現內存空間緊張的時候我們就可以把其中的某些進程把它放到暫時換出外存。

而進程相關的PCB會保留在內存當中,並且會插入到所謂的掛起隊列裏。那一直到內存空間不緊張了,內存空間充足的時候又可以把這些進程相關的數據再給換入內存。那爲什麽進程的PCB需要常駐內存呢?因爲進程被換出外存之後其實我們必須要通過某種方式記錄下來這個進程到底是放在外存的什麽位置,那這個信息也就是進程的存放位置這個信息,我們就可以把它記錄在與它對應的這些PCB當中。那操作系統就可以根據PCB當中記錄的這些信息,對這些進程進行管理了,所以進程的PCB是需要常駐內存的。

那麽中級調度或者説內存調度,其實就是在交換技術當中,選擇一個處於外存的進程把它換入內存這樣一個過程。那講到這個地方大家也需要再回憶一下低級調度和高級調度分別是什麽。

那既然提到了掛起我們就再來回憶一下和掛起相關的知識點。暫時換出外存等待的那些進程的狀態稱之爲掛起狀態或者簡稱掛起態。那掛起態又可以進一步細分為就緒掛起和阻塞掛起兩種狀態。在引入了這兩種狀態之後我們就提出了一種叫做進程的七狀態模型。那如果一個本來處於就緒態的進程被換出了外存,那這個進程就處於就緒掛起態。如果一個本來處於阻塞態的進程被換出外存的話,那麽這個進程就處於阻塞掛起態。那七狀態模型相關的知識點咱們在第二章當中已經進行過補充,這兒就不再贅述。那大家可以再結合這個圖回憶一下這些狀態之間的轉換是怎麽進行的,特別是中間的這三個最基本的狀態之間的轉換。所以采用了交換技術之後,如果說某一個時刻內存裏的空間不夠用了,那麽我們可以把內存中的某一些進程數據暫時換到外存裏,再把某一些更緊急的進程數據放回內存,所以交換技術其實也在邏輯上擴充了內存的空間。

在現代計算機當中,外存一般來説就是磁盤。那具有對換功能或者說交換功能的操作系統當中,一般來説會把磁盤的存儲空間分爲文件區和對換區這樣兩個區域。文件區主要是用來存放文件的,主要是需要追求存儲空間的利用率。所以在對文件區,一般來説是采用離散分配的方式。而這個地方一會兒再做解釋。那對換區的空間一般來説只占磁盤空間的很小的部分,注意被換出的進程數據一般來説就是存放在對換區當中的,而不是文件區。那由於對換區的這個換入換出速度會直接影響到各個進程並發執行的這種速度,所以對於對換區來説我們應該首要追求換入換出的速度。因此對換區通常會經常采用連續分配的方式。那這個地方大家理解不了暫時沒有關係,咱們在第四章文件管理的那個章節會具體地再進一步學習什麽是對換區什麽是文件區,並且到時候大家就能夠理解爲什麽離散分配方式可以更大地提高存儲空間的利用率,而連續分配方式可以提高換入換出的速度。那這個地方大家只需要理解一個結論,對換區的I/O速度或者説輸入輸出的速度,是要比文件區更快的。所以我們的進程數據被換出的時候,一般是放在對換區,換入的時候也是從對換區再換回內存。

一般來説交換會發生在系統當中有很多進程在運行並且內存吃緊的時候。那在這種時候,我們可以選擇換出一些進程來騰空內存空間那一直到系統負荷明顯降低的時候就可以暫停換出。比如說如果操作系統在某一段時間發現許多進程運行的時候都經常發生缺頁,那這就説明內存的空間不夠用,所以這種時候就可以選擇換出一些進程來騰空一些內存空間。那如果說缺頁率明顯下降,也就是說看起來系統負荷明顯降低了,我們就可以暫停換出進程了。那這個地方涉及到之後的小節會講到的缺頁還有缺頁率這些相關的知識點。這兒理解不了沒有關係,大家能夠有個印象就可以了。

首先我們可以考慮優先換出一些阻塞的進程。因爲處於就緒態的進程,其實是可以投入運行的。而處於阻塞態的進程,即使是在內存當中反正它暫時也運行不了了,所以我們可以優先把阻塞進程調出換到外存當中。第二,我們可以考慮換出優先級比較低的進程。那這個不用解釋,很好理解。第三,如果我們每次都是換出優先級更低的進程的話,那麽就有可能導致優先級低的進程剛被調入內存很快又被換出的問題。那這就有可能會導致優先級低的進程飢餓的現象。所以有的系統當中爲了防止這種現象,會考慮進程在內存當中的駐留時間。如果一個進程在內存當中駐留的時間太短,那這個進程就暫時不會把它換出外存。那這個地方再強調一點,PCB是會常駐內存的,並不會被換出外存。所以其實所謂的換出進程,並不是把進程相關的所有的數據一個不漏的全部都調到外存裏,操作系統爲了保持對這些換出進程的管理,那PCB這個信息還是需要放在內存當中。那麽這就是交換技術。

那這個小節我們學習了覆蓋技術和交換技術相關的知識點。那這兩個知識點一般來說只會在選擇題當中進行考查。大家只要能夠理解這兩種技術的思想就可以了。那么可能稍微需要記憶一點的就是,固定區和覆蓋區相關的這些知識點。在固定區當中的程序段,在運行過程當中是不會被調出的。而在覆蓋區當中的程序段,在運行過程當中是有可能會根據需要進行調入調出的。另外,如果考查了覆蓋技術的話,那么很有可能會把覆蓋技術的缺點作為其中的某一個選項進行考查。那在講解交換技術的過程當中我們補充了文件區和對換區相關的知識點,這些會在第四章中進行進一步的學習。那這個地方大家只需要知道換出的進程的數據一般來說是放在磁盤的對換區當中的。那最后我們再來看一下覆蓋與交換這兩種技術的一個明顯的區別。其實覆蓋技術是在同一個程序或者進程當中進行的。那相比之下交換技術是在不同進程或作業之間進行的,而暫時運行不到的進程可以調出外存。那比較緊急的進程可以優先被再重新放回內存。

在之前的學習中我們知道,操作系統對內存進行管理,需要實現這樣四個事情。那么內存空間的擴充,地址轉換和存儲保護,這是之前的小節介紹過的內容。從這個小節開始我們會介紹內存空間的分配與回收應該怎么實現。我們在這個小節中會先介紹連續分配管理方式,分別是單一連續分配,固定分區分配和動態分區分配。我們會按從上至下的順序依次講解。那么這兒需要注意的是,所謂的連續分配和非連續分配的區別在於,連續分配是指,系統為用戶進程分配的必須是一個連續的內存空間。而非連續分配管理方式是指系統為用戶分配的那些內存空間不一定是連續的,可以是離散的。

那么我們先來看單一連續分配方式。采用單一連續分配方式的系統當中,會把內存分為一個系統區和一個用戶區。那系統區就是用於存放操作系統相關的一些數據,用戶區就是用於存放用戶進程或者說用戶程序相關的一些數據。不過需要注意的是,采用單一連續分配方式的這種系統當中,內存當中同一時刻只能有一道用戶程序。也就是說它並不支持多道程序並發運行,所以用戶程序是獨占整個用戶區的,不管這個用戶區有多大。比如說一個用戶進程或者說用戶程序,它本來只需要這么大的內存空間。

那當它放到內存的用戶區之后,用戶區當中其他那些空閑的區間其實也不會被分配給別的用戶程序。所以說是整個用戶程序獨占整個用戶區的這種存儲空間的。所以這種方式其實優點很明顯就是實現起來很簡單,並且沒有外部碎片。那外部碎片這個概念我們在講到動態分區分配的時候再補充,這兒先有個印象。那由於整個系統當中同一時刻只會有一個用戶程序在運行,所以采用這種分配方式的系統當中不一定需要采用內存保護。注意只是不一定,有的系統當中它也會設置那種越界檢查的一些機制。但是像早期的個人操作系統,微軟的DOS系統,就沒有采用這種內存保護的機制。因為系統中只會運行一個用戶程序,那么即使這個用戶程序出問題了,那也只會影響用戶程序本身,或者說即使這個用戶程序越界把操作系統的數據損壞了,那這個數據一般來說也可以通過重啟計算機就可以很方便地就進行修復。所以說采用單一連續分配方式的系統當中,不一定采取內存保護,那這也是它的優點。那另一方面,這種方式的缺點也很明顯,就是只適用於單用戶、單任務的操作系統,它並不支持多道程序並發運行,並且這種方式會產生內部碎片。那所謂的內部碎片,就是指我們分配給某一個進程或者說程序的內存區間當中,如果有某一個部分沒有被用上,那這就是所謂的內部碎片。像這個例子當中,本來整個用戶區都是分配給這個用戶進程A的,但是有這么大一塊它是空閑的,暫時沒有利用起來。那本來給這個進程分配了,但是這個進程沒有用上的這一部分內存區域就是所謂的內部碎片。所以這種方式也會導致存儲器的利用率很低。那這是單一連續分配方式。

 

多道程序技術就是可以讓各個程序同時裝入內存,並且這些程序之間不能相互干擾,所以人們就想到了把用戶區划分成了若干個固定大小的分區,並且在每一個分區內只能裝入一道作業。也就是說每一道作業或者說每一道程序它是獨享其中的某一個固定大小的分區的。那這樣的話就形成了最早的可以支持多道程序的內存管理方式。那固定分區分配可以分為兩種,一種是分區大小相等,另外一種是分區大小不等。如果說采用的是分區大小相等的策略的話,系統會把用戶區的這一整片的內存區間分割為若干個固定大小並且大小相等的區域。

比如說每個區域十個字節,像這樣子。那如果說采用的是分區大小不相等的這種策略的話,系統會把用戶區分割為若干個大小固定但是大小又不相等的分區,比如說像這個樣子。那這兩種方式各有各的特點,如果說采用的是分區大小相等的這種策略的話,很顯然會缺乏靈活性。比如說一些小的進程它可能只需要占用很小的一部分內存空間,但是由於每個分區只能裝入一道作業,所以一個很小的進程又會占用一個比較大的、很多余的一個分區。那如果說一個有一個比較大的進程進入的話,那么如果這些分區的大小都不能滿足這個大進程的需求,那么這個大進程就不能被裝入這個系統,或者說只能采用覆蓋技術,在邏輯上來拓展各個分區的大小。但這又顯然又會增加一些系統開銷。所以說分區大小相等的這種情況是比較缺乏靈活性的,不過這種策略即使在現代也是有很廣泛的用途的。那由於這n個煉鋼爐本來就是相同的對象,所以對這些相同的對象進行控制的程序當然也是相同的程序。所以如果采用這種把它分割為n個大小相等的區域來分別存放n個控制程序,讓這n個控制程序並發執行,並發地控制各個煉鋼爐的話,那在這種場景下的應用就是很適合的。那如果分區大小不等的話,靈活性會有所增加。比如說小的進程我們可以給它分配一個小的分區,而大的進程可以給它分配一個大的分區。那一般來說可以先估計一下系統當中會出現的大作業、小作業分別到底有多少。然后再根據大小作業的比例來對這些大小分區的數量進行一個划分。比如說可以划分多個小分區,適量的中等分區、然后少量的大分區。

那接下來我們再考慮下一個問題,操作系統應該怎么記錄內存當中各個分區的空閑或者分配的這些情況呢?那一般來說我們可以建立一個叫做分區說明表的一個數據結構,用這個數據結構對各個分區進行管理。比如說如果系統當中內存的情況是這個樣子,那么我們可以給它建立一個對應的分區說明表。那每一個表項會對應其中的某一個分區,那每一個表項需要包含當前這個分區的大小還有這個分區的起始地址還有這個分區是否已經被分配的這種狀態。那像這樣一張表其實我們可以建立一個數據結構,數據結構當中有這樣一些屬性,然后把這個用這個數據結構組成一個數組或者組成一個鏈表來表示這樣一個表。那如果學過數據結構的同學這兒應該不難理解。那操作系統根據這個數據結構就可以知道各個分區的使用情況,如果說一個用戶程序想要裝入內存的話,操作系統就可以來檢索這個表,然后找到一個大小能夠滿足並且沒有被分配出去的分區,然后把這個分區分配給用戶程序。之后再把這個分區對應的狀態改成已分配的狀態就可以了。那么固定分區分配實現起來其實也不算復雜,並且使用這種方式也不會產生外部碎片。那么外部碎片這個概念咱們再往后拖一拖,下一個分配方式當中會進行講解。但是這種方式也有很明顯的缺點。如果說一個用戶程序太大了,大到沒有任何一個分區可以直接滿足它的大小的話,那么我們只能通過覆蓋技術來解決這個分區大小不夠的問題。但是如果采用了覆蓋技術,那就意味着需要付出一定的代價,會降低整個系統的性能。另外,這種分配方式很顯然也會產生內部碎片,比如說有一個用戶程序它所需要的內存空間是10MB,那么掃描了這個表之后會發現,只有分區6可以滿足10MB這么大的需求,所以這個用戶程序就會被裝到分區6里。但是由於這個用戶程序會獨占整個分區6,所以分區6總共有12MB,那么就有兩兆字節的空間是分配給了這個程序,那這個程序又用不到的。那這一部分就是所謂的內部碎片。所以固定分區分配是會產生內部碎片的,因此它的內存利用率也不是特別高。

動態動態

那么,為了解決這個問題人們又提出了動態分區分配的策略。動態分區分配又可以稱作可變分區分配,這種分配方式並不會像之前固定分區分配那樣預先划分內存區域。而是在進程裝入內存的時候才會根據進程的大小動態地建立分區。而每一個分區的大小會正好適合進程所需要的那個大小。所以和固定分區分配不同,如果采用動態分區分配的話,系統當中內存分區的大小和數目是可以改變的。那我們來看一個例子。比如說一個計算機的內存大小總共是64MB字節,然后系統區會占8MB字節,那用戶區就是56MB字節。剛開始一個用戶進程1到達,它總共占用了20MB字節的分區,之后一個用戶進程2到達,占用了14MB字節的分區,用戶進程3到達,占用了18MB字節的分區。那么56MB字節的用戶區總共只會占4MB字節的空閑分區。那么系統中這些分區的大小和數量是可變的,並且有些分區是已經被分出去的,有些分區又是沒有被分出去的。操作系統應該用什么樣的數據結構來記錄這個內存的使用情況呢?這是我們之后要探討的第一個問題。那再來看第二個問題,

如果此時占有14MB字節的進程2已經運行結束了,並且被移出了內存,那么內存當中就會有這樣一片14MB字節的空閑區間,那此時如果有一個新進程到達,並且這個進程需要4兆字節的內存空間。那這一片空閑區間是14MB,這一片空閑區間是4MB。那到底應該放這一片還是放下面這一片呢?這又是第二個問題。當我們的內存當中有很多個空閑分區都可以滿足進程的需求的時候,應該把哪個空閑區間分配給那個進程呢?這是第二個問題。

第三個問題,假設此時占18MB字節的進程三運行結束,並且被撤離了內存。那么內存當中就會出現18MB字節的一個新的空閑分區。那這個空閑分區應該怎么處理?是否應該和與它相鄰的這些分區進行合並呢?這就是第三個問題,我們應該如何進行分區的分配和回收的操作。那接下來我們依次對這三個問題進行探討。

先來看第一個問題,操作系統應該用什么樣的數據結構記錄內存的使用情況?那一般來說會采用兩種常用的數據結構,要么是空閑分區表,或者采用空閑分區鏈。比如某一個時刻系統當中內存的使用情況是這個樣子。總共有三個空閑分區,那么如果采用空閑分區表的話,這個表就會有三個對應的表項,每一個表項會對應一個空閑分區,並且每一個表項都需要記錄與這個表項相對應的空閑分區的大小是多少,起始地址是多少等等一系列的信息。那如果說沒有在空閑分區表當中記錄的那些分區當然就是已經被分配出去的。再來看第二種數據結構,空閑分區鏈。如果采用這種方式的話,那么每一個分區的起始部分和末尾部分,都會分別設置一個指向前面一個空閑分區和指向后面一個空閑分區的指針,就像這個樣子。所以就會把這些空閑分區用一個類似於鏈表的方式把它們鏈接起來。那每一個空閑分區的大小,還有空閑分區的起始地址,結尾地址等等這些信息,可以統一地把它們放在各個空閑分區的起始部分。所以這是我們可以采用的兩種數據結構——空閑分區表和空閑分區鏈。

那再來看第二個問題,當有很多空閑分區都可以滿足需求的時候,到底應該選擇哪個空閑分區進行分配呢?假如此時有一個進程5它只需要4兆字節的空間,那么這個空閑分區、這個分區還有這個分區這三個空閑分區都可以滿足它這個需求。那我們應該用哪個分區進行分配呢?那由這個問題我們可以引出動態分區分配算法相關的問題。那所謂的動態分區分配算法,就是要從空閑分區表,或者空閑分區鏈當中,按照某一種規則,選擇出一個合適的分區把它分配給此時請求的這個進程或者說作業。那由於這個分配算法對系統性能造成的影響是很大的,所以人們對於這個問題進行了很多的研究。那這個問題我們現在暫時不展開處理,會在下一個小節進行詳細的介紹。

接下來我們再來看第三個問題,如何進行分區的分配與回收?那假設我們采用的是空閑分區表的這種數據結構的話,進行分配的時候需要做一些什么操作呢?那這個地方我們只以空閑分區表為例,其實空閑分區鏈的操作也是大同小異的。那假如說此時系統當中有這樣三塊空閑的分區,如果此時有一個進程需要申請四兆字節的內存空間,那假設我們采用了某一種算法,最后決定從這20MB的空閑分區當中摘出四兆分配給這個進程5,

就像這樣。那么我們需要對這個空閑分區表進行一定的處理,那由於這個空閑分區的大小本來就是比此次申請的這塊內存區域的大小要更大的。所以即使我們從這個分區當中摘出一部分進行了分配,那么分區的數量依然是不會改變的。所以我們只需要在這個分區對應的那個表項當中,修改一下它的分區大小還有起始地址就可以了。那這是第一種情況。

再來看第二種情況。還是相同的地址,有一個進程5需要4MB字節。那如果說我們采用了某種分配算法,最后決定把這4MB字節的空閑分區分配給這個進程5,

那么本來這個空閑分區的大小就和此次申請的這個內存空間大小是相同的,所以如果把這個分區、空閑分區全部分配給這個進程的話,那么顯然空閑分區的數量會減1,所以我們需要把這個分區對應的這個表項給刪除。那如果說我們采用的是空閑分區鏈的話,那我們就只需要把其中的某一個空閑分區鏈的結點給刪掉,那這是分配的時候可能會遇到的兩種情況。

接下來我們再來看進行回收的時候可能會需要做一些什么樣的處理?假設此時系統內存中的情況是這樣的。那如果采用“空閑分區表”這種數據結構的話,那這個表應該是由兩個表項分別對應一個10MB的空閑分區和一個4MB的空閑分區。那假設此時進程4已經運行結束了,我們可以把進程4占用的這4MB字節的空間給回收。那么此時這塊回收的區域的后面,有一個相鄰的空閑分區,也就是10MB的這塊分區,

因此我們把這塊內存分區回收之后,我們需要把空閑分區表當中對應的那個表項的分區大小和起始地址也進行一個更新。所以可以看到,如果兩個空閑分區相鄰的話,那么我們需要把這兩個空閑分區進行合二為一的操作。

再來看第二種情況。假設此時進程三已經運行結束了,那么當進程三占用的這一塊分區被回收之后,在它的前面也有一個相鄰的空閑分區,

所以參照剛才的那種思路,我們也需要把這兩塊相鄰的空閑分區進行合二為一的操作。那這和之前的那種情況其實是很類似的。

再看第三種情況。假設此時進程四已經運行結束,需要把這四兆字節給回收,那么進程四的前面和后面都會有一個相鄰的空閑分區。所以本來我們的空閑分區表有三個表項,也就是有三個空閑分區,

但是當進程四的這塊空間被回收之后,需要把這一整塊的空間都進行一個合並。所以本來系統中有三個空閑分區,但如果把進程四回收之后就會合並為兩個空閑分區。那當然我們也需要修改相應表項的這些分區大小、起始地址等等這一系列的信息。那這第三種情況需要把三個相鄰的空閑分區合並為一個。

再來看第四種情況。假如回收區的前后都沒有相鄰的空閑分區的話,應該怎么處理。假設此時進程2已經運行結束,那么當進程2的這塊內存區間被回收之后,

系統當中就出現了兩塊空閑分區。所以相應的我們當然也需要增加一個空閑分區表的表項。那通過剛才的這一系列講解,大家可能會發現,我們對空閑分區表的這種順序一般來說是采用這種按照起始地址的先后順序來進行排列的。但是這個並不一定,各個表項的排序我們一般來說需要根據我們采用哪種分區分配算法來確定。比如說有的時候我們按照分區從大到小的順序排列會比較方便,有的時候我們按照分區大小從小到大進行排列比較方便。當然也有的時候我們就像現在這樣按照起始地址的先后順序來進行排列會比較方便。那這個地方會到下一個小節進行進一步的解釋。那到這個地方,我們就回答了之前提出的三個問題,第一個問題我們需要用什么樣的數據結構來記錄內存的使用情況。一般來說會使用兩種數據結構——空閑分區表或者空閑分區鏈。那第二個問題涉及到動態分區分配算法就會在下一個小節中進行進一步的解釋。第三個問題我們討論了怎么對內存的空間進行分配與回收。進行分配與回收的時候需要對這些數據結構進行什么處理。那特別需要注意的是,在回收的過程中,我們有可能會遇到四種情況。不過本質上我們可以用一句話來進行總結,在進行內存分區回收的時候如果說回收了之后發現有一些空閑分區是相鄰的,那么我們就需要把這些相鄰的空閑分區全部給合並。

那接下來我們再來討論一下動態分區分配關於內部碎片和外部碎片的問題。這兒我們給出了內部碎片和外部碎片的完整的定義,內部碎片是指分配給某個進程的內存區域當中,如果說有些部分沒有用上,那么這些部分就是所謂的內部碎片。注意是分配給這個進程但是這個進程沒有用上的那些部分。而外部碎片是指內存當中的某些空閑分區由於太小而難以利用。那因為各個進程需要的都是一整片連續的內存區域,所以如果這些空閑的分區太小的話那么任何一個空閑分區都不能滿足進程的需求,那這種空閑分區就是所謂的外部碎片。

比如說我們系統當中依次進入了進程1、進程2、進程3它們的大小分別是這樣。然后這個時候內存當中只剩下一片空閑的內部區域,就是4M字節這么大。那么此時如果進程2暫時不能運行,

我們可以暫時把它換出到外存當中。那於是這塊就有14M字節的空閑區域。

那接下來進程4到達占用4M字節,

那這一塊就應該是10M字節的大小。之后如果進程1也暫時不能運行,那么我們可以把進程1暫時換出外存。

於是這個地方可以空出20M字節的連續的空閑區間。

那接下來如果進程2又可以恢復運行了,它再回到內存當中,它又占用了其中的14M字節。

於是這一塊就只剩下6M字節。

那接下來如果說進程1也就是20M字節的這個進程又可以執行了又想回到內存的話,那么此時會發現內存當中的任何一個區域都已經不能滿足進程1的這個需求了。所以這就產生了所謂的外部碎片。這些空閑區間是暫時沒有分配給任何一個進程的,但是由於它們都太小了太零碎了所以沒辦法滿足這種大進程的需求。那像這種情況下,其實內存當中總共剩余的內存區間其實是6+10+4,也就是總共有20M字節。也就是說內存當中空閑區間的總和其實是可以滿足進程1的需求的。所以在這種情況下,我們可以采用緊湊技術或者是拼湊技術來解決外部碎片的問題。那緊湊技術很簡單,

其實就是把各個進程挪位,

把它們全部攢到一起,

然后挪出一個更大的空閑、連續的空閑區間出來。

這樣的話,這塊空閑區間就可以滿足進程1的需求了。那這個地方大家也可以停下來回憶一下咱們剛才提到的換入換出技術和中級調度相關的一些概念,這是咱們之前講過的內容。那顯然咱們之前介紹的三種裝入方式當中,動態重定位的方式其實是最方便實現這些程序或者說進程在內存當中移動位置這件事情的,所以我們采用的應該是動態重定位的方式。另外,緊湊之后我們需要把各個進程的起始地址給修改掉。那進程的起始地址這個信息一般來說是存放在進程對應的PCB當中。當進程要上CPU運行之前,會把進程的起始地址那個信息放到重定位寄存器里,或者叫基址寄存器里。那大家對這些概念還有沒有印象呢?

那這個小節我們介紹了三種連續分配管理的分配方式。連續分配的特點就是為用戶進程分配的必須是一個連續的內存空間。那么我們分別介紹了單一連續分配、固定分區分配和動態分區分配這三種分配方式。

那之前咱們留下了一個問題,單一連續分配和固定分區分配都不會產生外部碎片。那由於采用這兩種分配方式的情況下,不會出現那種暫時沒有被分配出去但是又由於這個空閑區間太小而沒有辦法利用的這種情況,所以這兩種分配方式是不會產生外部碎片的。那對於是否有外部碎片還是內部碎片這個知識點經常在選擇題當中進行考查,大家千萬不能死記硬背,一定要在理解了各種分配方式的規則的這種情況下,能夠自己分析到底有沒有外部碎片,有沒有內部碎片。另外,動態分區分配方式當中對外部碎片的處理“緊湊”技術也是曾經作為選擇題的選項進行考查過,這個地方也需要有一些印象。那在回收內存分區的時候我們可能會遇到的這四種情況也是曾經在真題當中考查過所以這個點也需要注意。不過只需要抓住一個它的本質,相鄰的空閑區間是需要合並的,我們只要知道這一點就可以了。另外呢我們也需要對空閑分區表和空閑分區鏈這兩種數據結構相關的概念還有它們的原理也要有一個印象。

在這個小節中我們會學習動態分區分配算法相關的知識點,

那這是我們上小節遺留下來的問題。在動態分區分配方式當中,如果有很多個空閑分區都能夠滿足進程的需求,那么我們應該選擇哪個分區進行分配呢?這是動態分區分配算法需要解決的問題。那考試當中,要求我們掌握的有這樣四種算法,首次適應、最佳適應、最壞適應、鄰近適應這四種,我們會按從上至下的順序依次講解。

首先來看首次適應算法。這種算法的思想很簡單,就是每次從低地址部分開始查找,找到第一個能夠滿足大小的空閑分區。所以按照這種思想,我們可以把空閑分區按照地址遞增的次序進行排列,而每一次分配內存的時候我們就可以順序地查找空閑分區鏈或者空閑分區表,找到第一個大小能夠滿足要求的空閑分區進行分配。那這個地方提到了空閑分區鏈和空閑分區表,這是兩種常用於表示動態分區分配算法當中內存分配情況的數據結構。那如果我們此時系統當中內存的使用情況是這樣的,那采用空閑分區表的話,我們就可以得到一個這樣的表。每一個空閑分區塊都會對應一個空閑分區表的表項,那這些空閑分區塊是按地址從低到高的順序依次進行排列的。那如果采用空閑分區鏈的話,其實也類似,也是按照地址從低到高的順序把這些空閑分區塊依次地鏈接起來。那這個算法對這兩種數據結構的操作其實是很類似的,無非就是從頭到尾依次檢索,然后找到第一個能夠滿足要求的分區。所以這個地方我們就以空閑分區鏈為例子。空閑分區表的操作其實也類似。

那按照首次適應算法的規則,那如果說此時有一個進程要求15M字節的空閑分區,那么我們會從空閑分區鏈的鏈頭開始,依次查找找到第一個能夠滿足大小的分區。那經過檢查發現第一個20M字節的這個空閑分區,已經可以滿足這個要求。

所以我們會從20M字節的空閑分區當中,摘出15M分配給進程5,於是這個地方會剩余5M字節的空閑分區。

那相應的,我們需要把空閑分區鏈的對應結點的這些數據包括分區的大小還有分區的起始地址等等這一系列的數據都進行修改。

那么此時如果還有一個進程到來,它需要8M字節的內存空間。那我們依然還是會從空閑分區鏈的鏈頭開始依次檢索,

那經過一系列的檢索會發現,

第二個空閑分區的大小是足夠的,於是我們會從第二個空閑分區10M字節當中,

摘出8M分配給進程6。那這個地方會剩余2M字節的空閑分區。所以我們和剛才一樣,也需要修改空閑分區鏈當中相應的分區大小還有分區的起始地址這一系列的信息。那這個地方就不再展開贅述。所以這就是首次適應算法的一個規則,我們按照空閑分區以地址遞增的次序進行排列,並且每一次分配內存的時候我們都會從鏈頭開始依次往后尋找,找到第一個能夠滿足要求的空閑分區進行分配。

接下來來看最佳適應算法,這種算法的思想其實也很好理解。由於動態分區分配算法是一種連續分配的方式,那既然是連續分配就意味着我們系統為各個進程分配的空間必須是連續的一整片區域。所以我們為了保證大進程到來的時候有大片的連續空間可以供大進程使用,所以我們可以嘗試盡可能多地留下大片的空閑區間。那也就是說,我們可以優先地使用更小的那些空閑區間。所以最佳適應算法會把空閑分區按照容量遞增的次序依次鏈接。那每次分配內存的時候會從頭開始依次查找空閑分區鏈或者空閑分區表,找到大小能夠滿足要求的第一個空閑分區。那由於這個空閑分區是按容量遞增的次序排序排列的,所以我們找到的第一個能夠滿足的空閑分區,一定是能夠滿足但是大小又最小的空閑分區。那這樣的話我們就可以盡可能多地留下大片的空閑分區了。那這個地方還是一樣,我們就以空閑分區鏈作為例子,空閑分區表的操作其實也類似。如果說系統當中的內存使用情況是這個樣子,那么我們按照空閑分區塊的大小從小到大也就是遞增的次序鏈接的話,那應該是4、10、20這樣的順序鏈接。如果說此時有一個新的進程到達,那這個進程需要9M字節的內存空間的話,按照最佳適應算法的規則,我們會從鏈頭開始依次往后檢索,找到第一個能夠滿足要求的空閑分區也就是10M字節。

於是我們會從這10M字節當中摘出其中的9M分配給這個進程,那這個地方就要只剩下1M字節的大小。但是由於最佳適應算法要求我們空閑分區必須按照容量遞增的次序進行鏈接,所以這個地方變成了1M之后我們就需要對這個整個空閑分區鏈進行重新排序,

那最后會更新為這個樣子,也就是把更小的這個空閑分區挪到這個鏈的鏈頭的位置。那之后如果還有另外一個進程需要到達它需要3M字節的空閑分區的話,那同樣的我們也需要從鏈頭開始依次查找,於是發現這個分區是可以滿足的。

那么第二個進程3M字節我們就可以從4M當中摘出3M給它分配,那這個地方也會變成只有1M字節的空閑分區。那我們之后就需要把這個結點對應的那些空閑分區大小、空閑分區的起始地址這些信息進行更新。那這個地方進行更新之后,整個空閑分區鏈依然是按照容量遞增的次序進行鏈接的,所以我們不需要像剛才那樣進行重新排列。那這個地方就不再展開細聊了。那從剛才的這個例子當中我們會發現最佳適應算法有一個很明顯的缺點,由於我們每一次選擇的都是最小的能夠滿足要求的空閑分區進行分配,所以我們會留下越來越多很小的、很難以利用的內存塊。比如說這個地方有1M字節這個地方又有1M字節,那假如我們所有的進程都是兩M字節以上,那這兩個地方的碎片就是我們難以利用的,所以采用這種算法的話是會產生很多很多的外部碎片的。那這是最佳適應算法的一個缺點。

那於是為了解決這個問題,人們又提出了最壞適應算法。它的算法思想和最佳適應剛好相反,由於最佳適應算法留下了太多難以利用的小碎片,所以我們可以考慮在每次分配的時候優先使用最大的那些連續空閑區,這樣的話我們進行分配之后,剩余的那些空閑區就不會太小,所以如果采用最壞適應算法的話,我們可以把空閑分區按照容量遞減的次序進行排列。而每一次分配內存的時候就順序地查找空閑分區鏈,找出大小能夠滿足要求的第一個空閑分區。那由於這個地方空閑分區是按容量遞減的次序進行排列的,所以鏈頭第一個位置的那個空閑分區肯定是能夠滿足要求的。如果第一個都滿足不了要求,那剩下的后面的那些空閑分區,肯定都比第一個空閑分區更小,那別的那些空閑分區肯定也不會滿足。那還是來看一個具體的例子。假設此時系統當中內存使用情況是這樣。那我們采用空閑分區表和空閑分區鏈可以表示出此時的這些空閑分區的情況。那按照最壞適應算法的規則,我們需要按照容量遞減的次序依次把這些空閑分區進行排列,也就是20、10、4。那此時假如有個進程它需要3M大小的內存空間,那由於鏈頭的第一個空閑分區就可以滿足,所以我們會從其中摘出3M進行分配,

那這個地方就變成了還剩17M。那接下來還有一個進程也到達,它需要9M內存,

那同樣的我們也是從這鏈頭的這17M當中摘出其中的9M分配給進程6,於是進行數據的更新。那更新了之后我們會發現,

此時這個空閑分區鏈,已經不是按照容量遞減的次序進行排列的,所以我們需要把這個空閑分區鏈進行重新排序,也就是變成這個樣子,10、8、4,依然保持按容量遞減的次序進行鏈接,那如果有下一個進程到達的話,那我們第一個需要檢查的就是10這個空閑分區。那從這個例子當中可以看到,最壞適應算法確實解決了剛才最佳適應算法留下了太多難以利用的碎片的問題。但是最壞適應算法又造成了一個新的問題,由於我們每次都是選擇最大的分區進行分配,所以這就會導致我們的那些大分區會不斷不斷地被分割為一個一個小分區。那如果之后有一個大進程到達的話就沒有連續的大分區可用了。比如說此時來了一個20M的大進程,那這個大進程就無處安放。所以這是最壞適應算法的一個明顯的缺點。

那接下來我們再來看第四種,鄰近適應算法,這種算法的思想其實是為了解決首次適應算法當中存在的一個問題。首次適應算法每一次都會從鏈頭開始查找,這有可能會導致低地址部分會出現很多很小的難以利用的空閑分區,也就是碎片。但是由於首次適應算法又必須按照地址從低到高的次序來排列這些空閑分區,所以我們在每次分配查找的時候都需要經過低地址部分那些很小的分區,這樣的話就有可能會增加查找的一個開銷。所以如果我們能夠從每次都從上一次查找結束的位置開始往后檢索的話,是不是就可以夠解決之前所說的這個問題了呢?所以鄰近適應算法和首次適應算法很像,它也是把空閑分區按照地址遞增的順序進行排列,當然我們可以把它排成一個循環鏈表,這樣的話比較方便我們檢索。那每一次分配內存的時候都是從上次結束的位置開始往后查找,找到大小能夠滿足的第一個空閑分區。那假如說此時系統當中的內存使用情況是這樣,那我們可以把這些空閑分區按照地址遞增的次序依次進行排列,排成一個循環鏈表。那剛開始如果說有一個進程到達,它需要5M字節的內存空間,剛開始我們會從鏈頭的位置開始查找,

那第一個不滿足,

第二個6M是滿足的。

於是我們會從6M當中摘出5M分配給它,

那這個地方就還剩余1M字節。於是我們需要更新這個分區鏈當中對應的結點,包括分區的大小還有分區的起始地址。但是有沒有發現,采用鄰近適應算法還有首次適應算法,我們只需要按照地址依次遞增的次序來進行排列,所以即使這個地方內存分區的大小發生了一個比較大的變化,但是我們依然不需要對整個鏈表進行重新排列,所以這也是鄰近適應算法還有首次適應算法比最佳適應算法和最壞適應算法更好的一個地方。算法的開銷會比較小,不需要我們再花額外的時間對這個鏈表進行重新排列。

那假如此時有一個新的進程到達,它需要5M字節的空間。那按照鄰近適應算法的規則,我們只需要從上一次查找到的這個位置依次再往后查找就可以了,

所以這個不滿足,

那我們看下一個,10M是滿足的,

於是會從10M當中摘出5M進行分配,

然后更新相應的這些數據結構。那這個地方大家有沒有發現,如果此時我們采用的是首次適應算法的話,如果此時需要分配5M的內存空間,那么我們依然會從鏈首的位置開始往后查找,所以第一個4M不滿足,第二個1M不滿足,第三個10M才能滿足,那就會有三次查找。那如果說我們采用的是鄰近適應算法的話,我們只需要從這個位置開始往后查找,也就是查兩次就可以了,所以這是鄰近適應算法比首次適應算法更優秀的一個地方。首次適應算法會導致低地址部分留下一些比較小的碎片,但是我們每一次開始檢索都需要從低地址部分的這些小碎片開始往后檢索,所以這就會導致首次適應算法在查找的時候可能會多花一些時間,不過這並不意味着鄰近適應算法就比首次適應算法更優秀很多。

其實鄰近適應算法又造成了一個新的問題。在首次適應算法當中,我們每次都需要從低地址部分的那些小分區開始依次往后檢索,但是這種規則也決定了,如果說在低地址部分有更小的分區可以滿足我們的需求的時候,我們就會優先地使用低地址部分的那些小分區,這樣的話就意味着高地址部分的那些大分區就有更大的可能性被保留下來。所以其實首次適應算法當中也隱含了一點最佳適應算法的優點。那如果我們采用的是鄰近適應算法的話,由於我們每一次都是從上一次檢查的位置開始往后檢查,所以我們無論是低地址部分還是高地址部分的空閑分區,其實都是有相同的概率被使用到的,所以這就導致了和首次適應算法相比,高地址部分的那些大分區,更有可能被使用被划分成小分區,這樣的話高地址部分的那些大分區也很有可能被我們用完,那之后如果有大進程到達的話就沒有那種連續的空閑分區可以進行分配了。所以其實鄰近適應算法的這種策略也隱含了一點最大適應算法的缺點。所以綜合來看,其實剛才介紹的這四種適應算法當中,反而首次適應算法的效果是最好的。

好的那么這個小節我們介紹了四種動態分區分配算法,分別是首次適應、最佳適應、最壞適應和鄰近適應。那這個小節的內容很容易作為選擇題進行考查,甚至有可能作為大題進行考查。其實我們只需要理解各個算法的算法核心思想就可以分析出這些算法的這些空閑分區應該怎么排列,它們的優點是什么,缺點是什么。那這幾個算法當中,比較不容易理解的其實是鄰近適應算法的優點和缺點,但是剛才咱們也進行了詳細的分析這兒就不再重復了。那這個地方大家會發現,各個算法提到的算法開銷的大小問題,那這個地方的算法開銷指的是為了保證我們的空閑分區是按照我們規定的這種次序排列的,在最佳適應和最壞適應這兩種算法當中,我們可能需要經常對整個空閑分區鏈進行重新排序,所以這就導致了算法開銷更大的問題。而首次適應和鄰近適應我們並不需要對整個空閑分區鏈進行順序地檢查和排序,所以這兩種算法的開銷是要更小的。那么這些算法大家還需要通過課后習題的動手實踐來進行進一步的鞏固。

在這個小節中我們會學習一個很重要的高頻考點,同時也是這門課的難點,叫做分頁存儲管理。

那在之前的小節中我們學習了幾種連續分配存儲管理方式,所謂的連續分配就是指,操作系統給用戶進程分配的是一片連續的內存區域,而非連續分配就是指,它給用戶進程分配的可以是一些離散的、不連續的內存區域。那這個小節我們會首先學習第一種,非連續的分配管理方式,叫做基本分頁存儲管理。

那首先來認識一下什么叫分頁存儲。那如果一個系統支持分頁存儲的話,那么系統會把內存分為一個一個大小相等的區域,比如說一個區域的大小是4KB,那這樣的一個區域稱為一個頁框或者叫一個頁幀,當然它還有別的一些名詞,不同的教材或者不同的題目上大家可能會看到各種各樣的名詞出現,不過需要知道它們指的都是頁框。那系統會給每個頁框一個編號,並且這個編號是從零開始的,這個編號就叫做頁框號,或者叫頁幀號、內存塊號、物理塊號、物理頁號。那接下來我們思考一下,內存里邊它存放的其實無非就是各個進程的數據對吧,包括進程的代碼啊、進程的指令啊等等這些數據,所以為了把各個進程的這些數據把它放到各個頁框當中,因此操作系統也會把各個進程的這些邏輯地址空間把它分為與這個頁框大小相等的一個一個的部分。比如說我們這個地方舉的例子進程A,它的邏輯地址空間是0-16K-1,也就是16K,所以這個進程的大小應該是16KB這么多。把它分為與頁框大小相等的一個一個部分,因此每個部分就是4KB這么多。並且系統也會給進程的各個頁進行一個編號,這個編號就稱作為頁號或者叫頁面號。

那進程的各個頁會被放到內存的各個頁框當中,所以進程的頁面和內存的頁框是有一一對應、一一映射的關系的。那這個地方建議大家暫停,好好地來區分一下這幾個很容易混淆的概念,特別是頁、頁面、頁框和頁幀。這四個術語在剛開始學習的時候,很容易認為它們指的是同一個東西。但其實不是,頁框和頁幀它指的是內存在物理上被划分為的這樣一個一個的部分,這個叫頁框。而頁和頁面指的是進程在邏輯上被划分為的一個一個的部分。那除了頁框頁幀之外,有的教材當中也會把頁框稱為內存塊、物理塊或者叫物理頁面,並且在我們的課后習題當中,這些名詞都有可能出現,所以這個地方建議大家特別注意一下這些很容易混淆的概念。那到這兒我們就初步了解了什么叫分頁存儲。接下來要思考的問題是這樣的,剛才我們不是說進程的頁面和內存的這個頁框它有一一對應的關系嗎?那操作系統是怎么記錄這種一一對應關系的呢?

這就涉及到一個很重要的數據結構,叫做頁表。操作系統會給每一個進程都建立一張頁表。並且這個頁表一般是存放在內存的控制塊當中的,也就是PCB當中。那剛才我們說過,進程的邏輯地址空間會被分為一個一個的頁面,那每一個頁面就會對應頁表當中的一個頁表項。所謂的頁表項,大家可以理解為就是這個頁表當中的一行。那頁表項當中包含了頁號和塊號這樣的兩個數據,所以這樣的一個頁表就可以記錄下來這個進程的各個頁面和實際存放的內存塊之間的映射關系。注意內存塊其實就是頁框,只不過內存塊這個術語可能更不容易讓人混淆一些,所以我們在接下來的講解當中更多地會使用的是內存塊這樣的表述方式。不過大家自己答題的時候,建議使用頁框這個術語。因為去看英文書的話,其實這個術語它的英文叫做page frame,所以大部分的教材其實習慣翻譯成頁框。因此,建議大家答題的時候使用的是頁框這個術語。好的,那么再回到頁表這個數據結構,從剛才的分析當中我們知道,頁表它由這樣一個一個的頁表項組成。那接下來我們要思考的問題是這樣的,首先,這些頁表項是存在內存里的,那每一個頁表項需要占幾個字節的空間呢?第二個問題是操作系統要怎么利用頁表來實現邏輯地址到物理地址的轉換。

那首先我們來分析第一個問題,直接結合一個例子來理解。不過呢計算機分配存儲空間它是以字節為單位分配,而不是以比特為單位分配。

1GB=2^10MB=2^20KB=2^30B 4GB=2^32B  1KB=2^10B 4KB=2^12B  20bit<3B

那接下來我們再來看一下這個頁號又需要占多少個字節呢?直接告訴大家答案。頁號是不需要占存儲空間的。因為各個頁表項在內存中連續存放,所以頁號可以是隱含的。什么意思呢?那剛才我們得出的結果是一個塊號它至少需要占用三個字節,並且這些頁表項在內存當中都是連續存放的。那如果在內存中只存儲塊號而沒有存儲頁號的話,那我們又怎么找到頁號為i的這個頁面對應的頁表項呢?其實很簡單,只要我們知道了這個頁表它在內存當中存放的起始地址X,我們就可以用X+3*I就得出這個i號頁表項它的存放地址了。那學過數據結構的線性表,相信這個地方並不難理解。其實就相當於是一個數組,對於普通的數組而言,數組的下標我們也不需要花存儲空間來存放對吧。因此我們得出結論,頁表當中的這個頁號可以是隱含的,它並不占用存儲空間。那結合之前的結論我們知道,一個頁表項它在邏輯上其實是包含了頁號和塊號這樣的兩個信息,但是在物理上它其實只需要存放塊號的這個信息,只有塊號需要占用存儲空間。那如果這個進程它的頁號是0-n號,也就是說它總共有n+1個頁面的話,那么存儲這個進程的頁表就至少需要3*(n+1)這么多個字節。那我們通過頁表可以知道各個頁面它存放在哪個內存塊當中。

但是需要注意、需要強調的是,這個地方它記錄的只是內存的塊號,而不是具體的內存塊的起始地址。如果我們要計算一個內存塊的起始地址的話,我們需要用這個塊號再乘以內存塊的大小。這個地方大家需要特別地注意體會一下,不然做題的時候很容易出錯。好的那么到這兒我們就弄清楚了第一個問題。

接下來要探索的是第二個問題,如何實現地址的轉換,也就是邏輯地址轉換到物理地址。那我們先來回憶一下,我們之前在講連續存放那種方式的時候,操作系統是怎么實現這種地址的轉換的呢?如果一個進程它在內存當中連續存放,那么我們只需要知道這個進程它的起始地址,然后把接下來要訪問的那個邏輯地址和起始地址相加就可以得到它最終的物理地址了,那這是連續存放的時候。那這個邏輯地址我們可以把它理解為是一種偏移量,也就是說相對於它的起始地址而言往后偏移了多少。

那如果采用分頁存儲的話,那這個地址轉換要怎么進行呢?

這個進程會被放到內存的各個位置當中,不過有這樣的一個特點,雖然進程的各個頁面在內存中是離散的存放的,但是各個頁面的內部它都是連續的。注意體會這個特點。那基於這個特點,我們來看一下,如果要訪問邏輯地址A,應該怎么來進行呢?首先我們可以確定這個邏輯地址A,它應該對應的是進程的哪個頁面。也就是說要確定這個邏輯地址A它所對應的頁號。接下來操作系統就可以用這個頁號去查詢頁表,然后找到這個頁面它存放在內存當中的什么位置。那第三步我們要確定的是,邏輯地址A它相對於這個頁面的起始位置而言的“偏移量”是多少。因為各個頁面內部都是連續存放的嘛,所以我們只需要把這個邏輯地址A它所對應的頁面在內存當中的起始地址,再加上這個邏輯地址的頁內偏移量W,就可以得到這個邏輯地址A所對應的物理地址了。那這個就是實現地址變換的一個基本的思路。那在之前的講解當中我們了解了怎么利用頁表來找到一個頁面在內存當中的起始地址。

那接下來我們要探討的就是怎么確定邏輯地址所對應的頁號和頁內偏移量。

還是結合一個例子來理解。

那在這個例子當中,一個頁面的大小是50個字節。那熟悉二進制乘法或者無符號左移、無符號右移這些操作的同學,可能很容易理解這個原理。但對於跨考的同學來說也許會覺得它比較神奇但不知道為什么會這樣。那如果想要了解呈現這種規律背后的原理的話,建議可以去看一下無符號左移、無符號右移和二進制的乘法、二進制的除法之間的一個聯系。好的扯遠了,回到我們的這個主題上來。

那除此之外它還有另外一個優點。我們剛才講頁表的時候強調過一個問題,頁表當中記錄的是內存塊號而不是內存塊的起始地址,所以如果我們要計算一個內存塊的起始地址的話,需要進行一個這樣的乘法運算。但是如果內存塊的大小剛好是2的整數冪,計算起來就沒有那么麻煩。我們假設1號頁面它存放的內存塊號是9,如果用二進制表示的話9這個數就應該是1001。那這么完美的特性其實就是因為頁面大小、內存塊的大小剛好是2的整數次冪,所以在地址轉換的過程當中,我們只要查到頁表當中存放的這個內存塊號,再把這個內存塊號和邏輯地址的頁內偏移量進行一個拼接其實就可以得到最終的物理地址了。如果不是2的整數次冪的話,頁面在內存中的起始地址必須用這樣的乘法的方式來進行,這也會導致硬件的效率降低。

那經過剛才的這兩個例子我們可以看到,頁面大小是2的整數次冪有這樣的兩個好處。這個地方大家再結合文字好好體會一下就可以了,就不再重復。

那如果頁面大小是2的整數次冪的話,我們可以把邏輯地址把它分為這樣的兩個部分,分別是頁號和頁內偏移量。總之呢,只要知道頁內偏移量的位數就可以推出頁面大小,同樣的知道頁面大小也可以反推出頁內偏移量應該占多少位,從而就可以確定邏輯地址的結構,這一點也是考題當中非常非常高頻的一個考點,大家在做題的時候會經常遇到。當然,有的題目當中它的頁面大小有可能不是2的整數次冪,那對於這種題目來說我們要計算頁號和頁內偏移量,還是只能用最原始的那種算法,用除法來得到頁號,用取余得到頁內偏移量。

系統會把進程分頁,會把各個頁面離散地放到各個內存塊當中,或者說放到各個頁框當中。那由於各個頁面會依次放到各個內存塊當中,所以需要記錄這種頁面和內存塊之間的映射關系,因此需要有一個很重要的數據結構叫做頁表。頁表由一個一個的頁表項組成,並且頁表項在內存中是連續存放的,各個頁表項大小相等。注意,頁號是隱含的,不需要占用存儲空間。那我們只需要知道頁表在內存當中存放的起始地址並且知道頁號和頁表項的大小就可以算出i號頁表項存放在什么位置了。那最后我們還介紹了分頁存儲的邏輯地址結構,可以分為頁號和頁內偏移量這樣兩個部分。如果頁面的大小剛好是2的整數次冪,那么硬件在拆分邏輯地址,在進行物理地址的計算的時候,都會更快。所以一般來說,頁面大小都是2的整數次冪。當然,這個小節中我們還介紹了在分頁存儲這種管理方式當中,怎么實現邏輯地址到物理地址的轉換,具體的轉換過程大家現在只需要有個大體的印象就可以。下個小節當中我們還會結合一些硬件的細節,再進一步地闡述地址轉換的過程。

那這個小節的內容也屬於基本分頁存儲管理。其實所謂的基本地址變換機構,就是在基本分頁存儲管理當中用於實現邏輯地址到物理地址轉換的一組硬件機構。那我們在學習這個小節的過程當中,需要重點掌握這些變換機構的工作原理還有流程,這個小節的內容十分重要,既有可能作為選擇題也有可能結合大題進行考查。

那通過上個小節的講解我們知道,在分頁存儲管理當中,如果要把邏輯地址轉換成物理地址的話,總共需要做四件事,第一,要知道邏輯地址對應的頁號。第二,還需要知道邏輯地址對應的頁內偏移量,第三我們需要知道邏輯地址對應的頁面在內存當中存放的位置到底是多少。第四,我們再根據這個頁面在內存當中的起始位置和頁內偏移量就可以得到最終的物理地址了。那為了實現這個地址轉換的功能,系統當中會設置一個頁表寄存器,用來存放頁表在內存當中的起始地址還有頁表的長度這兩個信息。在進程沒有上處理機運行的時候,頁表的起始地址還有頁表長度這兩個信息是放在進程控制塊里的。只有當進程被調度,需要上處理機的時候,操作系統內核才會把這兩個數據放到頁表寄存器當中。那我們接下來用一個動畫的形式看一下從邏輯地址到物理地址的轉換應該是什么樣一個過程。

我們知道操作系統會把內存分為系統區和用戶區,那在系統區當中會存放着一些操作系統對整個計算機軟硬件進行管理的一些相關的數據結構,包括進程控制塊PCB也是存放在系統區當中的。那如果說一個進程被調度,它需要上處理機運行的話,進程切換相關的那些內核程序就會把這個進程的運行環境給恢復,那這些進程運行環境相關的信息本來是保存在PCB當中的。那之后這個內核程序會把這些信息把它放到相應的一系列寄存器當中,包括頁表寄存器。頁表寄存器當中存放着這個進程的頁表的起始地址還有頁表的長度,另外呢像程序計數器PC也是需要恢復的。程序計數器是指向這個進程下一條需要執行的指令的邏輯地址,邏輯地址A。那么接下來我們來看一下怎么把這個邏輯地址轉換成實際的物理地址,也就是說CPU怎么在內存當中找到接下來要執行的這條指令。

那從上個小節的講解中我們知道,采用分頁存儲管理方式的這種系統當中,邏輯地址結構肯定是固定不變的。在一個邏輯地址當中,頁號有多少位,頁內偏移量有多少位這些操作系統都是知道的。所以只要知道了邏輯地址A,那么就可以很快地切分出頁號和頁內偏移量這樣的兩個部分。那接下來會對頁號的合法性進行一個檢查。一個進程的頁表長度M指的是這個進程的頁表當中有M個頁表項,也就意味着這個進程的頁面總共有M頁。所以如果此時想要訪問的頁號已經超出了這個進程的頁面數量的話,那么就會認為此時想要訪問的這個邏輯地址是非法的,這樣就需要拋出一個越界中斷。那如果說這個頁號是合法的,

那么接下來會用這個頁號和頁表始址來進行計算,找到這個頁號對應的頁表項到底是多少。那通過上個小節的講解我們知道,頁表當中的每一個頁表項的長度其實是相同的,所以其實只要我們知道了頁號還有頁表起始地址,再知道我們每一個頁表項的長度,我們就可以算出我們想要訪問的頁號對應的頁表項所存放的位置。那既然知道了它存放的內存塊號,我們就可以再用內存塊號結合內存偏移量得到最終的物理地址,然后就可以順利地訪問邏輯地址A所對應的那個內存單元了。所以整個過程做了這樣幾件事,第一是根據邏輯地址算出了頁號和頁內偏移量。第二需要檢查這個頁號是否越界,是否合法。第三,如果這個頁號是合法的,那么我們會根據頁號還有頁表始址來計算出這個頁號對應的頁表項應該是在什么地方,然后找到相應的頁表項。第四,在我們得知了這個頁面存放的內存塊號之后,我們就可以用內存塊號還有頁內偏移量來計算出最終的物理地址。然后最后再對這個物理地址進行訪問。那在考試當中,經常會給出一個邏輯地址還有頁表然后讓我們計算對應的物理地址,所以大家需要對上面所說的這些過程都非常熟悉。

那接下來我們再用文字的方式再給出一個描述,雖然說這個內容比較重復,但是也是因為這個部分的內容極其重要,所以想多讓大家過幾遍。特別是頁表長度還有頁表項長度這兩個概念一定要着重注意一下。

那這個地方的驗證這兒就暫時不展開,大家下去動手嘗試一下。

頁號2對應的內存塊號b=8,也就是2號頁面應該存在內存塊號為8的地方。按字節尋址就意味着這個系統當中每個地址對應的是一個字節。邏輯地址結構中,頁內偏移量占10位,這個信息很重要,頁內偏移量的位數其實就直接決定了一個頁面的大小是多少。那么偏移量占10位的話,那么就說明一個頁面的大小是2的10次方個字節,也就是1KB。所以這種說法和上面這種說法其實是等價的,在做題的時候一定要注意這個頁內偏移量還有頁面大小之間的這種對應關系。那進行地址的轉換第一步我們應該根據這個條件算出頁號和頁內偏移量。由於題目當中給出的是這種十進制表示的邏輯地址,所以我們用除法還有取余操作這樣的方式來計算會更方便一些。而根據題目當中給出的條件,頁號2對應的內存塊號b=8,也就說明,頁號為2的頁表項是存在的,因此頁號2肯定沒有越界。並且查詢頁表之后已經知道這個頁面應該是存放在內存塊號為8的地方。那第三步,我們知道了內存塊號、知道了頁號、頁內偏移量我們就可以計算物理地址。物理地址=內存塊號*每個頁面的大小(或者說每一個內存塊的大小)+頁內偏移量。其實在分頁存儲管理(頁式存儲管理)的系統當中,只要我們確定了每個頁面的大小是多少,那么邏輯地址的結構肯定就已經確定了。所以頁式管理當中的地址是一維的,我們並不需要告訴系統除了邏輯地址以外的別的信息,不需要顯式地告訴它頁內偏移量占多少,頁號占多少。因為這些信息都是確定的,所以在頁式管理當中,我們想要讓系統把邏輯地址轉換成物理地址,只需要告訴系統一個信息,也就是邏輯地址的值,不需要再告訴系統別的任何信息。那因為只需要告訴它一個信息,因此這個地址是一維的。那這就是我們手動地模擬基本地址變換機構轉換地址的一個過程。很多初學者會忽略的是,對頁號進行越界檢查的這一步操作,所以這個地方需要留個心眼。

但是1365個頁表項並不能占滿整個頁框。這個頁框還會剩余一個字節的頁內碎片。那由於這個地方只剩一個字節的空閑區域了,所以下一個頁表項只能存放在下一個頁框當中,它不能跨頁框地存儲。+1就是為了消除這一字節剩余的誤差。所以說可以發現,如果說我們的這些頁表項並不能裝滿整個頁框的話,那在查找頁表項的時候其實是會造成一些麻煩的。所以為了解決這個問題,我們可以把每個頁表項的長度再拓展一下,把它拓展到四個字節。這樣的話我們就可以保證每個頁框剛好可以存放整數個1024個頁表項,並且不會有任何的這種頁內碎片,

就像這個樣子。這樣的話,我們要查詢1024號的頁表項,我們就不需要像上面這么麻煩了。因為這個頁框當中不會有任何的頁內碎片,所以在理論上來說,頁表項的長度最短三個字節就可以表示所有的這些內存塊號的范圍。但實際的應用當中,為了方便頁表的查詢,經常會讓一個頁表項占更多的字節,使得每個頁面恰好可以裝得下整數個頁表項。不過即使這個頁表項長度是3個字節,其實也沒問題,只不過在查詢頁表的時候可能會需要做一些更麻煩的處理。如果在題目當中要我們算頁表項的長度最小應該是多少,那我們按照3字節這樣的思路來處理就可以了。四個字節這樣的處理只是實際應用當中為了方便而采用的一種策略。那經過剛才的這個例子大家有沒有發現,一個進程如果它的頁表太大,也就是頁表項太多的話,那么這個進程的頁表一般來說裝到內存里也是會盡可能地讓它裝在連續的一些內存塊當中。因為這樣的話我們都可以用一個統一的計算方式就可以得到我們想要得到的那個頁表項所存儲的位置。

好的,那么在這個小節當中我們學習了如何使用基本地址變換機構這一系列的硬件來實現地址轉換的一個過程。那基本地址變換機構當中,最重要的硬件就是頁表寄存器。大家需要知道頁表寄存器有什么作用。那這個小節中,最重要的是要掌握地址變換的整個過程。我們要知道計算機是怎么一步一步實現這些地址變換的,並且還要能用手動的方式、手算的方式來模擬出整個地址變換的過程。那這一部分是大題和小題的極高頻的出題點。那除了地址變換過程之外,我們在講解的過程中,也補充了一些小的細節。比如說頁內偏移量的位數和頁面大小之間是有一個對應關系的。那如果說題目當中給出了頁內偏移量的位數,大家需要能夠推出頁面的大小。同樣的,如果告知我們頁面大小,也要能夠推出頁內偏移量的位數。如果知道地址、邏輯地址的總位數的話,我們還要能夠寫出整個邏輯地址的地址結構。那這個小知識點在計算題當中是很容易用到的。那除了這個之外,頁式管理的地址是一維的。這一點也經常在選擇題當中進行考查。那大家要理解什么叫一維,所謂的一維就是說,我們要讓CPU幫我們找到某一個邏輯地址對應的物理地址的話,我們只需要告訴CPU一個信息,也就是邏輯地址的值,並不需要再告訴它其他的任何信息,所以這是一維的含義。那另外的兩個小細節只是為了能夠讓大家更充分地了解這種頁式管理的這種機制才補充的,當然考試當中一般來說不會考查。那除了這些內容之外,我們還需要注意一個很重要的知識點。在CPU得到一個想要訪問的邏輯地址之后,一直到實際訪問的這個邏輯地址對應的內存單元的整個過程當中,總共需要進行兩次訪問內存的操作。第一次訪問內存是在查詢頁表的時候進行的,第二次訪問內存是在實際訪問目標內存單元的時候進行的。那在下個小節當中我們會探討一種新的地址變換機構,是否能用一種別的地址變換機構來減少訪問內存的次數,從而加快整個地址變換還有訪問的過程呢?那這是下個小節想要探討的問題。

在這個小節中我們會學習具有快表的地址變換機構。

那上個小節中我們學了基本地址變換機構,還有邏輯地址到物理地址轉換的一個過程。那在基本地址變換機構的基礎上,如果引入了快表的話,就可以讓這個地址變換的過程更快,所以這個小節中我們首先會介紹什么是快表,並且會介紹引入了快表之后,地址變換的過程有什么區別。最后我們會解釋為什么引入快表之后,可以讓計算機的整體效率、整體性能都得到很高的提升。

 注意TLB它不是內存,它是一種高速緩存。那快表中存放的是最近我們訪問過的一些頁表項的副本,這樣的設計可以讓地址變換速度更快。頁表其實是存放在內存當中的,在引入了快表之后,我們可以把存放在內存中的頁表稱為慢表。因為訪問內存中的這個頁表的速度更慢,而訪問快表當中存放的這些頁表項的速度會更快,所以這是快表和慢表名字的由來。但是由於硬盤的讀寫速度很慢,而CPU處理數據的速度又很快,因為硬盤速度慢而拖累CPU的速度,導致系統整體性能的降低。內存的速度要比硬盤快好幾十倍,所以我們把CPU要訪問的那些數據先放到內存中就可以緩和CPU和硬盤之間的速度矛盾。把內存當中最近有可能會被頻繁訪問到的東西放到高速緩存里,進一步地緩和CPU和存儲設備之間的一個速度矛盾。高速緩存它本質上也是用於存取數據的一個硬件設備。緩存並不是內存,CPU訪問高速緩存的速度要比訪問內存的速度要快的多。因此如果我們可以把最近想要訪問的那些頁表項的副本把它存到這個快表這種高速緩存當中,那么CPU在地址變換的時候查詢頁表的這個速度就會快的多了。快表TLB它和我們平時所說的那種狹義上的高速緩存,狹義上的Cache其實也是有區別的。快表的查詢速度要比慢表快很多。

那接下來我們要探討的問題是,既然快表的查詢速度快那么多,那能不能把整個頁表都放在快表當中呢?其實這個原因不難理解,因為快表這種存儲硬件的造價更貴,因此在成本相同的情況下,快表可以存的東西肯定沒有那么多。所以我們系統當中存儲分級的這個思想和我們這兒提到的這個例子其實是一模一樣的。

所以為了兼顧系統整體的運行效率,同時也要考慮這個造價成本,因此才采用了這種多級的存儲設備。好的那么剛才我們從硬件的角度理解了快表為什么要比慢表更快,那接下來我們再從這個操作系統的角度來看一下快表到底有什么作用。

我們來看這樣的一個例子,(0,0)、(0,4)、(0,8)這樣的幾個邏輯地址,那前面的這個是指頁號,后面的這個指的是頁內偏移量。這個進程的頁表存放在內存當中,是這個樣子。那當這個進程上處理機運行的時候,系統會清空快表的內容。注意啊,快表是一個專門的硬件,當進程切換的時候,快表的內容也需要被清除。

那我們假設訪問快表、訪問TLB只需要1微秒的時間,而訪問內存需要100微秒的時間。接下來我們來看一下快表是如何工作的。

那首先這個進程它想要訪問的邏輯地址是頁號為0、頁內偏移量也為0的這個邏輯地址。首先這個頁號需要和頁表寄存器當中的頁表長度進行比對,進行越界異常的檢查,然后發現這個頁號並沒有越界。接下來就會查詢快表,但是由於這個進程剛上處理機運行,因此快表此時的內容是空的。在快表中找不到頁號為0所對應的頁表項,因此快表沒有命中。那由於快表沒有命中,因此接下來就不得不去訪問內存當中存放的慢表,所以接下來通過頁表始址還有頁號計算出對應的頁表項存放的位置。於是,在查詢完慢表之后就可以知道,0號頁面它所存放的內存塊號是600。注意,在訪問了這個頁表項之后,同時也會把這個頁表項把它復制一份放到快表當中。同時,剛才不是已經查到這個頁面所對應的內存塊號了嗎?那么通過這個內存塊號和頁內偏移量就可以得到最終的物理地址。最后,就可以訪問這個邏輯地址所對應的內存單元了。那這是進程訪問的第一個地址。

接下來這個進程想要訪問的地址是頁號為0、頁內偏移量為4的這個地址。那同樣的,剛開始會進行一個越界異常的判斷,發現沒有越界。所以接下來會根據頁號來查詢快表,需要確認一下這個頁號所對應的頁表項是否在快表當中。那由於剛才我們已經把它復制到了快表當中,因此這一次的查詢就可以命中。

而快表命中之后,系統就可以直接知道,0號頁面它存放的內存塊號是600,因此接下來它就不需要再查詢內存當中的慢表而是直接用這個內存塊號和頁內偏移量得到最終想要訪問的物理地址,然后進行訪存。

因此,如果快表命中的話,就不需要再訪問內存中的慢表了。

那最后的這個地址其實也是一樣的。也是會先進行越界的檢查,

然后查詢快表結果快表命中。於是系統可以直接根據查詢快表的結果,得到最終的這個物理地址,然后訪問最終需要訪問的這個內存單元。那如果這個系統中沒有快表的話,每一次地址變換的過程肯定都需要查詢內存中的慢表,而訪問一次內存需要100微秒的時間,因此每一次地址變換都需要花100微秒。而如果說引入了快表的話,那只要快表命中,我們的地址變換過程就只需要花費1微秒的時間,所以這也是為什么快表能夠加快地址變換的一個原因。

那需要注意的是,快表中存放的是進程頁表當中的一部分副本。因為之前我們已經說了,快表雖然速度更快,但是造價其實也要比內存高很多,因此為了控制成本,快表的容量就不會特別大,所以快表當中只有可能存放慢表中的一部分頁表項的副本,不過這已經可以讓系統的效率有很大的提升了,這個我們之后還會繼續細聊。

那接下來我們用文字的方式來總結一遍,引入了快表機構之后,地址變換的過程。首先通過這個邏輯地址,我們可以得到頁號和頁內偏移量,然后進行了越界判斷之后,會把這個頁號和快表當中所有的這些頁號進行對比。只不過查詢快表的速度要比查詢慢表的速度快很多。如果慢表命中,也就是找到了這個頁號對應的表項的話,那么就可以直接通過快表當中存放的那些信息,直接得到最終的物理地址,最終再訪問我們想要訪問的那個內存單元。所以在引入了快表機構之后,如果快表命中的話,我們訪問一個邏輯地址,只需要一次訪存。也就是訪問我們最終想要訪問的那個地址單元的時候才需要訪存,而地址轉換的過程當中,不需要訪存。當然,如果快表沒有命中的話,那么我們依然需要訪問內存當中的頁表,所以在這種情況下,我們要訪問一個邏輯地址就需要兩次訪存。第一次訪存是查詢內存當中存放的頁表,第二次訪存是訪問我們最終想要訪問的那個內存單元。那需要注意的是,在我們查詢慢表之后,同時也需要把慢表當中的頁表項給它復制到快表當中。而如果快表已經存滿了,那么我們需要按照一定的算法,淘汰快表當中的某一些頁表項進行替換。那這個是我們之后置換算法當中會學習的一個內容,這兒就暫時不展開。總之在引入了快表之后,系統在進行地址變換的時候,它會優先查詢快表。只有快表沒有命中的時候,它才會去查詢內存當中的頁表。那由於查詢快表的速度要比查詢慢表的速度快很多,所以這就可以使這個系統的整體效能得到提升。基於局部性原理,一般來說快表的命中率可以達到90%以上。什么是局部性原理,我們一會兒再解釋。我們先來看一下假設快表的命中率可以達到90%的話,它到底可以讓這個系統性能提升多少。那根據上面的分析我們知道,系統在訪問一個邏輯地址的時候,它首先會查詢快表,會消耗1微秒的時間。如果快表命中的話,那么系統就可以直接得到最終想要訪問的物理地址並且訪問這個物理地址對應的內存單元。那訪問這個內存單元總共需要100微秒的時間。所以如果快表命中的情況下,訪問這樣的一個地址總共就需要耗費1+100這么多的時間。那再來看第二種情況,如果快表沒有命中的話,首先系統會查詢快表消耗1微秒的時間,接下來由於快表沒有命中,所以系統需要訪問內存當中的慢表。那查詢慢表其實就需要訪問一次內存,所以這兒就需要消耗100微秒的時間。那得到最終的物理地址之后,還需要訪問最終想要訪問的內存單元,因為這兒還需要加上100微秒。那發生這種情況的概率是10%,所以我們給它乘上0.1的權重。那如果這個系統沒有快表機構的話,那每一次訪問邏輯地址肯定都需要先查詢內存中的慢表,然后最終再訪問我們的目標內存單元。總之大家在做題的時候,需要注意的點就是,題目當中有沒有告訴你快表和慢表是同時查找的。還是說,只有快表查詢未命中的時候,再查詢慢表。那不管怎樣,在引入了快表之后,肯定這個地址變換的過程都快了很多,系統效能得到了大幅度的提升。

那接下來我們來解釋一下剛才所說的這個快表和慢表同時查找到底是什么意思。我們的第一個例子當中我們是默認了系統先查詢快表,也就是先消耗了1微秒的時間。當快表查詢未命中的時候,它才會開始查詢慢表。那查詢慢表的過程又需要消耗100微秒的時間,而如果快表和慢表同時查詢的話,情況就會變成這樣。快表和慢表是同時開始查詢的,而在1微秒的時候系統發現,這個快表查詢未命中。但是在這個時刻,其實慢表也已經查了一微秒的時間,因此接下來再消耗99微秒就可以得到這個慢表的查詢結果。那通過這個甘特圖相信並不難理解,什么叫快表和慢表同時查找,什么叫先查快表,快表未命中的時候再查慢表。這是做題的時候大家需要注意的一個小細節。那接下來我們來思考一個問題,為什么TLB當中只存放了頁表中的一部分就可以讓系統的效能提升那么多呢?

這其實是因為著名的局部性原理。程序當中的變量,數組還有變量i,這些變量是存放在23號頁面當中的。因為10號頁面當中,存放的是它的這些代碼指令。而這個數組在內存中其實是連續地存放的。那由於局部性原理,也就是說這個程序在某段時間內可能會頻繁連續地訪問某幾個特定的頁面,因此在地址變換的過程中,只要它訪問的是同一個頁面,那么它查詢頁表的時候其實查到的也都是同一個頁表項。所以只要我們把慢表當中的頁表項把它復制到快表當中,那這樣就可以讓地址變換的速度快很多了,因為就不需要每次查詢慢表。那這就是為什么快表機構能夠大幅度地提升系統效能的一個原因。

在沒有引入快表之前,我們訪問一個邏輯地址至少需要兩次訪存。第一次訪存是查詢內存當中的頁表,第二次訪存才是訪問我們最終想要訪問的那個內存單元。而在引入了快表之后,如果快表命中的話,那么就只需要一次訪存。如果快表未命中的話,我們仍然需要兩次訪存,仍然需要查詢內存中的慢表。TLB當中我們只存有頁表項的副本,存放的是頁表項的副本,而普通的高速緩存當中存放的是其他數據的副本。所以TLB和Cache還是有區別的,不能混為一談。

介紹兩級頁表相關的一系列知識點。

最后我們還會強調幾個兩級頁表問題在考試當中有可能會作為考點的一個很重要的幾個細節。那我們會按照從上至下的順序依次講解。

首先來看咱們之前介紹過的單級頁表機制存在什么問題?而我們知道每一個頁面需要對應一個頁表項,那么這么多的頁面就需要對應同等的2的20次方個頁表項。而每個頁表項的大小是4個字節,所以總共就需要2的22次方個字節來存儲這個進程的頁表。那這么多的字節,總共就是2的10次方個頁框,也就是1024個頁框。但是之前咱們講過,為了實現通過頁號查詢對應的頁表項這件事情,那么一般來說整個頁表都是需要連續地存放在內存當中的。因此在這個系統當中,一個進程光它的頁表就有可能需要占用連續的1024個頁框來存放。那要為一個進程分配這么多的連續的內存空間,這顯然是比較吃力的,並且這已經喪失了我們離散分配這種存儲管理方式的最大的一個優點,所以這是單級頁表存在的第一個很明顯的缺陷、問題。

那第二個問題,由之前我們介紹過的局部性原理我們可以知道,很多時候其實進程在一段時間內只需要訪問某幾個特定的頁面就可以正常地運行了。因此,我們沒有必要讓進程的整個頁表都常駐內存,我們只需要讓進程此時會用到的那些頁面對應的頁表項在內存當中保存就可以了,所以這是單級頁表存在的第二個問題。

那么從剛才的分析當中我們知道,單級頁表存在兩個明顯的問題。第一個問題就是頁表必須連續地存放,所以如果頁表很大的話,那么光頁表就需要占用連續的很多個頁框。那這和我們離散分配存儲管理的這種思想其實是相悖的,所以我們要嘗試解決這個問題。那第二個問題就是,我們沒有必要讓整個頁表都常駐內存,因為進程在一段時間內可能只需要訪問某幾個特定的頁面就可以順利地執行了,那這是基於局部性原理得出的一個結論。那我們首先討論第一個問題應該怎么解決。其實我們可以參考一下我們之前解決進程在內存當中必須連續存儲的這個問題的時候,提出的那種思路。那我們之前的做法其實很簡單,就是把進程的地址空間進行分頁,然后再為進程建立一張頁表,用來記錄它的各個頁面之間的順序,還有保存的位置這些信息。那同樣的思路其實我們也可以用來解決一個頁表必須連續存儲、連續占用多個頁框的問題。那我們可以把這個很長的頁表進行分組,讓每一個內存塊剛好可以放入一個分組。那為了保證我們把這些分組離散地放到各個內存塊之后,還能夠知道這些分組之間的先后順序,因此我們依然是像需要模仿之前的這種思路,為這些分組再建立一個頁表,然后這個頁表就稱為頁目錄表,或者叫外層頁表,或者叫頂層頁表。當然408的真題當中比較喜歡用的是頁目錄表這個名詞。那這個地方觀看這些文字描述會比較抽象,我們直接結合圖像來進行進一步的理解。

那既然我們的頁號有20位,就意味着在這個系統當中,一個進程最多有可能會有2^20次方個頁面,那相應的也會有2^20次方個頁表項。如果用十進制表示的話,這些頁表項的編號應該是0-1048575(這其實就是2^20-1這么一個數)。那現在由於這個頁表的長度過大,所以我們按照之前所說的那種思路,我們可以把這么大的一個長長的頁表,把它拆分成一個一個的小分組,那每個小分組的大小可以讓它剛好能夠裝入一個內存塊。那我們每個內存塊或者說每個頁面的大小是4KB,而頁表項的大小是4B,所以一個內存塊、一個頁面可以存放4K/4=1K個頁表項,那么換算成十進制,就應該是1024個頁表項。因此,我們可以把這么大的頁表,拆分成一個一個的小分組,

每一個分組的頁表項有1024個,就像這個樣子。另外,我們可以給這些小頁表進行編號。那進行這樣的拆分之后,最后總共就會形成1024個一個一個的小頁表。那這個地方可以稍微注意一下的是,以前在這個大頁表當中,編號為1024的這個頁表項在進行拆分以后,應該是變成了第二個小頁表當中的第一個頁表項,所以可以看到這個頁表項和這個頁表項的這個塊號是一樣的,只不過頁號變為了從0開始。

那我們繼續往下分析,在把大頁表拆分這樣的一個一個的小頁表之后,由於每個小頁表的大小都是4KB,因此每個小頁表都可以依次放到不同的內存塊當中。所以為了記錄這些小頁表之間的相對順序,還有它們在內存當中存放的塊號、位置,

那我們需要為這些小頁表再建立上一級的頁表,這一級的頁表就叫做頁目錄表或者叫頂級頁表、外層頁表。

那相應的,這一層的小頁表我們可以把它稱為二級頁表。那從這個圖當中也可以很直觀地看到,頁目錄表其實是建立了二級頁表的頁號,還有二級頁表在內存當中存放的塊號之間的一個映射的關系。所以如果此時我們想要找到0號頁表的話,那么我們可以通過頁目錄表就可以知道0號頁表是存放在3號內存塊里的,所以只要在3號內存塊這個地方來找0號頁表就可以了。那在采用了這樣的兩級頁表結構之后,邏輯地址的結構也需要發生相應的變化。我們可以把以前的20位的頁號,拆分成兩個部分。第一個部分是10位的二進制,用來表示一級頁號,第二部分也是10位二進制,用來表示二級頁號。

那10位的二進制大家會發現,剛好是可以表示0-1023這么一個范圍,

所以用一級頁號來表示這個范圍是剛好的。

那相應的二級頁號這十個二進制位,就是用來表示二級頁表當中的這些頁號。

那接下來我們再結合這個例子來看一下我們應該怎么實現地址的變換?那么要進行這個地址變換,我們要做第一件事情就是根據我們的地址結構把邏輯地址拆分成三個部分,也就是一級頁號,二級頁號還有頁內偏移量這么三個部分。那第二步,我們可以從PCB當中知道我們的頁目錄表在內存當中存放的位置到底是哪里。

那這樣的話我們就可以根據一級頁號來查詢頁目錄表了。那一級頁號是0,所以我們查到的表項應該是這個表項。那從這個頁表項當中我們可以知道,0號的二級頁表存放在內存塊號為3號的地方,也就是這個位置。

所以我們可以從這個位置讀出二級的頁表,然后開始用二級頁號來再進行查詢。那二級頁號是1,所以我們查詢到的頁表項應該是這一項。那通過這個頁表項我們就可以知道,最終我們想要訪問的地址應該是在4號內存塊里的。

所以接下來我們就可以根據最終要訪問的內存塊號和頁內偏移量得出我們最終的物理地址了。

那由於我們想要訪問的是4號內存塊,並且每個內存塊的大小是4KB,也就是4096個字節,所以4號內存塊的起始地址應該是4*4096就等於16384。另外,頁內偏移量把它轉換為十進制之后,應該是1023。所以我們可以用內存塊的起始地址再加上頁內偏移量的這個數字就可以得到最終的物理地址,17407了。

那經過剛才的一系列分析我們就解決了我們之前提出的第一個問題。當頁表很大的時候,其實我們可以采用兩級頁表的這種結構來解決這個頁表必須連續地占用多個頁框的問題。那接下來我們再來看一下第二個問題應該怎么解決。其實如果說不讓整個頁表常駐內存的話,那么我們可以在需要訪問頁面的時候才把頁面調入內存。其實這是咱們之后會介紹的虛擬存儲技術。這個在之后的小節當中會有更詳細的介紹,這兒只是先簡單地提一下它的思想。

那我們可以給每一個頁表項增加一個標志位,用來表示這個頁表項對應的頁面到底有沒有調入內存。

那如果說此時想要訪問那個頁面暫時還沒有調入內存的話,那么就會產生一個缺頁中斷。然后操作系統負責把我們想要訪問的那個目標頁面從外存調入內存。那缺頁中斷肯定是我們在執行某一條指令,這個指令想要訪問到某一個暫時還沒有調入的頁面的時候產生的,所以這個中斷信號和當前執行的指令有關,因此這種中斷應該是屬於內中斷。那這個部分的內容咱們在之后的小節當中還會有更詳細的介紹。

那接下來我們再來強調幾個在考試當中需要特別注意的小細節。第一個,如果我們采用的是多級頁表機構的話,那么各級頁表的大小不能超過一個頁面。那這個限制的條件我們在做題的時候應該怎么應用呢?我們直接來看一個例子。那由於采用多級頁表的時候,各級頁表的大小不能超過一個頁面,所以說各級頁表當中頁表項最多不能超過2^10個。那相應的,各級頁號所占的位數也不能超過10位。所以28位的頁號我們可以把它分成3個部分,一級頁號占8位,二級頁號10位,三級頁號也占10位。那相應的,這樣的話我們就需要再建立更高一級的頁表,最終會形成三級頁表的一個結構。那三級頁表的原理,和兩級頁表的原理其實是一模一樣的,這個地方就不再展開贅述。那這個地方假如說我們只是采用了兩級頁表的結構的話,那么第一級的頁號就會占18位,也就是說在頁目錄表中,最多有可能會有2^18個頁表項。那這么多的頁表項,顯然是不能放在一個頁面里的,所以這就違背了采用多級頁表的時候,各級頁表的大小不能超過一個頁面這樣的一個條件,因此,如果我們只把它分成兩級是不夠的。那這就是我們需要注意的第一個細節,這個很有可能作為考點在選擇題甚至是結合大題來進行考查。

那第二個我們需要注意的點是,兩級頁表的訪存次數的分析。假設我們沒有采用快表機制的話,那么第一次訪存應該是訪問內存當中的頁目錄表,也就是頂級頁表。第二次訪存應該是訪問內存當中的二級頁表。第三次訪存才是訪問最終的目標內存單元。所以采用兩級頁表結構的話,我們要訪問一個邏輯地址需要進行三次訪存。那還記得我們分析的單級頁表的訪存次數問題嗎?如果采用的是單級頁表結構的話,那么第一次訪存就是查詢頁表,第二次訪存就是訪問我們最終想要訪問的內存單元。所以單級頁表在訪問一個邏輯地址的時候,只需要進行兩次訪存。因此,兩級頁表雖然解決了我們之前提出的單級頁表的那兩大問題,但是這種內存空間的利用率的上升,付出的代價就是,邏輯地址變換的時候,需要進行更多一次的訪存,這樣的話就會導致我們要訪問某一個邏輯地址的時候,需要花費更長的時間,所以這是兩級頁表相比於單級頁表來說的一個很明顯的缺點。那如果我們繼續分析三級頁表、四級頁表結構當中的訪存次數的話,會發現三級頁表訪問一個邏輯地址需要訪存四次,四級頁表需要訪存五次,五級頁表需要訪存六次。所以其實是有一個規律,如果沒有快表機構的話,那么N級頁表在訪問一個邏輯地址的時候,訪存次數應該是N+1次。那這就是我們需要注意的兩個很重要的小細節。

好的那么這個小節當中我們介紹了兩級頁表相關的知識點。我們從單級頁表存在的兩個問題出發,來依次探討了這兩個問題應該怎么解決。特別是第一個。那采用了兩級頁表結構之后,我們就可以解決第一個問題。但第二個問題的解決需要采用虛擬存儲技術,這個咱們會在之后的小節進行更詳細的講解。那在本節當中,我們需要重點理解兩級頁表的邏輯地址結構。還需要注意頁目錄表、外層頁表、頂級頁表這幾個說法,不過在408當中,最常用的是頁目錄表這個術語。另外,大家也需要理解采用了兩級頁表之后,如何實現邏輯地址到物理地址的轉換。那這個轉換過程其實和咱們之前介紹的單級頁表並沒有太大的差異,無非就是還需要多查一級的頁表而已。那這個過程需要能夠自己分析。那最后,我們強調了兩個我們需要注意的小細節,第一個小細節,多級頁表當中,各級頁表的大小不能超過一個頁面。所以說,如果兩級頁表不夠的話,那么我們可以進行更多的分級。第二個小細節,我們要需要自己能夠分析多級頁表的訪存次數,那N級頁表訪問一個邏輯地址是需要N+1次訪存的。

那另外,大家還需要能夠根據題目給出的邏輯地址位數,頁面大小,頁表項大小這幾個條件來確定多級頁表的邏輯地址結構。那這些內容還需要大家結合課后習題來進行鞏固和消化。

在這個小節中我們會學習另一種離散分配的存儲管理方方式,叫基本分段存儲管理。

那這種管理方式,和咱們之前學習的分頁存儲最大的區別其實就是,離散分配的時候,所分配的地址空間的基本單位是不同的。那這個小節中,我們會首先介紹什么是分段。那分段的這個概念、思想其實有點類似於我們分頁存儲管理當中的分頁。而之后我們會介紹什么是段表。段表就有點類似於分頁存儲管理當中的頁表。另外,在離散分配存儲管理方式當中,咱們避免不了一定要談的問題是怎么實現地址變換。最后,我們會對分段和分頁這兩種管理方式進行一個對比。那我們會按照從上至下的順序依次講解。

那首先來看一下什么是分段。每一個段就代表一個完整的邏輯模塊。比如說0號段的段名叫MAIN,然后0號段存放的就是main函數相關的一些東西。然后1號段存放的是某一個子函數。2號段存放的是進程A當中某些局部變量的這些信息。那可以看到,每一個段都會有一個段名。這個段名是程序員在編程的時候使用的。另外呢,每個段的地址都是從0開始編址的。所以,進程A本來是有16KB的地址空間。那分段之后,第一個段,0號段,它的地址空間就是0-7KB-1,總共的大小就是7KB。然后1號段是0-3K-1,總共的大小是3KB,2號段也一樣。那操作系統在為用戶進程分配內存空間的時候,是以段為單位進行分配的。每個段在內存當中會占據一些連續的內存空間,並且各段之間可以不相鄰。比如說0號段占據的是從80K這個地址開始的連續的4KB的內存空間,而1號段占據的是從120K這個地址開始連續的3KB的地址空間。那由於分段存儲管理當中,是按照邏輯功能來划分各個段的,所以用戶編程會更加方便,並且程序的可讀性會更高。比如說用戶可以用低級語言、匯編語言寫這樣兩條指令。那第一條指令是把分段D當中的A單元內的值讀到寄存器1當中。第二個指令是把寄存器1當中的內容存到X分段當中的B單元當中。那由於各個分段是按邏輯功能模塊來划分的,並且這些段名也是用戶自己定義的,所以用戶在讀這個程序的時候就知道這兩句代碼做的事情,就是把某個全局變量的值賦給X這個子函數當中的某一個變量。因此對於用戶來說采用了分段機制之后,程序的可讀性還是很高的。那在用戶編程的時候,使用的是段名來操作各個段。但是在CPU具體執行的時候,其實使用的是段號這個參數,

所以在編譯程序其實會把這些段名轉換成與它們各自相對應的這些一個一個段號,然后CPU在執行這些指令的時候,是根據段號來區分各個段的。

那在采用了分段機制之后,邏輯地址結構就變成了這個樣子。由段號和段內地址(或者叫段內偏移量)組成。比如說像這個例子當中,段內地址是占了0-15總共16位,然后段號是16-31,總共占的也是16位。那在考試當中我們需要注意的一個很高頻的考點就是,段號的位數決定了每個進程最多可以分多少個段。而段內地址的位數決定了每個段的最大長度是多少。那我們以這個例子為例來看一下16位的段號和16位的段內地址,最大可以支持幾個分段,每個段的最大長度又是多少。那我們假設這個系統是按字節編址的,也就是說一個地址對應的是一個字節的大小。那段號占16位,所以在這個系統當中,每個進程最多可以有2^16個段,也就是64K個段。因為16位的二進制數,最多也就能用來表示這樣一個范圍的數字。那同樣的,段內地址也是占16位,並且這個系統是按字節編址的,所以每個段的最大長度應該是2的16次方也就是64KB這樣的一個大小。那剛才我們提到的這兩句用匯編語言寫的指令,

在經過編譯程序編譯之后,段名會被編譯程序翻譯成對應的段號。而這里提到的A單元、B單元這樣的助記符,會被編譯程序翻譯成段內地址,也就是這個第二個部分。就像這個樣子,每個段名會被翻譯成與它們對應的各個段號,另外,各個段之間的這些用助記符表示的內存單元,會被最終翻譯為這個段當中的段內地址。那這就是分段相關的一些最基本的概念。

那接下來我們再來看下一個問題。既然我們的程序被分為了多個段,並且各個段是離散地存儲在內存當中的。

為了保證程序能夠正常地運行,所以操作系統必須能夠保證要能從物理內存當中找到各個邏輯段存放的位置。因此,為了記錄各個段的存放位置,

操作系統會建立一張段映射表,簡稱“段表”,就像這個樣子。

那用段表記錄了各個邏輯段在內存當中的存放的位置。那這個地方大家會發現,段表的作用其實和咱們之前學習的頁表的作用是比較類似的。頁表是建立了各個邏輯頁面到實際的物理頁框之間的映射關系,而段表是記錄了各個邏輯段到實際的物理內存存放位置之間的映射關系。那每個段表由段號、段長和段基址組成。這個段基址其實就是段在內存當中的存放的起始位置,那從這個圖當中我們也能很直觀地看到,每個段會對應一個段表項。那相比於頁表來說,段表當中多了一個更不同的信息就是段長,因為每個分段的長度可能是不一樣的。而我們在分頁存儲管理當中,每個頁面的長度肯定都是一樣的。所以在分頁內存管理當中,頁長是不需要這樣顯式地記錄的。但是在分段存儲管理當中,段的長度是需要這樣顯式地記錄在段表當中。

那第二點我們需要注意的是,我們的各個段表項的長度其實是相同的。也就是說,這些一行一行的段表項,在內存當中所占的空間,是大小是相同的。比如說,這個系統按照字節尋址,並且采用分段存儲管理方式。邏輯地址結構,段內地址是16位,段的長度不可能超過2的16次方字節。所以在各個段表項當中,用16位就肯定可以表示這個段的最大段長了。那假設這個系統的物理內存大小是4GB,那也就是2的32次方個字節。那這么大的物理內存的地址空間,可以用32位的二進制來表示,所以對於基址,也就是內存的某一個地址這個數據,我們只需要用32個二進制位就可以表示了。因此每個段的段表項,其實只需要16+32位也就是48位總共6個字節就可以表示一個段表項。因此在這個系統當中,操作系統可以規定每一個段表項的長度就是固定的6個字節。前兩個字節表示的是段長,而后面四個字節表示的是這個段存放的在內存當中的起始地址。

所以和頁表類似,這個地方的頁號可以是隱含的,頁號並不占存儲空間。那我們在查詢段表的時候,只要我們能夠知道段表在內存當中的起始地址M,那我們想要查詢K號段對應的段表項,那我們只需要用段表的起始地址M,再加上K乘以每個段表項的大小6個字節,那就可以得到我們想要找到的那個段對應的段表項在內存當中的什么位置了。所以即使這個段號是隱含的,沒有顯式地給出。但是我們依然可以根據段號來查詢這個段表。

那接下來我們再來看一下采用了分段存儲管理之后,地址變換的過程是什么樣的。那還是以剛才提到的這個指令為例,這個用匯編語言寫的指令經過編譯程序編譯之后,會形成一條等價的機器指令。比如說這條機器指令就是告訴CPU,從段號為2,段內地址為1024的這個內存單元當中取出內容,放到寄存器1當中。不過在計算機硬件看來,段號、段內地址這些邏輯地址其實是用二進制表示,比如說是這個樣子。那前面的紅色的這16位表示的是段號,而后面的黑色的這16位表示的是段內地址。所以CPU在執行指令的時候,或者說在訪問某一個邏輯地址的時候,需要把這個邏輯地址變換為物理地址。

那我們看一下具體的變換過程。在內存的系統區當中,存放着很多用於管理系統當中的軟硬件資源的數據結構,包括進程控制塊PCB也是存放在系統當中的。那當一個進程要上處理機運行之前,進程切換相關的那些內核程序會把進程的運行環境給恢復,那這就包括一個很重要的硬件寄存器當中的數據的恢復。這個寄存器叫做段表寄存器,用於存放這個進程對應的段表在內存當中的起始地址還有這個進程的段表長度到底是多少。因此段表存放的位置還有段表長度這兩個信息在進程沒有上處理機運行的時候是存放在進程的PCB當中的。那當進程上處理機運行的時候,這兩個信息會被放到很快的段表寄存器當中。那當知道了段表的起始地址之后,就可以知道段表是存放在內存當中的什么地方。

那接下來這個進程的運行過程當中,避免不了要訪問一些邏輯地址。

 

比如說要訪問邏輯地址A。那么系統會根據邏輯地址得到段號S和段內地址W,這是第一步要做的事。第二步,知道了段號之后,需要用段號和段表長度進行一個對比來判斷一下段號是否產生了越界。如果段號大於等於段表長度的話,就會產生越界中斷。那么接下來就會由中斷處理程序來負責處理這個中斷。如果沒有產生中斷的話,就會繼續執行下去。這個地方稍微注意一下,段號是從0開始的,段表長度至少是1,所以當S=M的時候,其實也是會產生越界中斷的。那在確定這個段號是合法的沒有越界之后,就會根據段號還有段表始址來查詢段表,找到這個段號對應的段表項。那之前咱們提過,由於各個段表項的大小是相同的,所以用段表始址+段號*段表項的長度就可以找到我們要找的目標段對應的段表項在內存中的位置了,那接下來就可以讀出這個段表項的內容。第四步,在找到了這個段號對應的段表項之后,系統還會對這個邏輯地址當中的段內地址W進行一個檢查,看看它是否已經超過了這個段的最大段長,那如果段內地址大於等於這個段的段長的話,就會產生一個越界中斷,否則繼續執行。那這一步也是和我們頁式管理當中區別最大的一個步驟。因為在頁式管理當中,每個頁面的頁長肯定是一樣的,所以系統並不需要檢查頁內偏移量是否超過了頁面的長度。但是在分段存儲管理方式當中又不同,各個段的長度不一樣,所以一定需要對段內地址進行一個越界的檢查,所以這一步是需要着重注意的。那我們繼續往下,因為我們此時已經找到了目標段的段表項,

所以我們就知道目標段存放在內存當中的什么地方。那最后我們根據這個段的基址,也就是這個段在內存當中的起始地址,再加上這個最終要訪問的段內地址就可以得到我們最終想要的物理地址了。

那我們以之前提到的這個邏輯地址為例,進行一次完整的分析。如果說此時要訪問的邏輯地址的段號是2,然后段內地址是1024的話,那首先需要用段號2和段表長度M進行一個檢查,那顯然此時這個進程的段表長度應該是3,因為它有3個段,所以段號是小於段表長度的,因此段號合法,所以就可以進行下一步,用段號和段表始址查到這個段號對應的段表項,那這樣的話就找到了2號段對應的段表項。那接下來需要對段內地址的合法性進行一個檢查。段內地址和段長進行對比,發現2號段的段長是6K,而段內地址是1024,也就是1K,所以段內地址是小於段長的,因此在這個地方並不會產生越界中斷,可以繼續進行下去。那接下來通過這個段表項我們知道了這個段在內存當中存放的起始地址是40K,所以用這個段的起始地址40K再加上段內地址W也就是1024,那這樣的話我們就得到了最終想要訪問的目標內存單元,也就是A那個變量存放的位置,那這樣的話就完成了對這個邏輯地址的一個訪問。那分段存儲管理當中的這個地址變換的過程,需要和分頁存儲管理的過程進行一個對比記憶。那其實大家着重需要關注的是,分段和分頁最大的區別就在於,在分頁當中,每個頁面的長度是相同的,而分段當中每個段的長度是不同的,所以在分頁管理當中,並不需要對頁內偏移量(頁內地址)進行越界的檢查。但是在分段管理當中,我們一定需要對段內地址也就是段內偏移量和段長進行一個對比檢查,那這就是分段和分頁這兩種存儲管理方式當中進行地址變換過程時候最大的一個區別。

那接下來我們再把分段和分頁這兩種管理方式進行一個統一的對比。在分頁的時候只考慮各個信息頁面的物理大小,比如說每個頁面是4KB。但是在分段的時候必須考慮到信息的這些邏輯關系,比如說某一個具有完整邏輯功能的模塊,單獨地划分成一個段。那另外,分段的主要目的是為了實現離散分配,提高內存利用率。但是分段的主要目的是為了更好地滿足用戶需求,方便用戶編程。所以分頁其實僅僅只是系統管理上的需要,它只是一個系統行為,對用戶是不可見的。也就是說,用戶是並不知道自己的進程到底是分為了幾個頁面,甚至不知道自己的進程是不是被分頁了,但相比之下分段對於用戶是可見的,用戶在編程的時候就需要顯式地給出段名。所以用戶其實是知道自己的程序會被分段,甚至知道會被分為幾個段,每個段的段名是多少。另外,頁的大小是固定的,並且這個頁面的大小是由系統決定的。但段的長度卻不固定,取決於用戶編寫的程序到底是什么樣一個結構。

那從地址空間的角度來說,分頁的用戶進程,地址空間是一維的。比如說,一個用戶進程的大小總共是16KB,那么在用戶看來,它的整個進程的邏輯地址空間,應該是從0-16K-1。那用戶在編程的時候,只需要用一個記憶符就可以表示一個地址,比如說用一個記憶符A來表示某個頁面當中的某一個內存單元。

但如果系統采用的是分段存儲管理的話,那么用戶進程的地址空間是二維的,用戶自己也知道自己的進程會被分為0、1、2這么幾個段,並且每個段的這個邏輯地址都是從0開始的,

所以在分段管理的這種系統當中,用戶編程的時候既需要給出段名,也需要給出段內地址。

比如說咱們之前提到的這個匯編語言指令,用戶需要顯式地給出段名還有段內地址。那因此,在分頁管理當中,在用戶自己看來,自己的這個進程的地址空間是連續的,但是在分段存儲管理當中,用戶自己也知道自己的進程地址空間是被分為了一個一個的段,並且每個段會占據一連串的連續的地址空間。因此,分頁當中進程的地址空間是一維的,而分段的時候,進程的地址空間是二維的。那這個點在選擇題當中還是很容易進行考查的。

那除了之前所說的那些不同之外,分段相比於分頁來說最大的一個優點應該是它更容易實現信息的共享和保護。比如說一個生產者進程,總共是16KB這么大,

那么它可能會被分為這樣的三個段。其中一號段是用來實現判斷緩沖區此時是否可以訪問這樣一個功能,那其實除了這個生產者進程之外,其他的生產者進程消費者進程它們也需要判斷緩沖區此時是否可以訪問。因此,這個段當中的代碼,應該允許各個生產者進程、消費者進程共享地訪問。那怎么實現共享地使用這個段呢?

假設我們的這個生產者進程它有這樣的一個段表。它的1號段也就是判斷緩沖區的那個段,是存放在內存的120K這個地址開始的這個內存空間當中的。

 

那如果說消費者進程想要和它共享地使用這個1號段的話,那么很簡單,可以讓消費者進程的某一個段表項同樣是指向這個段存放的起始地址的。所以如果我們想要實現共享的話,就要讓各個進程的某一個段表項指向同一個段就可以了。

那這個地方需要注意的是,只有純代碼或者叫可重入代碼也就是不能被修改的代碼,可以被共享地訪問。那這種代碼不屬於臨界資源,各個進程即使並發地訪問這一系列的代碼也不會因為並發產生問題。

比如說有一個代碼段只是簡單地輸出“Hello World!”這么一個字符串,那么所有的進程並發地訪問這個代碼段那顯然是不會出問題的。但是對於可修改的代碼段來說,是不可以共享的。因此,對於代碼來說,只有純代碼這種不屬於臨界資源的代碼可以被共享地訪問。那這是在分段存儲管理方式當中實現共享的一個很簡單的方式。

那接下來我們再來看一下為什么分頁管理當中不方便實現這種信息的共享。假設我們把這個消費者進程進行分頁的話,那么第一個頁是0號段當中的前半部分的位置占4KB,那第二個頁它會包含0號段當中的3KB和1號段當中的1KB,那這兩個總共組成了4KB的頁面。那類似於的,第三個頁面也會包含一半1號段的內容,還有另一半是2號段的內容。

所以如果采用分頁這種方式的話,那么我們如果讓消費者的某一個頁表項也指向這個生產者進程的分頁的話,那么顯然是不合理的。因為生產者進程的這個分頁當中,只有綠色部分是允許被消費者進程共享的,但是橙色部分不應該被消費者進程所共享。

因此,由於頁面它並不是按照邏輯模塊來進行划分的,所以我們就很難實現共享,並不像分段那么方便。 

那其實對於信息的保護,原理也是類似的。比如說在生產者進程當中,1號段應該是允許被其他進程訪問的。那我們只需要把這個段標記為允許其他進程訪問,其他的那些段標記為不允許其他進程訪問。那這就很簡單地就實現了對於各個段的保護。

但是如果采用分頁存儲管理的話,1號頁和2號頁當中只有一部分也就是綠色這些部分是允許其他進程訪問的,而其他的橙色和紫色的部分,不應該允許被其他進程訪問。所以這樣的話我們其實不太方便對各個頁面進行標記到底是否允許被其他進程訪問。因此,采用分頁存儲的時候,更不容易實現對信息的保護和共享這兩個功能。

那這是關於信息的共享和保護,通過剛才的講解,相信不難理解。那接下來我們再來探討我們在分段和分頁這兩種方式當中,訪問一個邏輯地址需要幾次訪存。如果我們采用的是單級頁表的分頁存儲管理的話,那么第一次訪存應該是查詢內存當中的頁表,第二次訪存才是查詢最終的目標內存單元。那這個過程咱們在之前已經分析過很多次,就不再展開。所以采用單級頁表的分頁存儲管理,總共需要兩次訪存。

那如果采用分段的話,第一次訪存是查詢內存當中的段表,第二次訪存是訪問目標內存單元。所以采用分段的時候,也是總共需要兩次訪存。那在分頁存儲管理當中我們知道,我們可以引入快表機構來減少在進行地址轉換的時候訪問內存的次數。所以其實在分段管理當中也類似,我們也可以引入快表機構,然后可以把近期訪問過的段表項放到快表當中,那這樣的話只要快表能夠命中,那么我們就不需要再到內存當中查詢段表,我們就可以少一次訪存。那這就是分段和分頁管理的一個對比。

那在學習了分頁存儲管理之后,這個小節的內容其實並不難理解。我們介紹了什么是分段,在分段存儲管理當中,邏輯地址結構是什么樣的。另外,我們介紹了和頁表很類似的段表,只不過對於段表來說,大家需要着重注意的是,每個段表項當中,一定會記錄這個段的段長是多少。那在分頁存儲管理當中,每個頁面的長度是不需要顯式地在頁表當中記錄的。因為各個頁面的長度一樣,而在分段存儲當中,各個段的長度是不一樣的。所以這是它們倆之間的一個最明顯的一個區別。那由於各個段的段長不一樣的,所以在地址變換的時候大家也需要注意,在找到了對應的段表項之后,還需要對段長和段內地址進行一個對比的檢查,看一下段內地址是否越界。那除了這個步驟之外,其他的那些步驟其實和頁式管理當中,地址變換的過程也是大同小異的。那分段和分頁的對比這些知識點,是很容易在選擇題當中進行考查的。所以大家還是需要理解這些點。那這個小節的內容還需要大家通過課后的習題再進行進一步的實踐鞏固,也需要能夠根據題目當中給出的信息來手動地完成這個地址變換的過程。

那段頁式管理其實是分段和分頁這兩種管理方式的一個結合。那之后我們會介紹分段和分頁這兩種方式、這兩種思想的一種結合,從而引出了段頁式管理方式。那之后我們還會介紹在段頁式管理當中,段表和頁表與分段、分頁管理當中的段表、頁表有什么相同和不同的地方。那最后我們還會介紹怎么實現從邏輯地址到物理地址的變換。那我們會按照從上至下的順序依次講解。

由於分頁是按照信息的物理結構來進行划分的,所以我們不太方便按照邏輯模塊、邏輯結構來實現對信息的共享和保護。分段是按照信息的邏輯結構來進行划分的,因此采用這種方式就很方便按照邏輯模塊實現信息的共享和保護。不過缺點呢,如果說我們的段很長的話,就需要為這個段分配很長很大的連續空間,那很多時候分配很大的連續空間會不太方便。那另外,段式管理是hi會產生外部碎片的,它產生外部碎片的原理其實和動態分區分配很類似。比如說一個系統的內存本來是空的,

那么先后來了三個分段,它們都需要占用連續的這種存儲空間。

那這個地方有4M字節的空閑區間,

那之后這個分段用完了,於是把它撤離內存。

那接下來又來了一個分段,占4M字節。

如果它占用了這個分區的話,那這個地方就會產生10M字節的一個空間。

那接下來如果上面這個段也撤離了,

那接下來再來了一個分段,也是占14M字節,

那這個地方就會產生6M字節的空閑的區間。

那在接下來如果還有一個分段到來,它總共需要占20M字節的這種連續的內存區間。那由於此時這些空閑區間並不連續,所以雖然它們的大小總和是20M字節,

但是這個分段是放不進內存當中的,因為分段必須連續地存放。所以很顯然,段式管理是會產生這些難以利用的外部碎片的。

不過,對於外部碎片的解決,其實和咱們之前介紹的那種動態分區分配也一樣,

可以通過這種緊湊的方式,

來創造出更大的一片連續的空間。

但是緊湊技術需要付出比較大的時間代價,所以顯然這種處理方式也並不是一個很完美的解決方式。所以基於分頁管理和分段管理的這些優缺點,人們又提出了分段和分頁這兩種思想的一個結合,於是產生了段頁式管理,段頁式管理就具備了分頁管理和分段管理的各自的優點。

在采用段頁式管理的系統當中,一個進程會按照邏輯模塊進行分段。之后各個段還會進行分頁,比如說每個頁面的大小是4KB,那么0號段本來是7KB它會被分為4KB和3KB這樣兩個頁面。

那對於內存來說,內存空間也會被分為大小相等的內存塊,或者叫頁框、頁幀、物理塊。那每一個內存塊的大小和系統當中頁面的大小是一樣的,也就是4KB。那最后,進程的這些頁面會被依次放到內存當中的各個內存塊當中。

那我們在上個小節中學過,如果采用的是分段管理的話,那么邏輯地址結構是由段號和段內地址組成的。而在段頁式管理當中我們會發現,一個進程被分段之后,各個段還會被再次分頁,所以對於段頁式管理來說,它的邏輯地址結構,應該是由段號、頁號還有頁內偏移量組成。那這個地方的頁號和頁內偏移量其實就是分段管理當中的段內地址進行再拆分的一個結果。

那在考試當中需要的注意的是,段號的位數決定了我們一個進程最多可以分幾個段,而頁號的位數決定了每個段最大會有多少頁,頁內偏移量的位數又決定了頁面的大小和內存塊的大小。

所以如果一個系統當中它的地址結構是這樣的,並且這個系統是按字節尋址的話,那么段號占16位,所以這個系統當中每個進程最多可以有2^16也就是64K個段。而頁號占4位,所以每個段最多會有2^4=16頁。另外頁內偏移量占12位,所以每個頁面/每個內存塊的大小是2^12=4096=4KB。

那在段頁式管理當中,分段這個過程對用戶來說是可見的,程序員在編程的時候需要顯式地給出段號和段內地址這樣兩個信息。但是把各個段進行分頁的這個過程,對用戶來說是不可見的,這只是一個系統的行為。系統會把段內地址自動地划分為頁號和頁內偏移量這樣兩個部分。所以對於用戶來說,他在編程的時候,只需要關心段號和段內地址這兩個信息,而剩下的分頁是由操作系統完成的。因此段頁式管理的地址結構是二維的。那與此相對的,段式管理當中地址結構也是二維的。而頁式管理當中,地址結構是一維的。

那與之前咱們介紹的分頁和分段管理當中的思想相同,對進程分段再分頁之后,我們也需要記錄各個段、各個頁面存放的一個位置。所以系統會為每個進程建立一個段表,進程當中的各個段會對應段表當中的一個段表項。而每個段表項由段號、頁表長度和頁表存放塊號組成。那由於每個物理塊的大小是固定的,所以只要知道頁表存放的物理塊號,其實就可以知道頁表存放的實際的物理地址到底是多少了。那比如說我們要查找0號段對應的頁表,

那么我們知道這個頁表存放在內存為1號塊的地方,也就是這個位置。於是就可以從這個內存塊當中讀出0號段對應的頁表。

那由於0號段長度是7KB,而每個頁面大小是4KB,所以它會被分成兩個頁面,相應的這兩個頁面就會依次對應頁表當中一個頁表項。每一個頁表項記錄了每一個頁面存放的內存塊號到底是多少。

所以通過剛才的講解大家會發現,在段頁式管理當中,段表的這個結構和段式管理當中的段表是不一樣的。段式管理當中的段表記錄的是段號還有段的長度,還有段的起始地址這么三個信息。而段頁式管理當中,記錄的是段號、頁表長度、頁表存放塊號這么三個信息,也就是后面的這兩個信息不太一樣。而對於頁表來說,段頁式管理和分頁管理的頁表結構基本上都是相同的,都是記錄了頁號到物理塊號的一個映射關系。那各個段表項的長度是相等的,所以段號可以是隱含的。各個頁表項的長度也是相等的,所以頁號也是可以隱含的。那這兩點咱們在之前的小節有詳細地介紹過,這兒就不再展開。那從這個分析當中我們會發現,一個進程只會對應一個段表,但是每個段會對應一個頁表,因此一個進程有可能會對應多個頁表。再重復一遍,一個進程會對應一個段表,但是一個進程有可能會對應多個頁表。

那么接下來我們再來看一下怎么實現段頁式管理當中的這種邏輯地址轉換為物理地址的這個過程。首先需要知道的是系統當中也會有一個段表寄存器這么一個硬件,然后在這個進程上處理機運行之前,會從PCB當中讀出段表始址還有段表長度這些信息然后放到段表寄存器當中。

那在進行地址轉換的時候,第一步是需要根據邏輯地址得到段號、頁號還有頁內偏移量這么三個部分。

那第二步需要把段號和段表長度進行一個對比,檢查段號是否越界,是否合法。如果越界的話就會拋出一個中斷,之后由中斷處理程序進行處理。如果沒有越界的話,就證明段號合法,就可以繼續執行。

那接下來一步可以根據段號還有段表始址來計算出這個段號對應的段表項在內存當中的位置。這樣的話,就找到了我們想要找的這個段表項。

接下來一步需要注意,由於各個段的長度是不一樣的,所以各個段把它們分頁之后,可能分為數量不等的不同的一些頁面。比如說有的段長一些,它就可以分為兩個頁面。有的段短一些,只需要用一個頁面。所以由於各個段分頁之后頁面數量可能不同,因此這個地方我們也需要對頁號的合法性進行一個檢查,看看頁號是否已經越界。如果頁號沒有超出頁表長度的話,那么就可以繼續往下執行。那通過這個頁號我們知道了頁表存放的位置,

於是就可以從這個位置讀出頁表。於是可以根據頁號來找到我們想要找的那個頁表項,那找到這個頁表項之后我們就知道這個頁面在內存當中存放的位置。

所以最后我們可以根據頁表項當中對應的這個內存塊號和頁內偏移量進行二進制的拼接,最終形成要訪問的物理地址。那最終我們就可以根據這個物理地址進行訪存,訪問目標內存單元。因此在段頁式管理當中,進行地址轉換的這個過程總共需要三次訪存。

第一次是訪問內存當中的段表,第二次訪存是訪問內存當中的頁表,第三次訪存才是訪問最終的目標內存單元。那我們之前也介紹過,在分頁和分段這兩種管理方式當中,可以用引入快表機構的方式來減少地址轉換過程當中訪存的次數。

所以這個地方我們也可以用相同的思路。我們可以引入快表機制,用段號和頁號作為快表的查詢的關鍵字。那如果快表命中的話,我們就可以知道我們最終想要訪問的那個頁面到底在什么位置。因此,只要快表命中,我們就不需要再查詢段表和頁表了,這樣的話我們僅需要一次訪存也就是最終訪問目標內存單元這一次。那么這就是段頁式管理方式當中進行地址變換的一個過程。需要着重注意的是,這一步就是檢查頁號是否越界,那這個段式存儲當中檢查段內地址是否越界是比較類似的。需要檢查的本質原因就在於各個段的長度可能是不相等的,因此需要進行這樣一個合法性的檢查。

段頁式管理當中,邏輯地址結構由段號、頁號和頁內偏移量這么三個部分構成。但是用戶在編程的時候只需要顯式地給出段號和段內地址,之后會由系統自動地把段內地址拆分為頁號和頁內偏移量這么兩個部分。因此由於用戶只需要提供段號和段內地址這么兩個信息,因此段頁式管理當中,地址結構是二維的。那顯然分段對於用戶來說是可見的,但是分頁是操作系統管理的一個行為,對於用戶來說不可見。那么在這個小節當中我們還介紹了段表和頁表的結構還有原理。需要注意的是,段頁式管理中的段表,和分段管理當中的段表結構是不太一樣的。段頁式管理當中,段表由段號、頁表長度和頁表存放地址這么三個信息組成。但是在分段管理當中,由段號、段的長度還有段的起始地址這么三個信息組成,所以段表是不太一樣的。但是頁表的話和分頁存儲當中的頁表的結構是相同的,都是由頁號還有頁面存放的內存塊號來組成。那之后我們介紹了地址變換的過程,那比起分頁和分段的地址變換過程來說,段頁式管理需要先查詢段表,之后還需要再查詢頁表,並且在找到段表項之后還需要對頁表長度還有頁號進行一個對比檢查,看看頁號是否已經越界。那同學們需要理解這個過程,能夠自己寫出來它的地址變換過程到底是什么樣的。那最后我們還分析了段頁式管理當中訪問一個邏輯地址所需要的訪存次數。第一次訪存是需要查段表,第二次訪存是查頁表,第三次訪存才是訪問目標內存單元。那如果我們引入了快表機構之后,就可以以段號還有頁號作為關鍵字去查詢快表,如果快表命中的話,那么僅需要一次訪存。

請求分頁管理方式相關的一系列知識點。

請求分頁管理方式是在基本分頁管理方式的基礎上進行拓展從而實現的一種虛擬內存管理技術。那相比於基本分頁存儲管理,操作系統還需要新增兩個最主要的功能。

第一個功能就是請求調頁的功能。系統需要判斷一個頁面是否已經調入內存,如果說還沒有調入內存,也就是頁面缺失的話,那么還需要將頁面從外存調到內存當中,那這是請求調頁功能。

第二個需要提供的功能是頁面置換功能。就是當內存暫時不夠用的時候,需要決定把哪些頁面換出到外存。

那針對於這兩個功能如何實現,我們會介紹在請求分頁管理方式當中頁表機制與基本分頁存儲管理方式當中有哪些相同和不同的地方。另外,為了實現請求調頁的功能,那請求分頁管理系統當中引入了缺頁中斷機構,

最后我們會介紹在這種管理方式當中,地址變換到底是什么樣一個過程。那在學習這個小節的時候,需要注意和基本分頁存儲管理方式進行一個對比。那首先我們來看一下這種管理方式和基本分頁管理方式的頁表機制有哪些相同和不同的地方。那我們還是從如何實現頁面置換和請求調頁這兩個功能的角度出發,來分析頁表機制應該怎么設計。

所以為了知道這些信息,那么肯定需要把這些信息記錄在某種數據結構當中。那頁表其實就是一個很好的地方。

另外,為了實現頁面置換功能,那么操作系統肯定需要通過某種規則來決定到底是把哪個頁面換出到外存,所以我們需要記錄每個頁面的一些指標,然后操作系統根據這個指標來決定到底換出哪一頁。另外呢,如果說一個頁面在內存當中沒有被修改過,那么這個頁面其實換出外存的時候不用浪費時間再寫回外存。因為它沒有修改過,所以外存當中保存的那個副本其實和內存當中的這個數據是一模一樣的。那只有頁面修改過的時候才需要把它換到外存當中,把以前舊的那個數據覆蓋。所以操作系統也需要記錄各個頁面是否被修改這樣的信息。

因此,相比於基本分頁的頁表來說,請求分頁存儲管理的頁表增加了這樣的四個字段,

第一個是狀態位,狀態位就是用於表示此時這個頁面到底是不是已經調入內存了。比如說在這個表當中,0號頁面的狀態位是0,表示0號頁面暫時還沒有調入內存,那1號頁面的狀態位是1,表示1號頁面此時已經在內存當中了。

第二個新增的數據是訪問字段。操作系統在置換頁面的時候,可以根據訪問字段的這些數據來決定到底要換出哪一個頁面。所以我們可以在訪問字段當中記錄每個頁面最近被訪問過幾次,我們可以選擇把訪問次數更少的那些頁面換出外存。或者我們也可以在訪問字段當中記錄我們上一次訪問這個頁面的時間,那這樣的話我們可以實現優先地換出很久沒有使用的頁面這樣的事情。所以這是訪問字段的功能。

那第三個新增的數據是修改位,就是用來標記這個頁面在調入內存之后是否被修改過。因為沒有被修改過的頁面是不需要再寫回外存的。那不寫回外存的話就可以節省時間。

第四個需要增加的數據就是各個頁面在外存當中存放的位置。

那這是請求分頁存儲管理方式當中的頁表新增的四個字段。

那在有的地方也會把這個頁表稱為請求頁表,然后這個頁表稱為基本頁表或者簡稱頁表。那這是請求分頁存儲管理當中頁表機制產生的一些變化,新增的一些東西。那這也是實現請求調頁和頁面置換的一個數據結構的基礎。

那為了實現請求調頁功能,系統當中需要引入缺頁中斷機構。我們直接來結合一個例子來理解缺頁中斷機構工作的一個流程。假設在一個請求分頁的系統當中,要訪問一個邏輯地址,頁號為0,頁內偏移量為1024。那么為了訪問這個邏輯地址,需要查詢頁表。那缺頁中斷機構,會根據對應的頁表項來判斷此時這個頁面是否已經在內存當中。如果說沒有在內存當中,也就是這個狀態位為0的話,那么會產生一個缺頁中斷信號,之后操作系統的缺頁中斷處理程序會負責處理這個中斷。那由於中斷處理的過程需要I/O操作,把頁面從外存調入內存,所以在等待I/O操作完成的這個過程當中,之前發生缺頁的這個進程應該被阻塞,放入到阻塞隊列當中。只有調頁的事情完成之后,才把它再喚醒,重新放回就緒隊列。

那通過這個頁表項就可以知道這個頁面在外存當中存放在什么地方。

那如果說此時的內存當中有空閑的塊,比如說a號塊空閑的話,那就可以把這個空閑塊分配給此時缺頁的這個進程,再把目標頁面從外存放到內存當中。

那相應的也需要修改頁表項當中對應的一些數據,那這是第一種情況,就是有空閑的內存塊的情況。

第二種情況,如果說此時內存中沒有空閑塊的話,那么需要由頁面置換算法通過某種規則來選擇要淘汰一個頁面。

比如說頁面置換算法選中了要淘汰2號頁面,那由於2號頁面的內容是被修改過的,所以2號頁面的內容需要從內存再寫回外存,把外存當中的那個舊數據給覆蓋掉。那這樣的話,2號頁面以前占有的c號塊就可以空出來讓0號頁面使用了。

於是,可以把0號頁面從外存調入內存當中的c號塊。

那相應的,我們也需要把換出外存的頁面還有換入外存的頁面相應的那些數據給更改,那這是第二種情況。就是內存當中沒有空閑塊的時候,需要用頁面置換算法淘汰一個頁面。

那缺頁中斷的發生肯定和當前執行的指令是有關的。由於這個指令想要訪問某一個邏輯地址,而系統又發現這個邏輯地址對應的頁面還沒有調入內存,因此才發生了缺頁中斷。那由於它和當前執行的指令有關,因此缺頁中斷是屬於內中斷的。

還記得咱們之前講的內中斷和外中斷的分類嗎?內中斷可以分為陷阱、故障還有終止這樣三種類型。其中故障這種內中斷類型是指有可能被故障處理程序修復的,比如說缺頁中斷這種異常的情況是有可能被操作系統修復的,因此它是屬於故障這種分類。

另外呢我們需要注意的是,一條指令在執行的過程當中,有可能會產生多次缺頁中斷。因為一條指令當中可能會訪問多個內存單元,比如說把邏輯地址A當中的數據復制到邏輯地址B當中。那如果說這兩個邏輯地址屬於不同的頁面,並且這兩個頁面都沒有調入內存的話,那么在執行這一條指令的時候就有可能會產生兩次中斷。那通過之前的講解我們會發現,引入了缺頁中斷機構之后,系統才能實現請求調頁這樣的事情。

那接下來我們再來看一下請求分頁存儲管理與基本分頁存儲管理相比,在地址變換的時候需要再多做一些什么事情。

第一個事情,在查找到頁面對應的頁表項的時候,一定是需要對這個頁面是否在內存這個狀態進行一個判斷。

第二個事情,在地址變換的過程當中,如果說我們發現此時想要訪問的頁面暫時沒有調入內存,但是此時內存當中又沒有空閑的內存塊的時候,那么在這個地址變換的過程當中,也需要進行頁面置換的工作,換出某一些頁面來騰出內存空間。

第三個與基本分頁存儲管理不同的就是,當頁面調入或者調出,或者頁面被訪問的時候,需要對與它對應的這些頁表項進行一個數據的修改。所以我們在理解和記憶請求分頁存儲管理當中地址變換過程的時候,需要重點關注這三件事情需要在什么時候進行。

那與基本分頁存儲管理相同,請求分頁存儲管理在邏輯地址變換為物理地址的過程當中,需要做的第一件事情同樣是檢查頁號的合法性,看一下頁號是否越界。那如果頁號沒有越界的話,就會查詢此時在快表當中有沒有這個頁號對應的頁表項,那如果快表命中,就可以直接得到最終的物理地址。如果快表沒有命中的話,就需要查詢內存當中的慢表。

那在找到對應的頁表項之后,需要檢查此時這個頁面是否已經在內存當中。如果說這個頁面此時沒有在內存當中的話,那缺頁中斷機構會產生一個缺頁中斷的信號,之后就會由操作系統的缺頁中斷處理程序進行處理包括請求調頁還有頁面置換那一系列的事情。那當然,當頁面調入之后也需要修改這個頁表項對應的一些數據。

那這個地方注意一個細節。在請求分頁管理方式當中,如果說能夠在快表當中找到某一個頁面對應的頁表項。那么就說明這個頁面此時肯定是在內存當中的,如果一個頁面被換出了外存的話,那么快表項當中對應的這些頁表項也應該被刪除。所以只要快表命中,那么就可以直接根據這個內存塊號還有頁內偏移量得到最終的物理地址了,這個頁面肯定是在內存當中的。那這個地方並沒有像基本分頁管理方式當中那樣,一步一步很仔細地分析。那其實大家只需要關注請求分頁管理方式與基本分頁管理方式相比,不同的這些步驟就可以了。

那其實課本當中給出了一個很完整的請求分頁管理方式當中,地址變換的一個流程圖。大家需要重點關注的是這兩個紅框部分當中的內容。這些內容就是請求分頁管理方式與基本分頁管理方式相比增加的一些步驟和內容。

那這兒根據這個圖補充幾個大家可能注意不到的細節。第一個地方,通過這個圖,特別是這個步驟,大家有可能會發現,似乎只要訪問了某一個頁面,那么這個頁面相關的修改位是不是就需要修改呢?其實並不是。只有執行寫指令的時候,才會改變這個頁面的內容。如果說執行的是讀指令,那么就其實不需要修改這個頁面對應的修改位。並且一般來說,在訪問了某一個頁面之后,只需要把這個頁面在快表當中對應的表項的那些數據修改了就可以了。那只有它所對應的那些表項要從快表當中刪除的時候,才需要把這些數據從快表再復制回慢表當中。那采取這樣的策略的話就可以減少訪問內存當中慢表的次數,可以提升系統的性能。

第二個需要注意的地方是,在產生了缺頁中斷之后,缺頁中斷處理程序也會保存CPU現場。那這個地方其實和普通的中斷處理是一樣的。在中斷處理的時候,需要保存CPU的現場,然后讓這個進程暫時進入阻塞態。那只有這個進程再重新回處理機運行的時候,才需要再恢復它的CPU現場。

第三個需要注意的地方是,內存滿的時候需要選擇一個頁面換出。那到底換出哪個頁面,這是頁面置換算法要解決的問題,也是咱們下個小節當中會詳細介紹的內容。

第四個需要注意的點是,如果我們要把頁面寫回外存,或者要把頁面從外存調入內存的話,那么需要啟動I/O硬件。所以其實把頁面換入換出的工作都是需要進行慢速的I/O操作的。因此,如果換入換出操作太頻繁的話,那系統會有很多的時間是在等待慢速的I/O操作完成的。因此頁面的換入換出不應該太頻繁。

第五個需要注意的地方。當我們把一個頁面從外存調入內存之后,需要修改內存當中的頁表,但是其實我們同時也需要把這個頁表項復制到快表當中。

所以由於新調入的頁面在快表當中是有對應的頁表項的,因此在訪問一個邏輯地址的時候,如果發生了缺頁,那么地址變換變換的步驟應該是這樣的:第一步首先是查詢快表,如果快表沒有命中的話,才會查詢內存當中的慢表。然后通過慢表會發現此時頁面並沒有調入內存當中。之后系統會進行調頁相關的操作,那在頁面調入之后,不僅要修改內存當中的慢表,也需要把這個頁表項同時加入到快表當中。於是之后可以直接從快表當中得到這個頁面存放的位置,而不需要再查詢慢表。

那這是地址變換過程當中大家需要注意的幾個點。那其他的流程其實並不難理解,右半部分的這些流程其實和基本分頁存儲管理方式進行地址變換的這個過程是大同小異的,只不過是增加了修改這個頁表項相應的內容這樣一個步驟。然后左半部分是新增的一系列處理,那要做的無非也就是兩件事。第一件事就是當我們發現所要訪問的頁面沒有在內存當中的時候,需要把頁面從外存調入內存。那如果說內存此時已經滿了,那需要做頁面置換的工作。那當調頁還有頁面置換這些工作完成之后,也需要對頁表還有快表當中的對應的一些數據進行修改。所以其實只要理解了我們應該在什么時候請求調頁,又應該在什么時候進行頁面置換,當調頁和頁面置換完成之后,又需要對哪些數據結構進行修改。只要知道這三個事情,那就可以掌握請求分頁管理方式地址變換的這些精髓了。

與基本分頁管理方式相比,請求分頁管理方式在頁表當中增加了狀態位、訪問字段、修改位還有外存地址這樣幾個數據。那大家需要理解這幾個數據分別有什么作用。那之后我們介紹了缺頁中斷機構,那在引入了缺頁中斷機構之后,如果一個頁面暫時沒有調入內存,那就會產生一個缺頁中斷信號,然后之后系統會對這個缺頁中斷進行一系列的處理。另外呢,大家需要注意的是缺頁中斷它是一個內中斷,它和當前執行的指令有關。並且一條指令在執行的過程當中有可能會訪問到多個內存單元,而這些內存單元有可能是在不同的頁面當中的,因此一條指令執行的過程當中有可能會產生多次缺頁中斷。那最后我們介紹了請求分頁管理方式的地址變換機構,其實我們只需要重點關注與基本分頁方式不同的那些地方。第一,在找到頁表項的時候需要檢查頁面是否在內存當中,由此來判斷此時是不是需要請求調頁。那在調頁的過程當中如果發現此時內存當中已經沒有空閑塊,那我們還需要進行換出頁面的操作。另外,在調入和換出了一些頁面之后,我們也需要修改與這些頁面對應的那些頁表項。那除了這些步驟以外,其他的其實和基本分頁存儲管理當中地址變換的過程並沒有太大的區別。那這個小節的內容在於理解,不需要死記硬背。大家還需要通過課后習題進行進一步的鞏固和理解。

在這個小節中我們會學習請求分頁存儲管理當中很重要的一個知識點考點————頁面置換算法。

那么通過之前的學習我們知道,在請求分頁存儲管理當中,如果說內存空間不夠的話,那么操作系統會負責把內存當中暫時用不到的那些信息先換出外存。那頁面置換算法其實就是用於選擇到底要把哪個頁面換出外存。

那通過之前的學習我們知道,頁面的換入換出其實是需要啟動磁盤的I/O的,因此它是會造成比較大的時間開銷。所以一個好的頁面置換算法應該盡可能地追求更少的缺頁率,也就是讓換入換出的次數盡可能地少。那這個小節中,我們會介紹考試中要求我們掌握的五種頁面置換算法,分別是最佳置換、先進先出、最近最久未使用還有時鍾置換、改進型的時鍾置換這樣五種。那除了注意它們的中文名字之外,大家注意也需要能夠區分它們的英文縮寫到底分別是什么。

那我們按從上至下的順序依次介紹。首先來看什么是最佳置換算法,其實最佳置換算法的思想很簡單。由於置換算法需要追求盡可能少的缺頁率,那為了追求最低的缺頁率,最佳置換算法在每次淘汰頁面的時候選擇的都是那些以后永遠不會被使用到的頁面,或者在之后最長的時間內不可能再被訪問的頁面。

那根據最佳置換算法的規則,我們要選擇的是在今后最長時間內不會被使用到的頁面,所以其實我們在手動做題的時候,可以看一下它的這個序列。

我們從當前訪問的這個頁號開始往后尋找,看一下此時在內存當中的0、1、7這三個頁面出現的順序到底是什么。那最后一個出現的序號肯定就是在之后最長時間內不會再被訪問的頁面。

所以從這兒往后看,0號頁面是最先出現的,

然后一直到這個位置我們發現1號頁面也開始出現了。所以0、1、7這三個頁面當中0號和1號會在之后依次被使用,但是7號頁面是在之后最長的時間內不會再被訪問到的頁面。因此我們會選擇淘汰7號頁面,然后讓2號頁面放入到7號頁面原先占有的內存塊也就是內存塊1當中,因此2號頁面是放在這個位置的。

那接下來要訪問的0號頁面已經在內存當中了,所以此時不會發生缺頁,可以正常地訪問。

再之后訪問3號頁面,也會發現,此時3號頁面並沒有在內存當中,所以我們依然需要用這個置換算法選擇淘汰一個頁面。

 那和剛才一樣,我們從這個位置開始往后尋找,看一下此時內存當中存放的2、0、1這三個頁面出現的先后順序。那么我們會發現,2、0、1當中,那么1號頁面就是最后一個出現的,因此1號頁面是在今后最長時間內不會再被訪問的頁面,所以我們會選擇把2、0、1這三個頁面當中的1號頁面給淘汰,先換出外存,然后3號頁面再換入1號頁面以前占有的那個內存塊,也就是內存塊3當中,所以3號頁面是放在這個地方的。那對於之后的這些頁面序號的訪問我們就不再細細地分析了,大家可以自己嘗試着去完善一下這個表。

那最終我們會發現整個訪問這些頁面的過程當中,缺頁中斷發生了9次,也就是這兒打勾的這些位置發生缺頁中斷,但是頁面置換只發生了6次。所以大家一定需要注意,缺頁中斷之后未必發生頁面置換。只有內存塊已經都滿了的時候才發生頁面置換。因此剛開始訪問7、0、1這三個頁面的時候,雖然它們都沒有在內存當中,但是由於剛開始內置有空閑的內存塊,雖然發生了缺頁中斷,雖然會發生調頁,但是並不會發生頁面置換這件事情。那只有所有的內存塊都已經占滿了之后,再發生缺頁的話那才需要進行頁面置換這件事情。因此缺頁中斷總共發生了9次,但頁面置換只發生了6次,前面的3次只是發生了缺頁,但是並沒有頁面置換。那缺頁率的計算也很簡單,我們只需要把缺頁中斷次數再除以我們總共訪問了多少次的頁面就可以得到缺頁率是45%。那這是最佳置換算法。

那其實頁面置換執行的前提條件是我們必須要知道之后會依次訪問的頁面序列到底是哪些。

不過在實際應用當中,只有在進程執行的過程當中,才能一步一步地知道接下來會訪問到的到底是哪一個頁面。所以操作系統其實根本不可能提前預判各個頁面的訪問序列,所以最佳置換算法它只是一種理想化的算法,在實際應用當中是無法實現的。

那接下來我們再來看第二種————先進先出置換算法。這種算法的思想很簡單,每次選擇淘汰的頁面,是最早進入內存的頁面。所以在具體實現的時候,可以把調入內存的這些頁面根據調入的先后順序來排成一個隊列,當系統發現需要換出一個頁面的時候,只需要把隊頭的那個頁面淘汰就可以了。那需要注意的是,這個隊列有一個最大長度的限制,那這個最大長度取決於系統為進程分配了多少個內存塊。

那我們還是來看一個例子。

 

為一個進程分配的內存塊越多,那這個進程的缺頁次數應該越少才對啊。所以像這個地方我們發現的這種現象,就是為進程分配物理塊增大的時候,缺頁次數不增反減的這種現象就稱作為Belady異常。

那在我們要學習的所有的這些算法當中,只有先進先出算法會產生這種Belady異常。所以雖然先進先出算法實現起來很簡單,但是先進先出的這種規則其實並沒有考慮到進程實際運行時候的一些規律。因為先進入內存的頁面其實在之后也有可能會被經常訪問到,所以只是簡單粗暴地讓先進入的頁面淘汰的話,那顯然這是不太科學的,所以先進先出置換算法的算法性能是很差的。

那接下來我們再來看第三個————最近最久未使用置換算法,英文縮寫是LRU(least recently used),大家也需要記住它的這個英文縮寫。很多題目出題的時候就直接用這個LRU來表示置換算法。那這個算法的規則就像它的名字一樣,就是要選擇淘汰最近最久沒有使用的頁面。

所以為了實現這件事,我們可以在每個頁面的頁表項當中的訪問字段這兒,記錄這個頁面自從上一次被訪問開始,到現在為止所經歷的時間t。那我們需要淘汰一個頁面的時候,只需要選擇這個t值最大的,也就是最久沒有被訪問到的那個頁面進行淘汰就可以了。那我們依然還是結合一個例子。如果一個系統為進程分配了四個內存塊,然后有這樣的一系列的內存訪問序列。

那首先要訪問的是1號頁。此時有內存塊空閑,所以1號頁放到內存塊1當中。

然后第二個訪問8號頁面,也可以直接放到空閑的內存塊2當中。

那一直到后面訪問到這個3號頁面的時候,由於此時給這個進程分配的4個內存塊都已經用完了,所以必須選擇淘汰其中的某個頁面。那如果我們在手動做題的時候,可以從這個頁號開始逆向地往前檢查此時在內存當中擁有的1、8、7、2這幾個頁號從逆向掃描來看,出現的先后順序是什么樣的。那最后一個出現的頁號,那肯定就是最近最久沒有使用的頁面了。

那同樣的,在之后的這一系列訪問當中都不會發生缺頁,一直到訪問到7號頁的時候,又發生了一次缺頁,並且需要選擇淘汰一個頁面。

 那和之前一樣,我們從這個地方開始逆向地往前檢查,看一下這幾個頁號出現的順序分別是什么樣的。

那最近最久未使用置換算法在實際的應用當中其實是需要專門的硬件的支持的,所以這個算法雖然性能很好,但是實現起來會很困難,並且開銷很大。那在我們學習的這幾個算法當中,最近最久未使用這個算法的性能是最接近最佳置換算法的。

那接下來我們再來看第四種————時鍾置換算法。之前我們學到的這幾種算法當中,最佳置換算法性能是最好的,但是無法實現。先進先出算法雖然實現簡單,但是算法的性能很差,並且也會出現Belady異常。那最近最久未使用置換算法雖然性能也很好,但是實現起來開銷會比較大。所以之前提到的那些算法都不能做到算法效果還有實際開銷的一個平衡,因此人們就提出了時鍾置換算法。它是一種性能和開銷比較均衡的算法,又稱為CLOCK算法,或者叫最近未用算法(NRU,Not Recently Used),英文縮寫是NRU。

那在考試中我們需要掌握兩種時鍾置換算法,分別是簡單的時鍾置換算法還有改進型的時鍾置換算法。那我們先來看簡單的這種算法。首先我們要為每個頁面設置一個訪問位,訪問位為1的時候就表示這個頁面最近被訪問過,訪問位為0的時候表示這個頁面最近沒有被訪問過。因此如果說訪問了某個頁面的話,那需要把這個頁面的訪問位變為1。那內存中的這些頁面需要通過鏈接指針的方式把它們鏈接成一個循環隊列。那當需要淘汰某個頁面的時候,需要掃描這個循環隊列,找到一個最近沒有被訪問過的頁面,也就是訪問位為0的頁面。但是在掃描的過程中,需要把訪問位為1的這些頁面的訪問位再重新置為0,所以這個算法有可能會經過兩輪的掃描。如果說所有的頁面訪問位都是1的話,那第一輪掃描這個循環隊列就並不會找到任何一個訪問位為0的頁面。只不過在第一輪掃描當中,會把所有的頁面的訪問位都置為0,所以第二輪掃描的時候就肯定可以找到一個訪問位為0的頁面。所以這個算法在淘汰一個頁面的時候最多會經歷兩輪的掃描。

那光看這個文字的描述其實還很抽象的,我們直接來看一個例子。那剛開始由於有5個空閑的內存塊,所以前五個訪問的這個頁號1、3、4、2、5都可以順利地放入內存當中。只有在訪問到6號頁的時候,才需要考慮淘汰某個頁面。

那么在內存當中的1、3、4、2、5這幾個頁面會通過鏈接指針的方式鏈接成一個這樣的循環隊列。

時鍾置換算法的一個運行的過程,並且通過剛才的這個例子大家會發現,這個掃描的過程有點像一個時鍾的那個時針在不斷地轉圈的一個過程。所以為什么這個算法要叫做時鍾置換算法,它其實是一個很形象的比喻。那其實經過剛才的分析,我們也很容易理解,為什么它還稱作最近未用算法。因為我們會為各個頁面設置一個訪問位,訪問位為1的時候表示最近用過,訪問位為0的時候表示最近沒有用過。但是我們在選擇淘汰一個頁面的時候,是選擇那種最近沒有被訪問過也就是訪問位為0的頁面,因此這種算法也可以稱作為最近未用算法。

那接下來我們再來學習改進型的時鍾置換算法。其實在之前學習的這個簡單的時鍾置換算法當中,只是很簡單地考慮到了一個頁面最近是否被訪問過。但通過之前的講解我們知道,如果一個被淘汰的頁面沒有被修改過的話,那么是不需要執行I/O操作而把它寫回外存的。所以如果說我們能夠優先淘汰沒有被修改過的頁面的話,那么實際上就可以減少這些I/O操作的次數,

從而讓這個置換算法的性能得到進一步的提升。那這就是改進型的時鍾置換算法的一個思想。

所以為了實現這件事,我們還需要為各個頁面增加一個修改位,修改位為0的時候表示這個頁面在內存當中沒有被修改過,那修改位為1的時候表示頁面被修改過。那我們在接下來的討論當中,會用訪問位、修改位這樣二元組的形式來標識各個頁面的狀態。比如說訪問位為1、修改位也為1的話那就表示這個頁面近期被訪問過,並且也曾經被修改過。

那和簡單的時鍾置換算法一樣,我們也需要把所有的可能被置換的頁面排成一個循環隊列。

在第一輪掃描的時候,會從當前位置開始往后依次掃描,嘗試找到第一個最近沒有被訪問過並且也沒有修改過的頁面,對它進行淘汰,那第一輪掃描是不修改任何的標志位的。那如果第一輪掃描沒有找到(0,0)這樣的頁面的話,就需要進行第二輪的掃描。第二輪的掃描會嘗試找到第一個最近沒有被訪問過但是被修改過的這個頁面進行替換。並且被掃描過的那些頁面的訪問位,都會被設置為0。那如果第二輪掃描失敗,需要進行第三輪掃描。第三輪掃描會嘗試找到第一個訪問位和修改位都為0的這個頁面,進行淘汰。並且第三輪掃描並不會修改任何的標志位。

那如果第三輪掃描失敗的話,還需要進行第四輪掃描。找到第一個(0,1)的頁幀用於替換。那由於第二輪的掃描已經把所有的訪問位都設為了0了,所以經過第三輪、第四輪的掃描之后,肯定是可以找到一個要被淘汰的頁面的。所以改進型的這種時鍾置換算法,選擇一個淘汰頁面,最多會進行四輪掃描。

那其實這個過程光看文字描述也是很抽象的,不太容易理解。假設系統為一個進程分配了5個內存塊,那當這個內存塊被占滿之后,各個頁面會用這種鏈接的方式連成一個循環的隊列。

 那此時如果要淘汰一個頁面的話,需要從這個隊列的隊頭開始依次地掃描。

 

 

 

改進型的時鍾置換算法在選擇一個淘汰頁面的時候最多會進行四輪掃描,而簡單的時鍾置換算法在淘汰一個頁面的時候最多只會進行兩輪掃描。

 這個小節我們介紹了五種頁面置換算法,分別是最佳置換OPT、先進先出FIFO、最近最久未使用LRU、簡單型的時鍾置換(最近未用)NRU、改進型的時鍾置換(最近未用)NRU。那這個小節的內容重點需要理解各個算法的算法規則,如果題目中給出一個頁面的訪問序列,那需要自己能夠用手動的方式來模擬各個算法運行的一個過程。那除了算法規則之外,各個算法的優缺點有可能在選擇題當中進行考查。那需要重點注意的是,最佳置換算法在現實當中是無法實現的,然后先進先出算法它的性能差,並且是唯一一個有可能出現Belady異常的算法。

在這個小節中我們會學習頁面分配策略相關的一系列知識點。

什么是駐留集。考試中需要掌握的三種頁面分配、置換的策略。另外,頁面應該在什么時候被調入,應該從什么地方調入,應該調出到什么位置,這些也是我們之后會探討的問題。什么是進程抖動(進程顛簸)這種現象,那為了解決進程抖動(進程顛簸)現象,又引入了工作集這個概念,那我們會按照從上至下的順序依次講解。

駐留集是指請求分頁存儲管理當中給進程分配的物理塊(內存塊、頁框、頁幀)的集合。那在采用了虛擬存儲技術的系統當中,為了從邏輯上提升內存,並且提高內存的利用率,那駐留集的大小一般是要小於進程的總大小的。

駐留集太小導致進程缺頁頻繁,系統就需要花大量的時間處理缺頁,而實際用於進程推進、進程運行的時間又很少。駐留集太大,導致多道程序並發度下降,使系統的某些資源利用率不高。像系統的CPU和I/O設備這兩種資源,理論上是可以並行地工作的。那如果多道程序並發度下降,就意味着CPU和I/O設備這兩種資源並行工作的幾率就會小很多,所以資源的利用率當然就會降低。所以系統應該為進程選擇一個合適的駐留集大小。

那針對於駐留集的大小是否可變這個問題,人們提出了固定分配和可變分配這兩種分配策略。固定分配指駐留集的大小是剛開始就決定了,之后就不再改變了。可變分配其實指的就是駐留集的大小是可以動態地改變,可以調整的。

另外,當頁面置換的時候,置換的范圍應該是什么?根據這個問題,人們又提出了局部置換和全局置換這兩種置換范圍的策略。所以局部置換和全局置換的區別就在於,當某個進程發生缺頁,並且需要置換出某個頁面的時候,那這個置換出的頁面是不是只能是自己了。

那把這兩種分配和置換的策略兩兩結合,可以得到這樣的三種分配和置換的策略。分別是固定分配-局部置換,可變分配-局部置換和可變分配-全局置換。

那大家會發現,其實並不存在固定分配-全局置換這種策略。因為從全局置換的這個規則我們也可以知道,如果使用的是全局置換的話,就意味着一個進程所擁有的物理塊是必然會改變的。而固定分配又規定着進程的駐留集大小不變,也就是進程所擁有的物理塊數是不能改變的。所以固定分配-全局置換這種分配置換策略是不存在的,那接下來我們依次介紹存在的這三種分配和置換的策略。

不過在實際應用中,一般如果說采用這種固定分配-局部置換策略的系統,它會根據進程大小、進程優先級或者是程序員提出的一些參數來確定到底要給每個進程分配多少個物理塊,不過這個數量一般來說是不太合理的。那因為駐留集的大小不可變,所以固定分配局部置換這種策略的靈活性相對來說是比較低的。

那第二種叫做可變分配全局置換。因為是可變分配,所以說系統剛開始會為進程分配一定數量的物理塊,但是之后在進程運行期間,這個物理塊的數量是可以改變的,那操作系統會保持一個空閑物理塊的隊列。如果說一個進程發生缺頁的時候,就會從這個空閑物理塊當中取出一個分配給進程。那如果說此時這個空閑物理塊都已經用完了,那就可以選擇一個系統當中未鎖定的頁面換出外存,再把這個物理塊分配給缺頁的這個進程。

那這個地方所謂的未鎖定的頁面指的是什么呢?其實系統會鎖定一些很重要的就是不允許被換出外存、需要常駐內存的頁面,比如說系統當中的某些很重要的內核數據,就有可能是被鎖定的。那另外一些可以被置換出外存的頁面,就是所謂的“未鎖定”的頁面。當然這個地方只是做一個拓展,在考試當中應該不會考查。

那通過剛才對這個策略的描述大家也會發現,在這種策略當中,只要進程發生缺頁的話,那它必定會獲得一個新的物理塊。如果說空閑物理塊沒有用完,那這個新的物理塊就會從空閑物理塊隊列當中選擇一個給它分配。那如果說空閑物理塊用完了,系統才會選擇一個未鎖定的頁面換出外存,但這個未鎖定的頁面有可能是任何一個進程的頁面。所以這個進程的頁面被換出的話,那么它所擁有的物理塊就會減少,它的缺頁率就會有所增加。那顯然,只要進程發生了缺頁,就給它分配一個新的物理塊,這種方式其實也是不太合理的。

所以之后人們又提出了可變分配局部置換的策略。那在剛開始會給進程分配一定數量的物理塊,因為是可變分配,所以之后這個物理塊的數量也是會改變的。那由於是局部置換,所以當進程發生缺頁的時候,只允許這個進程從自己的物理塊當中選出一個進行換出。那如果說操作系統在進程運行的過程中發現它頻繁地缺頁,那就會給這個進程多分配幾個物理塊,直到這個進程的缺頁率回到一個適當的程度。那相反的,如果一個進程在運行當中缺頁率特別低的話,那系統會適當地減少給這個進程所分配的物理塊。那這樣的話,就可以讓系統的多道程序並發度也保持在一個相對理想的位置。

那這三種策略當中,最難分辨的是可變分配全局置換和可變分配局部置換。大家需要抓住它們倆最大的一個區別,如果采用的是全局置換策略的話,那么只要缺頁就會給這個進程分配新的物理塊。那如果說采用的是這種局部置換的策略的話,系統會根據缺頁的頻率來動態地增加或者減少一個進程所擁有的物理塊。那這是三種我們需要掌握的頁面分配策略,有可能在選擇題當中進行考查。

那接下來我們再來討論下一個問題。我們應該在什么時候調入所需要的頁面呢?那一般來說有這樣的兩種策略。第一種叫做預調頁策略。

根據我們之前學習的局部性原理,特別是空間局部性的原理。我們知道,如果說當前訪問了某一個內存單元的話,那么很有可能在之后不久的將來會接着訪問與這個內存單元相鄰的那些內存單元。所以根據這個思想我們自然而然的也會想到,如果說我們訪問了某一個頁面的話,那么是不是在不久的之后就也有可能會訪問與它相鄰的那些頁面呢?因此,基於這個方面的考慮,如果我們能夠一次調入若干個相鄰的頁面,那么可能會比一次調入一個頁面會更加高效。因為我們一次調入一堆頁面的話,那么我們啟動磁盤I/O的次數肯定就會少很多,這樣的話就可以提升調頁的效率。不過另一個方面,如果說我們提前調入的這些頁面在之后沒有被訪問過的話,那么這個預調頁就是一種很低效的行為。所以我們可以用某種方法預測不久之后可能會訪問到的頁面,將這些頁面預先地調入內存。當然目前預測的成功率不高,只有50%左右。所以在實際應用當中,預調頁策略主要是用於進程首次調入的時候,由程序員指出哪些部分應該是先調入內存的。

比如說我可以告訴系統把main函數相關的那些部分先調入內存,所以預調頁策略是在進程運行前就進行調入的一種策略。

那第二種就是請求調頁策略,這也是咱們之前一直在學習的請求調頁方式。只有在進程期間發現缺頁的時候,才會把所缺的頁面調入內存。所以這種策略其實在進程運行期間才進行頁面的調入。並且被調入的頁面肯定在之后是會被訪問到的。但是每次只能調入一個頁面,所以每次調頁都要啟動磁盤I/O操作,因此I/O開銷是比較大的。那在實際應用當中,預調頁策略和請求調頁策略都會結合着來使用。預調頁用於運行前的調入,而請求調頁策略是在進程運行期間使用的。那這是調入頁面的實際問題。

那接下來我們再來看一下我們應該從什么地方調入頁面。之前我們有簡單地介紹過,磁盤當中的存儲區域分為對換區和文件區這樣兩個部分。其中對換區采用的是連續分配的方式,讀寫的速度更快,而文件區的讀寫速度是更慢的,它采用的是離散分配的方式。那什么是離散分配什么是連續分配,這個是咱們在之后的章節會學習的內容,這個地方先不用管,有個印象就可以。在本章中,大家只需要知道對換區的讀寫速度更快,而文件區的讀寫速度更慢就可以了。那一般來說文件區的大小要比對換區要更大,那平時我們指的程序在沒有運行的時候,相關的數據都是存放在文件區的。

那由於對換區的讀寫速度更快,所以如果說系統擁有足夠的對換區空間的話,那么頁面的調入調出都是內存與對換區之間進行的。

所以系統中如果有足夠的對換區空間,那剛開始在運行之前會把我們的進程相關的那些數據從文件區先復制到對換區,

之后把這些需要的頁面從對換區調入內存。

那相應的,如果內存空間不夠的話,可以把內存中的某些頁面調出到對換區當中。頁面的調入調出都是內存和對換區這個更高速的區域進行的。那這是在對換區大小足夠的情況下使用的一種方案。

那如果說系統中缺少足夠的對換區空間的話,凡是不會被修改的數據都會從文件區直接調入內存。那由於這些數據是不會被修改的,所以當調出這些數據的時候並不需要重新寫回磁盤。

那如果說某些頁面被修改過的話,把它調出的時候就需要寫回到對換區,而不是寫回到文件區,因為文件區的讀寫速度更慢。

那相應的,如果之后還需要再使用到這些被修改的頁面的話,那就從對換區再換入內存。

第三種,UNIX使用的是這樣的一種方式。如果說一個頁面還沒有被使用過,也就是這個頁面第一次被使用的話,

那么它是從文件區直接調入內存。

那之后如果內存空間不夠,需要把某些頁面換出外存的話,那么是換出到這個對換區當中。

那如果這個頁面需要再次被使用的話,就是要從對換區再換回內存。這是UNIX系統采用的一種方式。

那接下來我們再來介紹一個很常考的一個概念,叫做抖動(或者叫顛簸)現象。那如果說發生了抖動現象的話,系統會用大量的時間來處理這個進程頁面的換入換出。而實際用於進程執行的時間就變得很少,所以我們要盡量避免抖動現象的發生。

那為了防止抖動的發生,就需要為進程分配足夠的物理塊。但如果說物理塊分配的太多的話,又會降低系統整體的並發度,降低某些資源的利用率。

所以為了研究應該為每個進程分配多少個物理塊,有的科學家在196幾年提出了進程工作集的概念。

工作集和駐留集其實是有區別的。駐留集是指在請求分頁存儲管理當中,給進程分配的內存塊的集合。

我們直接來看一個例子。一般來說操作系統會設置一個所謂的“窗口尺寸”來算出工作集。那假設一個進程對頁面的訪問序列是這樣的一個序列。

工作集的大小可能會小於窗口的尺寸。在實際應用當中,窗口尺寸一般會設置的更大一些,比如說設置10、50、100這樣的數字。那對於一些局部性很好的進程來說,工作集的大小一般是要比窗口尺寸的大小要更小的。所以系統可以根據檢測工作集的大小來決定到底要給這個進程分配多少個內存塊。換一個說法就是,根據工作集的大小,來確定駐留集的大小是多少。

那一般來說,駐留集的大小不能小於工作集的大小。如果說更小的話,那就有可能會發生頻繁的缺頁,也就是發生抖動現象。

另外,在有的系統當中,也會根據工作集的概念來設計一種頁面置換算法。比如說如果說這個進程需要置換出某個頁面的話,那完全就可以選擇一個不在工作集當中的頁面進行淘汰。那這些知識點只是作為一個拓展,大家只要有個印象就可以了。

需要特別注意駐留集這個概念。在之前咱們講過的那些內容當中,經常會遇到某些題目告訴我們一個條件就是說,系統為某個進程分配了N個物理塊,那這種說法其實也可以改變一種等價的表述方式,就是也可以說成是某個進程的駐留集大小是N。那如果說題目中的條件是用駐留集大小這種方式給出的話,大家也需要知道它所表述的到底是什么意思。那另外大家需要注意這三種分配置換策略在真題當中是進行考查過的。並且在有的大題當中有可能會告訴大家,一個進程采用固定分配局部置換的策略,那這個條件就是為了告訴大家,系統為一個進程分配的物理塊數是不會改變的,大家在做課后習題的時候可以注意一下,很多大題都會給出這樣的一個條件。那我們需要知道這個條件背后隱含的一系列的信息。那這個地方還需要注意,並不存在固定分配全局置換這種策略。因為全局置換意味着一個進程所擁有的物理塊數肯定是會改變的,而固定分配又要求一個進程擁有的物理塊數是不能改變的。所以固定分配和全局置換這兩個條件本身就是相互矛盾的,因此並不存在固定分配全局置換這種方式。那之后介紹的內容,何時調入頁面,應該給從何處調入頁面能有個印象就可以了。最后大家還需要重點關注抖動(顛簸)這個現象。那產生抖動的主要原因是分配給進程的物理塊不夠,所以如果要解決抖動問題的話,那么肯定就是用某種方法給這個進程分配更多的物理塊。那這一點在咱們的課后習題當中也會遇到。那我們還對工作集的概念做了一系列的拓展,不過一般來說工作集這個概念不太容易進行考查。但是大家需要注意的是駐留集大小一般來說不能小於工作集的大小,如果更小的話那就會產生抖動現象。那這個小節的內容一般來說只會在選擇題當中進行考查。但是在考試當中也有可能會用某些概念作為大題當中的一個條件進行給出,所以大家還需要通過課后習題進行進一步的鞏固。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM