先解釋下一個困擾了我很久的問題:虛擬地址(vitural address)和邏輯地址(logical address)的區別。
大部分作業系統的書籍要麼寫的是虛擬地址,要麼寫的是邏輯地址,看的我一臉懵逼。
在《深入理解 Linux 核心》這本書中終於找到了確切的答案,這裡我就不寫出來了,扣概念的話這倆確實是有些區別的,不過對於我們日常使用以及理解作業系統來說的話,暫且可以把虛擬地址和邏輯地址理解為同一個意思。
你看到的所有地址都不是真的
下面這段 C 程式碼摘錄自《作業系統導論 - [美] 雷姆茲·H.阿帕希杜塞爾》,依次列印出 main 函式的地址,由 malloc(類似於 Java 中的 new 操作)返回的堆空間分配的值,以及棧上一個整數的地址:
得到以下輸出:
我們需要知道的是,所有這些列印出來的地址都是虛擬的,在實體記憶體中這些地址並不真實存在,它們最終都將由作業系統和 CPU 硬體翻譯成真正的實體地址,然後才能從真實的物理位置獲取該地址的值。
OK,上述就當作一個引子,讓各位對實體地址和虛擬地址有個直觀的理解,下面正文開始。
物理定址 Physical Addressing
實體地址的概念很好理解,你可以把它稱為真正的地址。《深入理解計算機系統 - 第 3 版》中給出的實體地址(physical address)的定義如下:
計算機系統的主存被組織成一個由 M 個連續的位元組大小的單元組成的陣列。每位元組都有一個唯一的實體地址。
比如說,第一個位元組的實體地址是 0,接下來的位元組地址是 1,再下一個是 2,以此類推,給定這種簡單的結構,CPU 訪問記憶體的最自然的方式就是使用這樣的實體地址。我們把這種方式稱為物理定址(physical addressing)。
舉個例子,比如說當程式執行了一條載入指令,指令內容是從實體地址 4 中讀取 4 位元組字傳送到某個暫存器中。
物理定址過程如下:當 CPU 執行到這條指令時,會生成實體地址 4,然後通過記憶體主線,把它傳遞給記憶體,記憶體取出從實體地址 4 處開始的 4 位元組字,並將它返回給 CPU,CPU 會將它存放到指定的暫存器中。看下圖:
其實不難發現,物理定址這種方式,每一個程式都直接訪問實體記憶體,其實是存在重大缺陷的:
1)首先,使用者程式可以定址記憶體的任意一個位元組,它們就可以很容易地破壞作業系統,從而使系統慢慢地停止執行。
2)再次,這種定址方式使得作業系統中同時執行兩個或以上的程式幾乎是不可能的。
舉個例子,我們開啟了三個相同的程式(計算器),都執行到某一步。比方說,使用者在這三個程式的介面上分別輸入了 10、100、1000,其對應的指令就是把使用者輸入的數字儲存在記憶體中的某個地址中。如果這個位置只能儲存一個數,那應該儲存哪個呢?這不就衝突了嗎?
再舉個例子,摘自《現代作業系統 - 第 3 版》:
一個程式給實體記憶體地址 1000 賦值也就是存入了一些資料後,另一個程式也同樣給這個地址賦值,那麼第二個程式的賦值會覆蓋掉第一個程式所賦的值,這會造成兩個程式同時崩潰。
當然了,我們也說了是幾乎不可能,不是完全不可能,還是有一些方法可以在物理定址這種方式下實現多個程式併發執行的。
最簡單的方法就是:首先,將空閒的程式儲存在磁碟上,這樣當它們不執行時就不會佔用記憶體,然後,讓一個程式(或者說程式)單獨佔用全部記憶體執行一小段時間,當發生上下文切換的時候,就停止這個程式,並將它所有的狀態資訊儲存在磁碟上,再載入其他程式的狀態資訊,然後執行一段時間...... 只要在某一個時間記憶體中只有一個程式,那麼就不會發生上述所說的地址衝突。這就實現了一種比較粗糙的併發。
為什麼說他是粗糙的呢,因為這種方法有一個問題:將全部的記憶體資訊儲存到磁碟太慢了!特別是當記憶體增長的時候。
因此,我們考慮把程式對應的記憶體一直留在實體記憶體中,在發生上下文切換的時候就切換到特定的區域。
如下圖所示,有 3 個程式(A、B、C),每個程式擁有從 512KB 實體記憶體中切出來給它們的一小部分記憶體,可以理解為這 3 個程式共享實體記憶體:
顯然,這種方式是存在一定安全隱患的。畢竟如果各個程式之間可以隨意讀取、寫入內容的話那就亂套了。
那麼如何對每個程式使用的地址進行保護(protection)呢?繼續使用實體記憶體模型肯定是不行了,因此作業系統創造了一個新的記憶體抽象,引入了一個新的記憶體模型,那就是虛擬地址空間,很多書中都會直接稱呼為 “地址空間(Address Space)”。
虛擬定址 Virtual Addressing
我先通俗地解釋下虛擬地址空間和虛擬地址的概念,直接上書中的定義讀起來有點生澀。
就是說每個程式的棧啊、堆啊、程式碼段啊等等它們的實際實體記憶體地址對於這個程式來說是不可見的,誰也不能直接訪問這個實體地址。
那我們怎麼去訪問這個程式呢?
作業系統會給每個程式分配一個虛擬地址空間(vitural address),每個程式包含的棧、堆、程式碼段這些都會從這個地址空間中被分配一個地址,這個地址就被稱為虛擬地址。底層指令寫入的地址也是虛擬地址。
每個程式都擁有一個自己的地址空間,並且獨立於其他程式的地址空間。也就是說一個程式中的虛擬地址 28 所對應的實體地址與另一個程式中的虛擬地址 28 所對應的實體地址是不同的,這樣就不會發生衝突了。
可以這麼理解,實體地址就是一個倉庫,虛擬地址就是一個門牌,比方說一共有三十個門牌,那麼所有的程式都能看見這三十個門牌,但是他們看見的某個相同門牌,指向的並不是同一個倉庫。
OK,下面再來看《現代作業系統 - 第 3 版》書中對於地址空間的解釋,應該很容易理解了:
地址空間是一個程式可用於定址記憶體的一套地址集合。每個程式都有一個自己的地址空間,並且這個地址空間獨立於其他程式的地址空間(除了在一些特殊情況下程式需要共享它們的地址空間外)。
地址空間的概念非常通用,並且在很多場合中出現。比如電話號碼,在美國和很多其他國家,一個本地電話號碼通常是一個 7 位的數字。因此,電話號碼的地址空間是從 0 000 000 到 9 999 999。
地址空間也可以是非數字的,以 “.com” 結尾的網路域名的集合也是地址空間。這個地址空間是由所有包含 2~63 個字元並且後面跟著 “.com” 的字串組成的,組成這些字串的字元可以是字母、數字和連字元。
到現在你應該已經明白地址空間的概念了,它是很簡單的。
有了虛擬地址空間後,CPU 就可以通過生成一個虛擬地址來訪問主存,這個虛擬地址在被送到記憶體之前會先被轉換成合適的實體地址,這個虛擬地址到實體地址的轉換過程稱為 地址翻譯/地址轉換(address translation)。
地址翻譯需要 CPU 硬體和作業系統的密切合作:CPU 上的記憶體管理單元(Memory Management Unit,MMU)就是專門用來進行虛擬地址到實體地址的轉換的,不過 MMU 需要藉助存放在記憶體中的查詢表,而這張表的內容正是由作業系統進行管理的。
那麼,上述這一套 CPU 生成虛擬地址並進行地址翻譯的流程就是虛擬定址(virtual addressing)。舉個例子,看下圖:
References
- 《作業系統導論 - [美] 雷姆茲·H.阿帕希杜塞爾》
- 《現代作業系統 - 第 3 版》
- 《深入理解計算機系統 - 第 3 版》
? 關注公眾號 | 飛天小牛肉,即時獲取更新
- 博主東南大學碩士在讀,攜程 Java 後臺開發暑期實習生,利用課餘時間運營一個公眾號『 飛天小牛肉 』,2020/12/29 日開通,專注分享計算機基礎(資料結構 + 演算法 + 計算機網路 + 資料庫 + 作業系統 + Linux)、Java 技術棧等相關原創技術好文。本公眾號的目的就是讓大家可以快速掌握重點知識,有的放矢。關注公眾號第一時間獲取文章更新,成長的路上我們一起進步
- 並推薦個人維護的開源教程類專案: CS-Wiki(Gitee 推薦專案,現已累計 1.7k+ star), 致力打造完善的後端知識體系,在技術的路上少走彎路,歡迎各位小夥伴前來交流學習 ~ ?
- 如果各位小夥伴春招秋招沒有拿得出手的專案的話,可以參考我寫的一個專案「開源社群系統 Echo」Gitee 官方推薦專案,目前已累計 700+ star,基於 SpringBoot + MyBatis + MySQL + Redis + Kafka + Elasticsearch + Spring Security + ... 並提供詳細的開發文件和配套教程。公眾號後臺回覆 Echo 可以獲取配套教程,目前尚在更新中。