1. 程式人生 > 其它 >作業系統記憶體管理

作業系統記憶體管理

一、什麼是實體記憶體?

我們常說的實體記憶體大小就是指記憶體條的大小,一般買電腦時都會看下記憶體條是多大容量的,話說如果記憶體條大小是100G,那這100G就都能夠被使用嗎?不一定的,更多的還是要看CPU地址匯流排的位數,如果地址匯流排只有20位,那麼它的定址空間就是1MB,即使可以安裝100G的記憶體條也沒有意義,也只能視實體記憶體大小為1MB。

二、使用實體記憶體有什麼缺點?

這種方式下每個程式都可以直接訪問實體記憶體,有兩種情況:

1.系統中只有一個程序在執行:如果使用者程式可以操作實體地址空間的任意地址,它們就很容易在不經意間破壞了作業系統,使系統出現各種奇奇怪怪的問題;

2.系統有多個程序同時在執行:如圖,理想情況下可以使程序A和程序B各佔實體記憶體的一邊,兩者互不干擾,但這只是理想情況下,誰能確保程式沒有bug呢,程序B在後臺正常執行著,程式設計師在除錯程序A時有可能就會誤操作到程序B正在使用的實體記憶體,導致程序B執行出現異常,兩個程式操作了同一地址空間,第一個程式在某一地址空間寫入某個值,第二個程式在同一地址又寫入了不同值,這就會導致程式執行出現問題,所以直接使用實體記憶體會使所有程序的安全性得不到保證。

如何解決上述問題?

可以考慮為儲存器創造新的抽象概念:地址空間,地址空間為程式創造了一種抽象的記憶體,是程序可用於定址記憶體的一套地址集合,同時每個程序都有一套自己的地址空間,一個程序的地址空間獨立於其它程序的地址空間。

如何為程式創造獨立的地址空間?

最簡單的辦法就是把每個程序的地址空間分別對映到實體記憶體的不同部分。這樣就可以保證不同程序使用的是獨立的地址空間。

實現

給每個程序提供一個基址A和界限B,程序內使用的空間為x,則對應的實體地址為A + x,同時需要保證A + x < B,如果訪問的地址超過的界限,需要產生錯誤並中止訪問。為了達到目的CPU配置了兩個特殊硬體暫存器:基址暫存器和界限暫存器,當一個程序執行時,程式的起始實體地址和長度會分別裝入到基址暫存器和界限暫存器裡,程序訪問記憶體,在每個記憶體地址送到記憶體之前,都會先加上基址暫存器的內容。

缺點:每次訪問記憶體都需要進行加法和比較運算,比較運算很快,但是加法運算由於進位傳遞事件的問題,在沒有使用特殊電路的情況下會顯得很慢。

此外,每個程序執行都會佔據一定的實體記憶體,如果實體記憶體足夠大到可以容納許多個程序同時執行還好,但現實中實體記憶體的大小是有限的,可能會出現記憶體不夠用的情況,怎麼辦?

方法一:如果是因為程式太大,大到超過了記憶體的容量,可以採用手動覆蓋技術,只把需要的指令和資料儲存在記憶體中。

方法二:如果是因為程式太多,導致超過了記憶體的容量,可以採用自動交換技術,把暫時不需要執行的程式移動到外存中。

覆蓋技術

把程式按照自身邏輯結構,劃分成多個功能相互獨立的程式模組,那些不會同時執行的模組可以共享到同一塊記憶體區域,按時間順序來執行:

將常用功能需要的程式碼和資料常駐在記憶體中;

將不常用的功能劃分成功能相互獨立的程式模組,平時放到外存中,在需要的時候將對應的模組載入到記憶體中;

那些沒有呼叫關係的模組平時不需要裝入到記憶體,它們可以共用一塊記憶體區,需要時載入到記憶體,不需要時換出到外存中;

如圖:

交換技術

多個程式同時執行,可以將暫時不能執行的程式送到外存,獲得更多的空閒記憶體,作業系統將一個程序的整個地址空間內容換出到外存中,再將外存中某個程序的整個地址空間資訊換入到記憶體中,換入換出內容的大小是整個程式的地址空間。

