本文主要根據 “Operating Systems: Three Easy Pieces” 第15章總結而來。
在本頭條号之前的文章中有介紹,操作系統為了實現CPU虛拟化,采用的策略是:大部分情況下,讓進程直接在CPU上運行,這樣效率最高。但是為了不喪失對CPU的控制權,進程在進行系統調用時,或者操作系統設置的定時中斷被觸發時,操作系統又回重新奪回控制權。從這個層面看,高效和可控是現代操作系統設計的核心。内存虛拟化也不例外。
為了實現内存虛拟化,高效和可控也是首先需要考慮的因素。高效意味着,進程能很容易地在硬件的幫助下訪問内存;可控意味着沒有一個進程能随意訪問别的進程的内存,從而對進程進行保護,也對操作系統進行了保護。除了高效和可控,内存虛拟化還有一個額外的要求就是靈活,靈活指的是我們希望進程能按照它想要的方式随意使用它地址空間内的進程,從而讓編程變得簡單。
代碼很簡單,先加載内存中的值,然後加3,最後把計算後的值保存到内存中。從進程的角度看,代碼和數據在它的地址空間内如下圖所示:
可以看到,加3的代碼從地址128開始(靠近代碼段的頂端),變量x的值保存在地址15k的位置(靠近棧的底端),初始值是3000。當指令運行之後,從進程的角度看它會這樣使用内存:
從128獲取指令
執行指令(從15k的位置加載數據)
從132獲取指令
執行指令(不用讀取内存)
從135獲取指令
執行指令(把數據寫入15k的位置)
也就是說,進程認為它的地址空間是從0開始,最大是16k,所有的内存訪問都必須在0-16k之間。然而,為了實現内存虛拟化,操作系統不一定會把進程放到物理内存0開始的位置。一個可能的視圖如下圖所示:
從圖片來看,進程被放在了物理内存32k-48k的位置,16k-32k和48k-64k的位置還未被使用。那麼問題來了,如何在進程無感知的情況下,把進程地址空間放到物理内存的指定位置呢?
基于硬件的動态重定位(或者是動态遷移)
前面的介紹都是背景,下面具體來看操作系統到底是怎麼實現地址翻譯的。這裡就引出了内存管理單元(memory management unit, MMU)中最基本的兩個寄存器:基地址寄存機和上界寄存器,base and bounds。後續會使用英文,因為英文更能表達含義。
當進程最初開始運行的時候,操作系統會根據目前系統可用的内存情況,設置base寄存器的值。比如上面的例子進程的物理地址是從32k開始的,那麼base寄存器會設置成32k。當進程想獲取地址128的指令時,它會被翻譯成 32k(32768) 128=32896,這樣就能從物理地址裡面取到真正的指令了。這就是所謂的地址翻譯!重複一下公式: 物理地址 = 虛拟地址 + 基地址
那麼bounds寄存器什麼時候用呢?也是在進程最初運行的時候,操作系統預分配一定大小的内存給進程,bounds寄存器就會被設置成最大的值,比如上面的例子,被設置成16k。當進程試圖引用大于16k的地址空間時,系統會抛出一個異常,這個異常就是因為檢查了bounds寄存器中的值。
從上面的流程可以看到,操作系統是在進程運行起來之後,根據系統可用的内存狀況來設置base and bounds,這個過程被稱作動态重定位。與它相對的也有靜态重定位,靜态重定位發生在編譯器,也就是說,程序編譯完了就知道基地址是什麼了。可想而知,靜态重定位不具有移植性,在不同内存大小的系統就需要重新編譯,就算内存大小相同也要根據系統可用的内存而定。所以,目前基本上操作系統都是基于MMU中的 base and bounds寄存器,使用動态重定向的技術。
操作系統的角色
上面說到的是硬件,特别是MMU起的作用,那麼操作系統起了什麼作用呢?
首先,系統可用的内存列表是由操作系統維護的。有很多數據結構可以完成這個任務,随着我們的深入研究會不斷介紹新的數據結構。這裡我們先看最簡單的數據結構叫空閑鍊表,free list。Free List很簡單,把可用的内存分成很多塊,每一塊當作一個節點放到鍊表中。當新的進程來的時候,從列表中取一段内存給它,并把這段内存删掉。當進程退出後,再把内存塊放到free list裡面來。
其次,在CPU做上下文切換的時候,操作系統必須要把base and bounds的數據也存儲到PCB中。這樣當進程重新被調度運行的時候,它依然能找到自己的代碼和數據。
第三,既然進程可以切換,base and bounds可以被保存和讀取,那麼在适當的時候,操作系統也可以移動進程在内存中的位置,隻需要移動之後把base and bounds更新就可以了。這其實也是動态重定位的體現。
最後,操作系統必須在啟動的時候,額外注冊一個異常句柄,也就是bounds溢出的處理函數,保證進程訪問界限之外的内存區域被操作系統拒絕。
總結
基于之前我們做的假設,内存空間的大小是一定的,而且小于物理内存的大小,所以free list能滿足我們的需求。但是我們應該看到,這些假設可能造成的問題。畢竟,并不是所有的進程用的地址空間都是一樣的,我們系統地址空間的大小是可變的。另外,如果有些内存塊太小它就沒辦法被分配給進程,這塊内存就會被浪費掉,也就是我們說的内存碎片。
解決這些問題就需要引入内存分頁和分段的技術,這是後面會繼續的内容。歡迎大家訂閱我的頭條号,第一時間收到更新,謝謝!
,更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!