記憶體管理兩部曲之實體記憶體管理

飛天小牛肉發表於2021-05-22

記憶體管理總覽

先籠統地總結下記憶體管理到底是幹啥的,下面這段話摘自《現代作業系統 - 第 3 版》:

記憶體管理的任務就是有效地管理記憶體,即記錄哪些記憶體是正確使用的,哪些記憶體是空閒的,在程式需要時為其分配記憶體,在程式使用完後釋放記憶體。

眾所周知,當前計算機都是基於馮·偌依曼儲存程式式的計算機,程式和資料在執行和使用時都需要存放在記憶體中。

設計作業系統的重要目標之一就是提高計算機資源的利用率,而隨著多核 CPU 的盛行,多道程式設計技術大行其道。因此,必須合理地管理記憶體空間,使盡量多的程式/作業能夠同時存放於記憶體中以提高 CPU 的利用率。

通俗來說,記憶體管理所研究的內容無外乎以下這三個方面:

  • Fetch
  • Placement
  • 替換 Replacement

所謂 “取” 研究的就是,應該將哪個程式(或程式的某些部分)從外存(磁碟)調入記憶體。

“放” 研究的則是,將從外存(磁碟)中 “取” 來的程式(或程式的某部分)按照何種方式放在記憶體的什麼地方。

很顯然,“放” 是記憶體管理的基礎,目前 “放” 的技術可歸結成兩類:

1)一類是連續分配,即執行的程式和資料必須放在記憶體的一片連續空間中。

連續分配管理方式包括單一連續分配、固定分割槽分配和動態分割槽分配。

2)另一類是不連續分配,即執行的程式和資料可以放在記憶體的多個不相鄰的塊中。

不連續分配管理方式包括基本分頁管理、基本分段管理和基本段頁式管理。


看到這裡,各位不妨想一想,如果只有 “取” 操作和 “放” 操作,那麼會導致什麼問題?

隨著使用者程式功能的增加,程式所需要的記憶體空間越來越大,程式空間很容易就突破了實體記憶體的實際大小,導致程式無法執行。

因此,為了解決記憶體不足的情況,緩和大程式與小記憶體之間的矛盾,擴充記憶體容量勢在必行。

可以從物理和邏輯兩方面來考慮擴充記憶體容量,物理擴容沒啥技術含量,需要我們研究的自然是如何從邏輯上擴充記憶體容量。

所謂邏輯擴充,就是說實際上實體記憶體的容量沒有發生改變,但是它能裝的東西卻變多了。

對記憶體的邏輯擴充技術主要有三種:覆蓋技術、交換技術、以及虛擬記憶體。事實上,這些邏輯擴充技術的核心理念都是一致的,我覺得用一個詞來總結就是 “替換”:

所謂 “替換” 和 “取” 操作正好相反,它研究的是將哪個程式(或程式的某部分)暫時從記憶體移到外存(磁碟),以騰出記憶體空間供其他程式(或程式的某部分)佔用。

前兩種邏輯擴充技術已經成為歷史,虛擬記憶體技術是目前的主流。所以也有很多人把記憶體管理這塊的內容直接區分為實體記憶體管理和虛擬記憶體管理,一目瞭然。

對於虛擬記憶體的管理是建立在不連續分配管理方式之上的,包括請求分頁管理、請求分段管理和請求段頁式管理。這幾個概念和上文所說的基本分頁管理、基本分段管理和基本段頁式管理非常容易混淆

其實很容易區分,記住這句話就 OK,摘自百度百科:

如果不具備頁面置換的功能,則稱為基本分頁管理(或稱為純分頁管理),它不具有支援實現虛擬記憶體的功能,它要求把每個作業(程式)全部裝入記憶體後方能執行。

記憶體管理整部分總覽如上,而本文,記憶體管理第一部曲,講的僅是實體記憶體管理這塊。

連續分配管理方式

其實在早期的作業系統中,採用的都是連續記憶體空間分配的策略。那時還沒有引入程式概念,記憶體分配還是以作業(相當於程式)為單位,而所謂連續分配呢就是將作業分配到一段連續的記憶體空間