交換技術在實現上有很多困難:

  • 需要確定什麼時候發生交換:簡單的辦法是可以在記憶體空間不夠用時換出一些程式;

  • 交換區必須足夠大:多個程式執行時,交換區(外存)必須足夠大,大到可以存放所有程式所需要的地址空間資訊;

  • 程式如何換入:一個程式被換出後又重新換入,換入的記憶體位置可能不會和上一次程式所在的記憶體位置相同,這就需要動態地址對映機制。

覆蓋技術和交換技術的比較

  • 覆蓋只能發生在那些相互之間沒有呼叫關係的程式模組之間,因此程式設計師必須給出程式內的各個模組之間的邏輯覆蓋結構。

  • 交換技術是以在記憶體中的程式大小為單位來進行的,它不需要程式設計師給出各個模組之間的邏輯覆蓋結構。

通俗來說:覆蓋發生在程式的內部,交換髮生在程式與程式之間。

但是這兩種技術都有缺點:

覆蓋技術:需要程式設計師自己把整個程式劃分為若干個小的功能模組,並確定各個模組之間的覆蓋關係,增加了程式設計師的負擔,很少有程式設計師擅長這種技術;

交換技術:以程序作為交換的單位,需要把程序的整個地址空間都換進換出,增加了處理器的開銷,還需要足夠大的外存。

那有沒有更好的解決上述問題的方法呢?答案是虛擬記憶體技術。

三、什麼是虛擬記憶體?

虛擬記憶體,那就是虛擬出來的記憶體,它的基本思想就是確保每個程式擁有自己的地址空間,地址空間被分成多個塊,每一塊都有連續的地址空間,同時物理空間也分成多個塊,塊大小和虛擬地址空間的塊大小一致,作業系統會自動將虛擬地址空間對映到實體地址空間,程式所關注的只是虛擬記憶體,請求的也是虛擬記憶體,其實真正使用的是實體記憶體。

虛擬記憶體技術有覆蓋技術的功能,但它不是把程式的所有內容都放在記憶體中,因而能夠執行比當前的空閒記憶體空間還要大的程式。它比覆蓋技術做的更好,整個過程由作業系統自動來完成,無需程式設計師的干涉;

虛擬記憶體技術有交換技術的功能,能夠實現程序在記憶體和外存之間的交換,因而獲得更多的空閒記憶體空間。它比交換技術做的更好,它只對程序的部分內容在記憶體和外存之間進行交換。

虛擬記憶體技術的具體實現:

虛擬記憶體技術一般是在頁式管理(下面介紹)的基礎上實現

  • 在裝入程式時,不必將其全部裝入到記憶體,而只需將當前需要執行的部分頁面裝入到記憶體,就可讓程式開始執行;

  • 在程式執行過程中,如果需執行的指令或訪問的資料尚未在記憶體(稱為缺頁)。則由處理器通知作業系統將相應的頁面調入到記憶體,然後繼續執行程式;

  • 另一方面,作業系統將記憶體中暫時不使用的頁面調出儲存在外存上,從而騰出更多空閒空間存放將要裝入的程式以及將要調入的頁面。

虛擬記憶體技術的特點:

  • 大的使用者空間:通過把實體記憶體與外存相結合,提供給使用者的虛擬記憶體空間通常大於實際的實體記憶體,即實現了兩者的分離。如32位的虛擬地址理論上可以訪問4GB,而可能計算機上僅有256M的實體記憶體,但硬碟容量大於4GB;

  • 部分交換:與交換技術相比較,虛擬儲存的調入和調出是對部分虛擬地址空間進行的;

  • 連續性:程式可以使用一系列相鄰連續的虛擬地址來對映實體記憶體中不連續的大記憶體緩衝區;

  • 安全性:不同程序使用的虛擬地址彼此隔離。一個程序中的程式碼無法更改正在由另一程序或作業系統使用的實體記憶體。

四、虛擬記憶體如何對映到實體記憶體?

如圖,CPU裡有一個記憶體管理單元(Memory Management Unit),簡稱MMU,虛擬記憶體不是直接送到記憶體匯流排,而是先給到MMU,由MMU來把虛擬地址對映到實體地址,程式只需要管理虛擬記憶體就好,對映的邏輯自然有其它模組自動處理。

