第一階段:沒有內存抽象
沒有內存抽象對於內存的管理通常非常簡單,除去操作系統所用的內存之外,全部給用戶程序使用。或是在內存中多留一片區域給驅動程序使用,如圖1所示。

圖1. 沒有內存抽象時,對內存的使用
第一種情況操作系統存於RAM中,放在內存的低地址,第二種情況操作系統存在於ROM中,存在內存的高地址,一般老式的手機操作系統是這么設計的。
如果這種情況下,想要操作系統可以執行多進程的話,
缺陷:多線程直接操作內存,會產生沖突。唯一的解決方案就是和硬盤搞交換,當一個進程執行到一定程度時,整個存入硬盤,轉而執行其它進程,到需要執行這個進程時,再從硬盤中取回內存,只要同一時間內存中只有一個進程就行,這也就是所謂的交換(Swapping)技術。
第二階段:內存抽象——為了解決多線程
內存抽象允許每個進程擁有自己的地址。這還需要硬件上存在兩個寄存器,基址寄存器(base register)和界址寄存器(limit register),第一個寄存器保存進程的開始地址,第二個寄存器保存上界,防止內存溢出。
問題1:內存大小不可能容納下所有並發執行的進程。
解決方法:交換(Swapping)。和前面所講的交換大同小異,交換的基本思想是,將閑置的進程交換出內存,暫存在硬盤中,待執行時再交換回內存,比如下面一個例子,當程序一開始時,只有進程A,逐漸有了進程B和C,此時來了進程D,但內存中沒有足夠的空間給進程D,因此將進程B交換出內存,分給進程D。如圖2所示。

圖2. 交換技術
問題2:如圖2,進程D和C之間的空間由於太小無法另任何進程使用,這也就是所謂的外部碎片。
解決方法:比如內存整理軟件,原理是申請一塊超大的內存,將所有進程置換出內存,然后再釋放這塊內存,從而重新加載進程,使得外部碎片被消除。這也是為什么運行完內存整理會狂讀硬盤的原因。
問題3:創建進程時分配多少內存。如果分配多了,會產生內部碎片,浪費了內存,而分配少了會造成內存溢出。
解決方法:一種是直接多分配一點內存空間用於進程在內存中的增長,另一種是將增長區分為數據段和棧(用於存放返回地址和局部變量),如圖3所示。

圖3. 創建進程時預留空間用於增長
當預留的空間不夠滿足增長時,操作系統首先會看相鄰的內存是否空閑,如果空閑則自動分配,如果不空閑,就將整個進程移到足夠容納增長的空間內存中,如果不存在這樣的內存空間,則會將閑置的進程置換出去。
問題4:操作系統如何管理內存
法I:位圖。將內存划為多個大小相等的塊,比如一個32K的內存1K一塊可以划為32塊,則需要32位(4字節)來表示其使用情況,使用位圖將已經使用的塊標為1,位使用的標為0.
法II:鏈表。將內存按使用或未使用分為多個段進行鏈接,如下圖中的P表示進程,從0-2是進程,H表示空閑,從3-4表示是空閑。

圖4. 位圖和鏈表表示內存的使用情況
使用位圖表示內存簡單明了,但一個問題是當分配內存時必須在內存中搜索大量的連續0的空間,這是十分消耗資源的操作。相比之下,使用鏈表進行此操作將會更勝一籌。
當利用鏈表管理內存的情況下,創建進程時分配什么樣的空閑空間也是個問題。通常情況下有如下幾種算法來對進程創建時的空間進行分配。
- 臨近適應算法(Next fit)—從當前位置開始,搜索第一個能滿足進程要求的內存空間
- 最佳適應算法(Best fit)—搜索整個鏈表,找到能滿足進程要求最小內存的內存空間
- 最大適應算法(Wrost fit)—找到當前內存中最大的空閑空間
- 首次適應算法(First fit) —從鏈表的第一個開始,找到第一個能滿足進程要求的內存空間
第三階段:虛擬內存(Virtual Memory)——為了解決大進程的內存要求
在早期的操作系統曾使用覆蓋(overlays)來解決這個問題,將一個程序分為多個塊,基本思想是先將塊0加入內存,塊0執行完后,將塊1加入內存。依次往復,這個解決方案最大的問題是需要程序員去程序進行分塊,這是一個費時費力讓人痛苦不堪的過程。
虛擬內存的基本思想是,每個進程有用獨立的邏輯地址空間,內存被分為大小相等的多個塊,稱為頁(Page).每個頁都是一段連續的地址。對於進程來看,邏輯上貌似有很多內存空間,其中一部分對應物理內存上的一塊(稱為頁框,通常頁和頁框大小相等),還有一些沒加載在內存中的對應在硬盤上,如圖5所示。

