操作系統之Linux的內存分頁管理


內存是計算機的主存儲器。內存為進程開辟出進程空間,讓進程在其中保存數據。我將從內存的物理特性出發,深入到內存管理的細節,特別是了解虛擬內存和內存分頁的概念。

 

內存

簡單地說,內存就是一個數據貨架。內存有一個最小的存儲單位,大多數都是一個字節。內存用內存地址(memory address)來為每個字節的數據順序編號。因此,內存地址說明了數據在內存中的位置。內存地址從0開始,每次增加1。這種線性增加的存儲器地址稱為線性地址(linear address)。為了方便,我們用十六進制數來表示內存地址,比如0x00000003、0x1A010CB0。這里的“0x”用來表示十六進制。“0x”后面跟着的,就是作為內存地址的十六進制數。

內存地址的編號有上限。地址空間的范圍和地址總線(address bus)的位數直接相關。CPU通過地址總線來向內存說明想要存取數據的地址。以英特爾32位的80386型CPU為例,這款CPU有32個針腳可以傳輸地址信息。每個針腳對應了一位。如果針腳上是高電壓,那么這一位是1。如果是低電壓,那么這一位是0。32位的電壓高低信息通過地址總線傳到內存的32個針腳,內存就能把電壓高低信息轉換成32位的二進制數,從而知道CPU想要的是哪個位置的數據。用十六進制表示,32位地址空間就是從0x00000000 到0xFFFFFFFF。

內存的存儲單元采用了隨機讀取存儲器(RAM, Random Access Memory)。所謂的“隨機讀取”,是指存儲器的讀取時間和數據所在位置無關。與之相對,很多存儲器的讀取時間和數據所在位置有關。就拿磁帶來說,我們想聽其中的一首歌,必須轉動帶子。如果那首歌是第一首,那么立即就可以播放。如果那首歌恰巧是最后一首,我們快進到可以播放的位置就需要花很長時間。我們已經知道,進程需要調用內存中不同位置的數據。如果數據讀取時間和位置相關的話,計算機就很難把控進程的運行時間。因此,隨機讀取的特性是內存成為主存儲器的關鍵因素。

內存提供的存儲空間,除了能滿足內核的運行需求,還通常能支持運行中的進程。即使進程所需空間超過內存空間,內存空間也可以通過少量拓展來彌補。換句話說,內存的存儲能力,和計算機運行狀態的數據總量相當。內存的缺點是不能持久地保存數據。一旦斷電,內存中的數據就會消失。因此,計算機即使有了內存這樣一個主存儲器,還是需要硬盤這樣的外部存儲器來提供持久的儲存空間。

 

虛擬內存

內存的一項主要任務,就是存儲進程的相關數據。我們之前已經看到過進程空間的程序段、全局數據、棧和堆,以及這些這些存儲結構在進程運行中所起到的關鍵作用。有趣的是,盡管進程和內存的關系如此緊密,但進程並不能直接訪問內存。在Linux下,進程不能直接讀寫內存中地址為0x1位置的數據。進程中能訪問的地址,只能是虛擬內存地址(virtual memory address)。操作系統會把虛擬內存地址翻譯成真實的內存地址。這種內存管理方式,稱為虛擬內存(virtual memory)。

每個進程都有自己的一套虛擬內存地址,用來給自己的進程空間編號。進程空間的數據同樣以字節為單位,依次增加。從功能上說,虛擬內存地址和物理內存地址類似,都是為數據提供位置索引。進程的虛擬內存地址相互獨立。因此,兩個進程空間可以有相同的虛擬內存地址,如0x10001000。虛擬內存地址和物理內存地址又有一定的對應關系,如圖1所示。對進程某個虛擬內存地址的操作,會被CPU翻譯成對某個具體內存地址的操作。

圖1 虛擬內存地址和物理內存地址的對應

應用程序來說對物理內存地址一無所知。它只可能通過虛擬內存地址來進行數據讀寫。程序中表達的內存地址,也都是虛擬內存地址。進程對虛擬內存地址的操作,會被操作系統翻譯成對某個物理內存地址的操作。由於翻譯的過程由操作系統全權負責,所以應用程序可以在全過程中對物理內存地址一無所知。因此,C程序中表達的內存地址,都是虛擬內存地址。比如在C語言中,可以用下面指令來打印變量地址:

int v = 0;
printf("%p", (void*)&v);

 