作業系統如何表示的記憶體被佔用還是空閒?

五、分頁記憶體管理

將虛擬地址空間分成若干個塊,每個塊都有固定的大小,實體地址空間也被劃分成若干個塊,每個塊也都有固定的大小,實體地址空間的塊和虛擬地址空間的塊大小相等,虛擬地址空間這些塊就被稱為頁面,實體地址空間這些塊被稱為

關於分頁這裡有個問題,頁面的大小是多少合適呢?頁面太大容易產生空間浪費,程式假如只使用了1個位元組卻被分配了10M的頁面,這豈不是極大的浪費,頁面太小會導致頁表(下面介紹)佔用空間過大,所以頁面需要折中選擇合適的大小,目前大多數系統都使用4KB作為頁的大小。

上面關於虛擬記憶體如何對映到實體記憶體程式喵只介紹了MMU,但是MMU是如何工作的還沒有介紹,MMU通過頁表這個工具將虛擬地址轉換為實體地址。32位的虛擬地址分成兩部分(虛擬頁號和偏移量),MMU通過頁表找到了虛擬頁號對應的物理頁號,物理頁號+偏移量就是實際的實體地址。

具體如圖:

圖只表示了頁表的大體功能,頁表的結構其實還很複雜,下面會具體介紹。

頁表的目的就是虛擬頁面對映為實體記憶體的頁框,頁表可以理解為一個數學函式,函式的輸入是虛擬頁號,函式的輸出是物理頁號,通過這個函式可以把虛擬頁面對映到物理頁號,從而確定實體地址。不同機器的頁表結構不同,通常頁表的結構如下:

頁框號:最主要的一項,頁表最主要的目的就是找到物理頁號;

有效位:1表示有效,表示該表項是有效的,如果為0,表示該表項對應的虛擬頁面現在不在記憶體中,訪問該頁面會引起缺頁中斷,缺頁中斷後會去物理空間找到一個可用的頁框填回到頁表中;

保護位:表示一個頁允許什麼型別的訪問,可讀可寫還是可執行;

修改位:該位反應了頁面的狀態,在作業系統重新分配頁框時有用,在寫入一頁時由硬體自動設定該位,重新分配頁框時,如果一個頁面已經被修改過,則必須把它這個髒頁寫回磁碟,如果沒有被修改過,表示該頁是乾淨的,它在磁碟上的副本依然是有效的,直接丟棄該頁面即可。

訪問位:該位主要用於幫助作業系統在發生缺頁中斷時選擇要被淘汰的頁面,不再使用的頁面顯然比正在使用的頁面更適合被淘汰,該位在頁面置換演算法中發揮重要作用。

快取記憶體禁止位:該位用於禁止該頁面被快取記憶體。

如何加快地址對映速度?

每次訪問記憶體都需要進行虛擬地址到實體地址的對映,每次對映都需要訪問一次頁表,所有的指令執行都必須通過記憶體,很多指令也需要訪問記憶體中的運算元,因此每條指令執行基本都會進行多次頁表查詢,為了程式執行速度,指令必須要在很短的時間內執行完成,而頁表查詢對映不能成為指令執行的瓶頸,所以需要提高頁表查詢對映的速度。

如何才能提高速度呢?可以為頁表提供一個快取,通過快取進行對映比通過頁表對映速度更快,這個快取是一個小型的硬體裝置,叫快表(TLB),MMU每次進行虛擬地址轉換時,首先去TLB中查詢,找到了有效的物理頁框則直接返回,如果沒有找到則進行正常的頁表訪問,頁表中找到後則更新TLB,從TLB中淘汰一個表項,然後用新找到的表項替代它,這樣下次相同的頁面過來時可以直接命中TLB找到對應的實體地址,速度更快,不需要繼續去訪問頁表。

這裡之所以認為TLB能提高速度主要依靠程式區域性性原理,程式區域性性原理是指程式在執行過程中的一個較短時間,所執行的指令地址和要訪問的資料通常都侷限在一塊區域內,這裡可分為時間區域性性和空間區域性性:

時間區域性性:一條指令的一次執行和下次執行,一個數據的一次訪問和下次訪問都集中在一個較短時間內;