圖5. 虛擬內存和物理內存以及磁盤的映射關系
由圖5可以看出,虛擬內存實際上可以比物理內存大。當訪問虛擬內存時,會訪問MMU(內存管理單元)去匹配對應的物理地址(比如圖5的0,1,2),而如果虛擬內存的頁並不存在於物理內存中(如圖5的3,4),會產生缺頁中斷,從磁盤中取得缺的頁放入內存,如果內存已滿,還會根據某種算法將磁盤中的頁換出。
MMU中存儲頁表,用來匹配虛擬內存和物理內存。頁表中每個項通常為32位,即4byte,除了存儲虛擬地址和頁框地址之外,還會存儲一些標志位,比如是否缺頁,是否修改過,寫保護等。因為頁表中每個條目是4字節,現在的32位操作系統虛擬地址空間是2^32,假設每頁分為4k,也需(2^32/(4*2^10))*4=4M的空間,為每個進程建立一個4M的頁表並不明智。因此在頁表的概念上進行推廣,產生二級頁表,雖然頁表條目沒有減少,但內存中可以僅僅存放需要使用的二級頁表和一級頁表,大大減少了內存的使用。
每個進程有4GB的虛擬地址空間,每個進程自己的一套頁表。程序中使用的都是4GB地址空間中的虛擬地址。而訪問物理內存,需要使用物理地址。
一個頁表的大小為4K字節,頁表項(PTE, page table entry)的大小為4個字節(32bit),所以一個頁表中有1024個頁表項。
- 二級頁表中的每一項的內容高20bit用來放一個物理頁的物理地址,低12bit放着一些標志。
- 一級頁表中的每一項的內容高20bit用來放一個二級頁表的物理地址,低12bit放着一些標志。
CPU把虛擬地址轉換成物理地址:一個虛擬地址,大小4個字節(32bit),分為3個部分:第22位到第31位這10位(最高10位)是頁目錄中的索引,第12位到第21位這10位是頁表中的索引,第0位到第11位這12位(低12位)是頁內偏移。一個一級頁表有1024項,虛擬地址最高的10bit剛好可以索引1024項(2的10次方等於1024)。一個二級頁表也有1024項,虛擬地址中間部分的10bit,剛好索引1024項。虛擬地址最低的12bit(2的12次方等於4096),作為頁內偏移,剛好可以索引4KB,也就是一個物理頁中的每個字節。
頁面替換算法:物理內存是極其有限的,當虛擬內存所求的頁不在物理內存中時,將需要將物理內存中的頁替換出去,選擇哪些頁替換出去就顯得尤為重要。
- 最佳置換算法(Optimal Page Replacement Algorithm):將未來最久不使用的頁替換出去,這聽起來很簡單,但是無法實現。但是這種算法可以作為衡量其它算法的基准。
- 最近不常使用算法(Not Recently Used Replacement Algorithm):這種算法給每個頁一個標志位,R表示最近被訪問過,M表示被修改過。定期對R進行清零。這個算法的思路是首先淘汰那些未被訪問過R=0的頁,其次是被訪問過R=1,未被修改過M=0的頁,最后是R=1,M=1的頁。
- 先進先出頁面置換算法(First-In,First-Out Page Replacement Algorithm):淘汰在內存中最久的頁,這種算法的性能接近於隨機淘汰。並不好。
- 改進型FIFO算法(Second Chance Page Replacement Algorithm):這種算法是在FIFO的基礎上,為了避免置換出經常使用的頁,增加一個標志位R,如果最近使用過將R置1,當頁將會淘汰時,如果R為1,則不淘汰頁,將R置0.而那些R=0的頁將被淘汰時,直接淘汰。
- 時鍾替換算法(Clock Page Replacement Algorithm):雖然改進型FIFO算法避免置換出常用的頁,但由於需要經常移動頁,效率並不高。因此在改進型FIFO算法的基礎上,將隊列首位相連形成一個環路,當缺頁中斷產生時,從當前位置開始找R=0的頁,而所經過的R=1的頁被置0,並不需要移動頁。
- 最久未使用算法(LRU Page Replacement Algorithm):LRU算法的思路是淘汰最近最長未使用的頁。這種算法性能比較好,但實現起來比較困難。