連續分配管理並非本文的重點,面試中更是冷門,但事實上,這些方法對任何形式的記憶體空間分配都具有參考意義。因此,還是有必要做個簡單的瞭解。

連續分配管理方式包括單一連續分配、固定分割槽分配和動態分割槽分配。

單一連續分配

在沒有作業系統的時期,勿容置疑,整個記憶體空間由單個使用者使用。而隨著作業系統的出現,記憶體管理也隨之出現了,使用者再無法獨佔記憶體資源。

當時的記憶體管理十分簡單,僅將記憶體空間分成兩塊:系統區(用於存放作業系統相關資料)和使用者區(用於存放使用者程式相關資料)。

作業系統可以在低地址部分,也可以在高地址部分,假設作業系統在低地址部分,如圖所示:

單一連續分配的管理方式確實有點過於簡單了,記憶體中只能有一道使用者程式,使用者程式獨佔整個使用者區空間。

缺點自然是顯而易見:只能用於單使用者、單任務的作業系統中;有內部碎片(分配給某程式的記憶體區域 中,如果有些部分沒有用上,就是“內部碎片”);記憶體利用率極低。

固定分割槽分配

20 世紀 60 年代出現了支援多道程式的系統,為了能在記憶體中裝入多道程式,且這些程式之間又不會相互干擾, 於是考慮將整個使用者空間劃分為若干個固定大小的分割槽,在每個分割槽中只裝入一道作業,這樣就形成了最早的、最簡單的一種可執行多道程式的記憶體管理方式。

至於這些分割槽大小是否需要相等,各有各的適用場景:

  • 分割槽大小相等:缺乏靈活性。但是適合用於一臺計算機控制多個相同物件的場合(比如鋼鐵廠有 n 個相同的鍊鋼爐,就可以把記憶體空間分為 n 個大小相等的區域存放 n 個鍊鋼控制程式)
  • 分割槽大小不等:增加了靈活性,可以滿足不同大小的程式需求

遺憾的是,雖然固定分割槽分配的方式支援了多道程式,但是仍然會產生內部碎片,記憶體利用率依然比較低。為此,人們又引入了動態分割槽分配,這種方法對使用者區域實施動態分割,從而改善了記憶體空間的利用效果。

動態分割槽分配

動態分割槽分配又稱為可變分割槽分配。這種分配方式不會預先劃分記憶體分割槽,而是在程式裝入記憶體時, 根據程式的大小動態地建立分割槽,並使分割槽的大小正好適合程式的需要。因此係統分割槽的大小和數目是可變的。

動態分割槽分配比較複雜,需要用特殊的資料結構記錄記憶體的使用情況,具體的細節這裡就不再詳細介紹了。

非連續分配管理方式

可以看出來,連續的記憶體分配具有易理解、訪問效率高等優點。但是,由於其要求把作業(程式)放在記憶體的一片連續區域中,很容易出現大段的連續記憶體空間因為不足夠容納作業或程式而不可用。因此,為了充分利用記憶體空間資源而引入了非連續分配策略。

所謂非連續分配就是說作業(程式)可以放在記憶體的多個不相鄰的塊中。

非連續分配管理方式包括頁式管理、段式管理和段頁式管理。

在閱讀本段之前,需要先了解虛擬地址(邏輯地址)與實體地址的概念,可以參考這篇文章:你看到的所有地址都不是真的

基本分頁管理

所謂頁式管理,我們需要先解釋一下什麼是 “頁”?

首先,將記憶體空間分為一個個大小相等的分割槽,每個分割槽就稱為一個 “頁框(page frame)”。每個頁框有一個編號,即“頁框號”(也成為物理頁框號、記憶體塊號),頁框號從 0 開 始 。

將程式的虛擬地址空間也分為與頁框大小相等的一個個分割槽, 每個分割槽就稱為一個 “頁(page)” 或 “頁面” 。每個頁面也有一個編號, 即“頁號”(也稱為虛擬頁號),頁號也是從 0 開始。