本質上說,虛擬內存地址剝奪了應用程序自由訪問物理內存地址的權利。進程對物理內存的訪問,必須經過操作系統的審查。因此,掌握着內存對應關系的操作系統,也掌握了應用程序訪問內存的閘門。借助虛擬內存地址,操作系統可以保障進程空間的獨立性。只要操作系統把兩個進程的進程空間對應到不同的內存區域,就讓兩個進程空間成為“老死不相往來”的兩個小王國。兩個進程就不可能相互篡改對方的數據,進程出錯的可能性就大為減少。

另一方面,有了虛擬內存地址,內存共享也變得簡單。操作系統可以把同一物理內存區域對應到多個進程空間。這樣,不需要任何的數據復制,多個進程就可以看到相同的數據。內核和共享庫的映射,就是通過這種方式進行的。每個進程空間中,最初一部分的虛擬內存地址,都對應到物理內存中預留給內核的空間。這樣,所有的進程就可以共享同一套內核數據。共享庫的情況也是類似。對於任何一個共享庫,計算機只需要往物理內存中加載一次,就可以通過操縱對應關系,來讓多個進程共同使用。IPO中的共享內存,也有賴於虛擬內存地址。

 

內存分頁

虛擬內存地址和物理內存地址的分離,給進程帶來便利性和安全性。但虛擬內存地址和物理內存地址的翻譯,又會額外耗費計算機資源。在多任務的現代計算機中,虛擬內存地址已經成為必備的設計。那么,操作系統必須要考慮清楚,如何能高效地翻譯虛擬內存地址。

記錄對應關系最簡單的辦法,就是把對應關系記錄在一張表中。為了讓翻譯速度足夠地快,這個表必須加載在內存中。不過,這種記錄方式驚人地浪費。如果樹莓派1GB物理內存的每個字節都有一個對應記錄的話,那么光是對應關系就要遠遠超過內存的空間。由於對應關系的條目眾多,搜索到一個對應關系所需的時間也很長。這樣的話,會讓樹莓派陷入癱瘓。

因此,Linux采用了分頁(paging)的方式來記錄對應關系。所謂的分頁,就是以更大尺寸的單位頁(page)來管理內存。在Linux中,通常每頁大小為4KB。如果想要獲取當前樹莓派的內存頁大小,可以使用命令:

$getconf PAGE_SIZE

得到結果,即內存分頁的字節數:

4096

返回的4096代表每個內存頁可以存放4096個字節,即4KB。Linux把物理內存和進程空間都分割成頁。

內存分頁,可以極大地減少所要記錄的內存對應關系。我們已經看到,以字節為單位的對應記錄實在太多。如果把物理內存和進程空間的地址都分成頁,內核只需要記錄頁的對應關系,相關的工作量就會大為減少。由於每頁的大小是每個字節的4000倍。因此,內存中的總頁數只是總字節數的四千分之一。對應關系也縮減為原始策略的四千分之一。分頁讓虛擬內存地址的設計有了實現的可能。

無論是虛擬頁,還是物理頁,一頁之內的地址都是連續的。這樣的話,一個虛擬頁和一個物理頁對應起來,頁內的數據就可以按順序一一對應。這意味着,虛擬內存地址和物理內存地址的末尾部分應該完全相同。大多數情況下,每一頁有4096個字節。由於4096是2的12次方,所以地址最后12位的對應關系天然成立。我們把地址的這一部分稱為偏移量(offset)。偏移量實際上表達了該字節在頁內的位置。地址的前一部分則是頁編號。操作系統只需要記錄頁編號的對應關系。

 


圖2 地址翻譯過程

 

多級分頁表

內存分頁制度的關鍵,在於管理進程空間頁和物理頁的對應關系。操作系統把對應關系記錄在分頁表(page table)中。這種對應關系讓上層的抽象內存和下層的物理內存分離,從而讓Linux能靈活地進行內存管理。由於每個進程會有一套虛擬內存地址,那么每個進程都會有一個分頁表。為了保證查詢速度,分頁表也會保存在內存中。分頁表有很多種實現方式,最簡單的一種分頁表就是把所有的對應關系記錄到同一個線性列表中,即如圖2中的“對應關系”部分所示。

這種單一的連續分頁表,需要給每一個虛擬頁預留一條記錄的位置。但對於任何一個應用進程,其進程空間真正用到的地址都相當有限。我們還記得,進程空間會有棧和堆。進程空間為棧和堆的增長預留了地址,但棧和堆很少會占滿進程空間。這意味着,如果使用連續分頁表,很多條目都沒有真正用到。因此,Linux中的分頁表,采用了多層的數據結構。多層的分頁表能夠減少所需的空間。