空間區域性性:當前指令和鄰近的幾條指令,當前訪問的資料和鄰近的幾個資料都集中在一個較小區域內。

通過TLB可以加快虛擬地址到實體地址的轉換速度,還有個問題,現在都是64位作業系統啦,有很大的虛擬地址空間,虛擬地址空間大那對應的頁表也會非常大,又加上多個程序多個頁表,那計算機的大部分空間就都被拿去存放頁表,有沒有更好的辦法解決頁表大的問題呢?答案是多級頁表。

tips:頁表為什麼大?32位環境下,虛擬地址空間有4GB,一個頁大小是4KB,那麼整個頁表就需要100萬頁,而每個頁表項需要4個位元組,那整個頁表就需要4MB的記憶體空間,又因為每個程序都有一個自己的頁表,多個程序情況下,這簡直就是災難。

如圖,以一個32位虛擬地址的二級頁表為例,將32位虛擬地址劃分為10位的PT1域,10位的PT2域,以及12位的offset域,當一個虛擬地址被送入MMU時,MMU首先提取PT1域並把其值作為訪問第一級頁表的索引,之後提取PT2域把把其值作為訪問第二級頁表的索引,之後再根據offset找到對應的頁框號。

32位的虛擬地址空間下:每個頁面4KB,且每條頁表項佔4B:

一級頁表:程序需要1M個頁表項(4GB / 4KB = 1M, 2^20個頁表項),即頁表(每個程序都有一個頁表)佔用4MB(1M * 4B = 4MB)的記憶體空間。

二級頁表:一級頁表對映4MB(2^22)、二級頁表對映4KB,則需要1K個一級頁表項(4GB / 4MB = 1K, 2^10個一級頁表項)、每個一級頁表項對應1K個二級頁表項(4MB / 4KB = 1K),這樣頁表佔用4.004MB(1K * 4B + 1K * 1K * 4B = 4.004MB)的記憶體空間。

二級頁表佔用空間看著貌似變大了,為什麼還說多級頁表省記憶體呢?

每個程序都有4GB的虛擬地址空間,而顯然對於大多數程式來說,其使用到的空間遠未達到4GB,何必去對映不可能用到的空間呢?

也就是說,一級頁表覆蓋了整個4GB虛擬地址空間,但如果某個一級頁表的頁表項沒有被用到,也就不需要建立這個頁表項對應的二級頁表了,即可以在需要時才建立二級頁表。做個簡單的計算,假設只有20%的一級頁表項被用到了,那麼頁表佔用的記憶體空間就只有0.804MB(1K4B+0.21K1K4B=0.804MB),對比單級頁表的4M是不是一個巨大的節約?

那麼為什麼不分級的頁表就做不到這樣節約記憶體呢?我們從頁表的性質來看,儲存在主存中的頁表承擔的職責是將虛擬地址翻譯成實體地址。假如虛擬地址在頁表中找不到對應的頁表項,計算機系統就不能工作了。所以頁表一定要覆蓋全部虛擬地址空間,不分級的頁表就需要有1M個頁表項來對映,而二級頁表則最少只需要1K個頁表項(此時一級頁表覆蓋到了全部虛擬地址空間,二級頁表在需要時建立)。

二級頁表其實可以不在記憶體中:其實這就像是把頁表當成了頁面。當需要用到某個頁面時,將此頁面從磁碟調入到記憶體;當記憶體中頁面滿了時,將記憶體中的頁面調出到磁碟,這是利用到了程式執行的區域性性原理。我們可以很自然發現,虛擬記憶體地址存在著區域性性,那麼負責對映虛擬記憶體地址的頁表項當然也存在著區域性性了!這樣我們再來看二級頁表,根據區域性性原理,1024個第二級頁表中,只會有很少的一部分在某一時刻正在使用,我們豈不是可以把二級頁表都放在磁碟中,在需要時才調入到記憶體?

我們考慮極端情況,只有一級頁表在記憶體中,二級頁表僅有一個在記憶體中,其餘全在磁碟中(雖然這樣效率非常低),則此時頁表佔用了8KB(1K4B+11K*4B=8KB),對比上一步的0.804MB,佔用空間又縮小了好多倍!(這裡參考的下面知乎連結中大佬的回答)