作業系統以頁框為單位為各個程式分配記憶體空間。程式的每個頁面分別放入一個頁框中。也就是說,程式的頁面與記憶體的頁框有一一對應的關係。 各個頁面不必連續存放,可以放到不相鄰(離散)的各個頁框中

舉個例子,如下圖,每個頁面和頁框的大小都是 4KB,我們擁有 64KB 的虛擬地址空間和 32KB 的實體記憶體,因此可以得到 16 個頁面和 8 個頁框:

圖片來源《現代作業系統 - 第 3 版》

前文說過,指令真正執行的時候會將虛擬地址最終轉換為實體地址。

那麼,頁式管理中是如何將虛擬地址(頁面)和實體地址(頁框)進行對映的呢?換句話說,如何根據虛擬地址計算得到實體地址?

為此,作業系統為每個程式建立了一張頁表,這是一個十分重要的資料結構!頁表通常存在程式控制塊(PCB)中。

一個程式對應一張頁表,程式的每個頁面對應一個頁表項,每個頁表項由頁號和塊號(頁框號)組成,記錄著程式頁面和實際存放的記憶體塊之間的對映關係。

從數學角度來說,頁表是一個函式,它的引數是虛擬頁號,結果是物理頁框號。

頁式管理中的兩個重要問題

在任何分頁式系統中,都不可避免地要考慮下面這兩個問題:

  • 問題 1:如何保證虛擬地址到實體地址的轉換足夠快 — 使用快表解決
  • 問題 2:如何解決虛擬地址空間大,頁表也會很大的問題(頁表項多了,頁表自然也就大了)— 使用多級頁表解決

先來看第一個問題,由於每次訪問記憶體,都需要進行虛擬地址到實體地址的轉換,因此,每條指令進行一兩次或更多頁表訪問是必要的,而頁表又是存在於記憶體中的。

那麼,既然訪問頁表(記憶體)次數太多導致其成為了一個效能瓶頸,那我們想個方法使得這個地址對映不用訪問記憶體,訪問一個比記憶體快得多的東西不就行了?

計算機的設計者給出的解決方案大致如此,為計算機設定了一個小型的硬體裝置,將虛擬地址直接對映成實體地址,而不必再訪問頁表。這個裝置就是轉換檢測緩衝區(Translation Lookaside Buffer,TLB),也被稱為快表

為啥說他快呢?因為 TLB 通常內建在 CPU 的 MMU(記憶體管理單元) 中,這訪問速度跟記憶體不是一個檔次的。記憶體中的頁表一般被稱為慢表

那麼,擁有了 TLB 就可以一勞永逸直接放棄頁表了嗎?

當然不。

TLB 僅僅包含少量的表項,每個表項記錄了一個頁面的相關資訊,其表結構大致如下:

圖片來源《現代作業系統 - 第 3 版》

事實上,TLB 的出現是基於這樣一種現象的:大多數程式總是對少量的頁面進行多次的訪問。因此,只有很少的頁表項會被反覆讀取,而其他的頁表項很少被訪問。

TLB 中存放的就是那些會被反覆讀取的頁表項。換句話說,TLB 中存放的就是頁表中的一部分副本。

若 TLB 命中,就不需要再訪問記憶體了;若 TLB 中沒有目標頁表項,則還需要去查詢記憶體中的頁表(慢表),從頁表中得到物理頁框地址,同時將頁表中的該表項新增到 TLB 中。

那麼問題又隨之而來了,如果 TLB 填滿了怎麼辦?

當 TLB 填滿後又要登記新頁時,就會按照一定的淘汰策略淘汰掉快表中的一個頁。


再來看第二個問題,現代大多數計算機系統,一般都支援非常大的虛擬地址空間,從而使頁表變得十分龐大且需要佔用相對可觀的記憶體空間(頁表項多了,頁表自然也就大了)。我們假設系統中只有一個頁表,那即使我們使用的只是虛擬地址空間中的一小部分,也總是需要一整個頁表全部駐留在記憶體中