我們來看一個簡化的分頁設計,用以說明Linux的多層分頁表。我們把地址分為了頁編號和偏移量兩部分,用單層的分頁表記錄頁編號部分的對應關系。對於多層分頁表來說,會進一步分割頁編號為兩個或更多的部分,然后用兩層或更多層的分頁表來記錄其對應關系,如圖3所示。


圖3 多層分頁表


在圖3的例子中,頁編號分成了兩級。第一級對應了前8位頁編號,用2個十六進制數字表示。第二級對應了后12位頁編號,用3個十六進制編號。二級表記錄有對應的物理頁,即保存了真正的分頁記錄。二級表有很多張,每個二級表分頁記錄對應的虛擬地址前8位都相同。比如二級表0x00,里面記錄的前8位都是0x00。翻譯地址的過程要跨越兩級。我們先取地址的前8位,在一級表中找到對應記錄。該記錄會告訴我們,目標二級表在內存中的位置。我們再在二級表中,通過虛擬地址的后12位,找到分頁記錄,從而最終找到物理地址。

多層分頁表就好像把完整的電話號碼分成區號。我們把同一地區的電話號碼以及對應的人名記錄同通一個小本子上。再用一個上級本子記錄區號和各個小本子的對應關系。如果某個區號沒有使用,那么我們只需要在上級本子上把該區號標記為空。同樣,一級分頁表中0x01記錄為空,說明了以0x01開頭的虛擬地址段沒有使用,相應的二級表就不需要存在。正是通過這一手段,多層分頁表占據的空間要比單層分頁表少了很多。
多層分頁表還有另一個優勢。單層分頁表必須存在於連續的內存空間。而多層分頁表的二級表,可以散步於內存的不同位置。這樣的話,操作系統就可以利用零碎空間來存儲分頁表。還需要注意的是,這里簡化了多層分頁表的很多細節。最新Linux系統中的分頁表多達3層,管理的內存地址也比本章介紹的長很多。不過,多層分頁表的基本原理都是相同。

 

綜上,我們了解了內存以頁為單位的管理方式。在分頁的基礎上,虛擬內存和物理內存實現了分離,從而讓內核深度參與和監督內存分配。應用進程的安全性和穩定性因此大為提高。

頁面置換算法

  如果發生了缺頁中斷,就需要從磁盤上將需要的頁面調入內存。如果內存沒有多余的空間,就需要在現有的頁面中選擇一個頁面進行替換。使用不同的頁面置換算法,頁面更換的順序也會各不相同。如果挑選的頁面是之后很快又要被訪問的頁面,那么系統將很開再次產生缺頁中斷,因為磁盤訪問速度遠遠內存訪問速度,缺頁中斷的代價是非常大的。因此,挑選哪個頁面進行置換不是隨隨便便的事情,而是有要求的。

2.1 頁面置換的目標

  頁面置換時挑選頁面的目標主要在於降低隨后發生缺頁中斷的次數或概率

  因此,挑選的頁面應當是隨后相當長時間內不會被訪問的頁面,最好是再也不會被訪問的頁面。BTW,如果可能,最好選擇一個沒有修改過的頁面,這樣替換時就無須將被替換頁面的內容寫回磁盤,從而進一步加快缺頁中斷的響應速度。

  所以,為了達到這個目的,先驅們設計出了各種各樣的頁面置換算法,下面就來看看這些算法。

2.2 隨機更換算法

  在需要替換頁面的時候,產生一個隨機頁面號,從而替換與該頁面號對應的物理頁面。遺憾的是,隨機選出的被替換的頁面不太可能是隨后相當長時間內不會被訪問的頁面。也就是說,這種算法難以保證最小化隨后的缺頁中斷次數。事實上,這種算法的效果相當差。

2.3 先進先出算法

  顧名思義,先進先出(FIFO,First In First Out)算法的核心是更換最早進入內存的頁面,其實現機制是使用鏈表將所有在內存中的頁面按照進入時間的早晚鏈接起來,然后每次置換鏈表頭上的頁面就行了,而新加進來的頁面則掛在鏈表的末端,如下圖所示:

  FIFO的優點是簡單且容易實現,缺點是如果最先加載進來的頁面是經常被訪問的頁面,那么就可能造成被訪問的頁面替換到磁盤上,導致很快就需要再次發生缺頁中斷,從而降低效率。