六、什麼是缺頁中斷?

缺頁中斷就是要訪問的頁不在主存中,需要作業系統將頁調入主存後再進行訪問,此時會暫時停止指令的執行,產生一個頁不存在的異常,對應的異常處理程式就會從選擇一頁調入到記憶體,調入記憶體後之前的異常指令就可以繼續執行。

缺頁中斷的處理過程如下:

如果記憶體中有空閒的物理頁面,則分配一物理頁幀r,然後轉第4步,否則轉第2步; 選擇某種頁面置換演算法,選擇一個將被替換的物理頁幀r,它所對應的邏輯頁為q,如果該頁在記憶體期間被修改過,則需把它寫回到外存; 將q所對應的頁表項進行修改,把駐留位置0; 將需要訪問的頁p裝入到物理頁面r中; 修改p所對應的頁表項的內容,把駐留位置1,把物理頁幀號置為x; 重新執行被中斷的指令。

七、頁面置換演算法都有哪些?

當缺頁中斷髮生時,需要調入新的頁面到記憶體中,而記憶體已滿時,選擇記憶體中哪個物理頁面被置換是個學問,由此引入了多種頁面置換演算法,致力於儘可能減少頁面的換入換出次數(缺頁中斷次數)。儘量把未來不再使用的或短期內較少使用的頁面換出,通常在程式區域性性原理指導下依據過去的統計資料來進行預測。

最優頁面置換演算法:當一個缺頁中斷髮生時,對於儲存在記憶體當中的每一個邏輯頁面,計算在它的下一次訪問之前,還需等待多長時間,從中選擇等待時間最長的那個,作為被置換的頁面。注意這只是一種理想情況,在實際系統中是無法實現的,因為作業系統不可能預測未來,不知道每一個頁面要等待多長時間以後才會再次被訪問。該演算法可用作其它演算法的效能評價的依據(在一個模擬器上執行某個程式,並記錄每一次的頁面訪問情況,在第二遍執行時即可使用最優演算法)。

先進先出演算法:最先進入的頁面最先被淘汰,這種演算法很簡單,就不過多介紹啦。

最近最久未使用演算法:傳說中的LUR演算法,當發生缺頁中斷時,選擇最近最久沒有使用過的頁面淘汰,該演算法會給每個頁面一個欄位,用於記錄自上次訪問以來所經歷的時間T,當需要淘汰一個頁面時,選擇已有頁面中T值最大的頁面進行淘汰。

第二次機會頁面置換演算法:先進先出演算法的升級版,只是在先進先出演算法的基礎上做了一點點改動,因為先進先出演算法可能會把經常使用的頁面置換出去,該方法會給這些頁面多一次機會,給頁面設定一個修改位R,每次淘汰最老頁面時,檢查最老頁面的R位,如果R位是0,那麼代表這個頁面又老又沒有被二次使用過,直接淘汰,如果這個頁面的R位是1,表示該頁面被二次訪問過,將R位置0,並且把該頁面放到連結串列的尾端,像該頁面是最新進來的一樣,然後繼續按這種方法淘汰最老的頁面。

時鐘頁面置換演算法:第二次機會頁面演算法的升級版,儘管二次機會頁面演算法是比較合理的演算法,但它需要在連結串列中經常移動頁面,效率比較低,時鐘頁面置換演算法如圖,該演算法把所有的頁面都儲存在一個類似時鐘的環形連結串列中,一個錶針指向最老的頁面,當發生缺頁中斷時,演算法首先檢查錶針指向的頁面,如果它的R位是0就淘汰該頁面,並且把新的頁面插入這個位置,然後錶針移動到下一個位置,如果R位是1就將R位置0並把錶針移動到下一個位置,重複這個過程直到找到一個R位是0的頁面然後淘汰。

最不常用演算法:當發生缺頁中斷時,選擇訪問次數最少的那個頁面去淘汰。該演算法可以給每個頁面設定一個計數器,被訪問時,該頁面的訪問計數器+1,在需要淘汰時,選擇計數器值最小的那個頁面。