用來壓縮頁表的常用方法就是使用層次結構的頁表。

以最常見的二級頁表舉例,我們來看多級頁表的處理思路:

把頁表再分頁並離散儲存,然後再建立一張頁表記錄頁表各個部分的存放位置,稱為 “頁目錄表”(或稱外層頁表、頂層頁表)。

圖片來源《作業系統 - 第 3 版》

若分為兩級頁表後,頁表依然很長,則可以對外層頁表再分頁形成三級以上的多級頁表。

多級頁表技術不但突破了頁表必須連續存放的限制,同時當有大片虛擬地址空間未使用時,可以不分配對應頁表空間,因此可節省記憶體。另外,多級頁表增加了訪存次數,因此外層頁表的頁表項應該儘可能保持在 TLB 中,以減少訪存開銷。

基本分段管理

頁式管理雖具有記憶體空間利用率高、管理方法簡單等特點,但是將記憶體空間按頁進行劃分對使用者來說不是很自然。使用者看待程式是以自然段為單位的,比如主程式段、子程式段、資料段等。若使用者要求對資料進行保護,那麼受到保護的基本單位也是自然段。例如,某段只能讀,另一段可執行等。

而分頁完全可能把不屬於同一段的兩塊分到同一頁中。如下圖,第 4 頁中既包含程式段(可執行),又包含資料段(可讀、可寫):

換句話說,雖然頁式管理提高了記憶體利用率,但是頁式管理劃分出來的頁並無任何實際意義

為此,段式管理應運而生。

段式系統是按照使用者作業(程式)中的自然段來劃分邏輯空間的。比如說,使用者作業(程式)由主程式、兩個子程式、棧和一段資料組成,於是可將這個使用者作業(程式)劃分成 5 段,顯然,頁面是定長的而段不是

從圖中可以看出來,段與段之間可以不連續儲存,但是段的內部仍然是連續的。

另外,和基本分頁管理一樣,基本分段管理也需要一個資料結構來記錄虛擬地址和實體地址之間的對映,這個資料結構就是段表

基本段頁管理

如果一個段比較大,把它整個儲存在記憶體中可能很不方便甚至不可能的,因此對它產生了分頁的想法。

對段進行分頁的支援,這就是段頁式管理的基本思想。

簡單來說,就是對虛擬地址空間先進行段的劃分,然後在每一段內再進行頁的劃分。例如,若使用者程式由主程式、子程式和資料段組成,則通過段、頁劃分後如圖所示:

References

  • 《作業系統 - 第 3 版 - 羅宇》
  • 《現代作業系統 - 第 3 版》
  • 《深入理解計算機系統 - 第 3 版》

? 關注公眾號 | 飛天小牛肉,即時獲取更新

  • 博主東南大學碩士在讀,攜程 Java 後臺開發暑期實習生,利用課餘時間運營一個公眾號『 飛天小牛肉 』,2020/12/29 日開通,專注分享計算機基礎(資料結構 + 演算法 + 計算機網路 + 資料庫 + 作業系統 + Linux)、Java 技術棧等相關原創技術好文。本公眾號的目的就是讓大家可以快速掌握重點知識,有的放矢。關注公眾號第一時間獲取文章更新,成長的路上我們一起進步
  • 並推薦個人維護的開源教程類專案: CS-Wiki(Gitee 推薦專案,現已累計 1.7k+ star), 致力打造完善的後端知識體系,在技術的路上少走彎路,歡迎各位小夥伴前來交流學習 ~ ?
  • 如果各位小夥伴春招秋招沒有拿得出手的專案的話,可以參考我寫的一個專案「開源社群系統 Echo」Gitee 官方推薦專案,目前已累計 800+ star,基於 SpringBoot + MyBatis + MySQL + Redis + Kafka + Elasticsearch + Spring Security + ... 並提供詳細的開發文件和配套教程。公眾號後臺回覆 Echo 可以獲取配套教程,目前尚在更新中。

相關文章