2.4 第二次機會算法

  由於FIFO只考慮進入內存的時間,不關心一個頁面被訪問的頻率,從而有可能造成替換掉一個被經常訪問的頁面而造成效率低下。那么,可以對FIFO進行改進:在使用FIFO更換一個頁面時,需要看一下該頁面是否在最近被訪問過,如果沒有被訪問過,則替換該頁面。反之,如果最近被訪問過(通過檢查其訪問位的取值),則不替換該頁面,而是將該頁面掛到鏈表末端,並將該頁面進入內存的時間設置為當前時間,並將其訪問位清零。這樣,對於最近被訪問過的頁面來說,相當於給了它第二次機會。

  例如,當A頁面最近被訪問過,即其訪問位R的值為1,則使用第二次機會算法之后,鏈表的格局如下圖所示:

  第二次機會算法簡單、公平且容易實現。但是,每次給予一個頁面第二次機會時,將其移動到鏈表末端需要耗費時間。此外,頁面的訪問位只在頁面替換進行掃描時才可能清零,所以其時間局域性體現得不好,訪問位為1的頁面可能是很久以前訪問的,時間上的分辨粒度太粗,從而影響頁面替換的效果。

2.5 時鍾算法

  為了改善第二次機會算法的缺點,先驅們提出了時鍾算法。時鍾算法的核心思想是:將頁面排成一個時鍾的形狀,該時鍾有一個針臂,每次需要更換頁面時,我們從針臂所指的頁面開始檢查。如果當前頁面的訪問位為0,即從上次檢查到這次,該頁面沒有被訪問過,將該頁面替換。反之,就將其訪問位清零,並順時針移動指針到下一個頁面。重復這些步驟,直到找到一個訪問位為0的頁面。

  例如下圖所示的一個時鍾,指針指向的頁面是F,因此第一個被考慮替換的頁面是F。如果頁面F的訪問位為0,F將被替換。如果F的訪問位為1,則F的訪問位清零,指針移動到頁面G。

  從表面上看,它和第二次機會算法類似,都是訪問位為0就更換,反之則再給一次機會。但是,它和第二次機會算法還是有幾點不同:

  (1)他們的數據結構不一樣,第二次機會使用的是鏈表,時鍾算法使用的是索引(整數指針)。這樣,其使用的內存空間不一樣。

  (2)第二次機會需要使用額外的內存,而時鍾算法可以直接使用頁表。使用頁表的好處是無需額外的空間,更大的好處是頁面的訪問位會定期自動清零,這樣將使得時鍾算法的時間分辨粒度較第二次機會算法高,從而取得更好的頁面替換效果。

  時鍾算法的精髓是第二次機會,其缺點也就和第二次機會算法一樣:過於公平,沒有考慮到不同頁面調用頻率的不同,有可能換出不應該或不能換出的頁面,還可能造成無限循環。

PS:至此,隨機、FIFO、第二次機會與時鍾算法的介紹就到此結束,這四種算法都是屬於“公平算法”,即所有的頁面都或多或少地給予公平待遇,沒有頁面獲得特殊待遇。但是這種公平實現方式,會使效率受到一定影響,這時因為個體對於整個系統的貢獻沒有被區別對待,造成貢獻大的和貢獻小的待遇一樣,自然會影響整個系統的效率。

2.6 最優更換算法

  我們知道,最理想的頁面替換算法是選擇一個再也不會被訪問的頁面進行替換。如果不存在這樣的頁面,那至少選擇一個在隨后最長時間內不會被訪問的頁面進行替換。這樣,我們就可以保證在隨后發生缺頁中斷的次數最小或概率最低,這種算法就是最有替換算法。

  但是,我們沒法知道一個頁面隨后多長時間不會被訪問,因此最優更換算法在實際中沒法實現,那么為什么要介紹最有更換算法呢?這是為了定義一個標桿,以此來評判其他算法的優劣。