這裡有個問題:一個頁面如果在開始的時候訪問次數很多,但之後就再也不用了,那它可能永遠都不會淘汰,但它又確實需要被淘汰,怎麼辦呢?可以定期把減少各個頁面計數器的值,常見的方法是定期將頁面計數器右移一位。

tips:最不常用演算法(LFU)和最近最久未使用演算法(LRU)的區別:LRU考察的是最久未訪問,時間越短越好,而LFU考察的是訪問的次數或頻度,訪問次數越多越好。

工作集頁面置換演算法

介紹該演算法時首先介紹下什麼是工作集。

  • 工作集是指一個程序當前正在使用的頁面的集合,可以用二元函式W(t, s)表示:

  • t表示當前的執行時刻)s表示工作集視窗,表示一個固定的時間段

  • W(t, s)表示在當前時刻t之前的s時間段中所有訪問頁面所組成的集合

不同時間下的工作集會有所變化,如圖:

  • 程序開始執行後隨著訪問新頁面逐步建立較穩定的工作集

  • 當記憶體訪問的區域性性區域的位置大致穩定時(只訪問那幾個頁面 沒有大的改變時) 工作集大小也大致穩定

  • 區域性性區域的位置改變時(程序前一項事情做完 去做下一項事情時) 工作集快速擴張和快速收縮過渡到下一個穩定值

工作集置換演算法主要就是換出不在工作集中的頁面,示例如圖:

0次訪問e:缺頁,裝入e

1次訪問d:缺頁,裝入d

2次訪問a:缺頁,裝入a

3次訪問c:缺頁,裝入c

4次訪問c:命中,時間視窗【1-4】,淘汰e

5次訪問d:命中,時間視窗【2-5

6次訪問b:缺頁,時間視窗【3-6】,淘汰a,裝入b

7次訪問c:命中,時間視窗【4-7

8次訪問e:缺頁,時間視窗【5-8】,裝入e

9次訪問c:命中,時間視窗【6-9】,淘汰d,裝入c

10次訪問e:命中,時間視窗【7-10】,淘汰b

11次訪問a:缺頁,時間視窗【8-11】,裝入a

12次訪問d:缺頁,時間視窗【9-12】,裝入d

工作集時鐘頁面置換演算法

在工作集頁面置換演算法中,當缺頁中斷髮生後,需要掃描整個頁表才能直到頁面的狀態,進而才能確定被淘汰的是哪個頁面,因此比較耗時,所以引入了工作集時鐘頁面演算法。與時鐘演算法改進了先進先出演算法類似,工作集頁面置換演算法+時鐘演算法=工作集時鐘頁面置換演算法。避免了每次缺頁中斷都需要掃描整個頁表的開銷。

八、什麼是分段記憶體管理?

關於分段記憶體管理我們平時見的最多的應該就是Linux可執行程式的程式碼段資料段之類的啦,要了解分段最好的方式就是了解它的歷史。分段起源於8086CPU,那時候程式訪問記憶體還是直接給出相應單元的實體地址,為了方便多道程式併發執行,需要支援對各個程式進行重定位,如果不支援重定位,涉及到記憶體訪問的地方都需要將地址寫死,進而把某個程式載入到實體記憶體的固定區間。通過分段機制,程式中只需要使用段的相對地址,然後更改段的基址,就方便對程式進行重定位。而且8086CPU的地址線寬度是20位,可定址範圍可以達到1MB,但是它們的暫存器都是16位,直接使用1個16位暫存器不可能訪存達到1MB,因此引入了段,引入了段暫存器,段暫存器左移4位+偏移量就可以生成20位的地址,從而達到1MB的定址範圍。

以如今的科技水平,其實已經不再需要這種段移位加偏移的方式來訪存,分段更多的是一種歷史包袱,沒有多大實際作用,而且我們經常見到的可執行程式中程式碼段資料段這些更多是為了在邏輯上能夠更清晰有序的構造程式的組織結構。Linux實際上沒有使用分段而只使用了分頁管理,這樣會更加簡單,現在的分段其實更多是為了使邏輯更加清晰。一個公司,為了方便管理都會劃分為好多個部門,這其實和分段邏輯相似,沒有什麼物理意義但是邏輯更加清晰。

原文連線:原文連結在這