2.7 NRU(最近未被使用)算法

  顧名思義,NRU就是選擇一個在最近一段時間內沒有被訪問過的頁面進行替換,這是基於程序訪問的時空局域性。因為根據時空局域性原理,一個最近沒有被訪問的頁面,在隨后的時間里也不太可能被訪問,而NRU的實現方式就是利用頁面的訪問和修改位。

  每個頁面都有一個訪問位和一個修改位,凡是對頁面進行讀寫操作時,訪問位被設置為1。當進程對頁面進行讀寫操作時,修改位設置為1。根據這兩個位的狀態來對頁面進行分類的話,可以分成以下四種頁面類型:1、2、3、4。

  有了這個分類,NRU算法就按照這四類頁面的順序依次尋找可以替換的頁面。如果所有頁面皆被訪問和修改過,那也只能從中替換掉一個頁面,因此NRU算法總是會終結的。

  當然,這種分類比較籠統,在同一類頁面里,我們沒有辦法分辨出哪一類被訪問的時間更近一些。即在某些情況下,我們替換的可能並不是最近沒有被使用的頁面。

2.8 LRU(最近最少使用)算法

  與NRU算法相比,LRU算法不僅考慮最近是否用過,還要考慮最近使用的頻率。這里是基於過去的數據預測未來:如果一個頁面被訪問的頻率低,那么以后很可能也用不到。

  LRU算法的實現必須以某種方式記錄每個頁面被訪問的次數,這是個相當大的工作量。最簡單的方式就是在頁表的記錄項里增加一個計數域,一個頁面被訪問一次,這個計數器的值就增加1。於是,當需要更換頁面時,只需要找到計數域值最小的頁面替換即可,該頁面即是最近最少使用的頁面。另一種簡單實現方式就是用一個鏈表將所有頁面鏈接起來,最近被使用的頁面在鏈表頭,最近未被使用的放在鏈表尾。在每次頁面訪問時對這個鏈表進行更新,使其保持最近被使用的頁面在鏈表頭。

  LRU算法雖然很好,但是實現成本高(需要分辨出不同頁面中哪個頁面時最近最少使用的),並且時間代價大(每次頁面訪問發生時都需要更新記錄)。因此,一般的商業操作系統都沒有采納LRU頁面更新算法。

2.9 工作集算法

  由於不可能精確地確定那個頁面是最近最少使用的,那就干脆不花費這個力氣,只維持少量的信息使得我們選出的替換頁面不太可能是馬上又會使用的頁面即可。這種少量的信息就是工作集信息

  工作集概念來源於程序訪問的時空局限性,即在一段時間內,程序訪問的頁面將局限在一組頁面集合上。例如,最近k次訪問均發生在某m個頁面上,那么m就是參數為k時的工作集。我們用w(k,t)來表示在時間t時k次訪問所涉及的頁面數量。

  顯然,隨着k的增長,w(k,t)的值也隨之增長;但是當k增長到某個數值之后,w(k,t)的值將增長極其緩慢甚至接近停滯,並維持一段時間的穩定,如下圖所示:

  由上圖可以看出,如果一個程序在內存里面的頁面數與其工作集大小相等或者超過工作集,則該程序可在一段時間內不會發生缺頁中斷。如果其在內存的頁面數小於工作集,則發生缺頁中斷的頻率將增加,甚至發生內存抖動。

  因此,工作計算法的目標就是維持當前的工作集的頁面在物理內存里面。每次頁面更換時,尋找一個不屬於當前工作集的頁面替換即可。這樣,我們再尋找頁面時只需要將頁面分離為兩大類即可:當前工作集內頁面和當前工作集外頁面。如此,只要找到一個飛當前工作集的頁面,將其替換即可。

  工作集算法的優點:實現簡單,只需要在頁表的每個記錄增加一個虛擬時間域即可。而且,這個時間域不是每次發生訪問時都需要更新,而是在需要更換頁面時,頁面更換算法對其進行修改,因此時間成本也不大。

  工作集算法的缺點:每次掃描頁面進行替換時,有可能需要掃描整個頁表。然而,並不是所有頁面都內存里,因此掃描過程中的一大部分時間將是無用功。另外,由於其數據結構是線性的,會造成每次都按同樣的順序進行掃描,顯得不太公平。

2.10 工作集時鍾算法

  鑒於工作集算法的缺點,先驅們將工作集算法與時鍾算法結合起來,設計出了工作集時鍾算法,即使用工作集算法的原理,但是將頁面的掃描順序按照時鍾的形式組織起來。這樣每次需要替換頁面時,從指針指向的頁面開始掃描,從而達到更加公平的狀態。而且,按時鍾組織的頁面只是在內存里面的頁面,在內存外的頁面不放在時鍾圈里,從而提高實現效率

  鑒於其時間與空間上的優勢,工作集時鍾算法被大多商業操作系統所采納


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM