前文主要講了我們的程序是通過虛擬地址進行內存訪問的,那么操作系統是如何實現了虛擬地址到實際物理地址的轉換,又是如何對有限的物理內存進行管理,才能讓多個進程共同在有限的內存里跑起來的呢?總的來說,系統要做的工作包括:監控物理內存的使用情況、在程序需要更多內存時進行內存分配、把不同進程的地址空間映射到物理內存的不同區域、動態地把程序運行需要的資源移進內存或把暫時不需要的資源移出內存以騰出空間,接下來將對Linux是通過怎樣的機制完成這些工作做一個簡要的介紹。
1、分頁和頁表
首先,分頁的概念相信很多人都不陌生,我這里想說的是“分”的思想,學習計算機兩年多,我最大的感受是計算機就是在利用有限的資源干無限的事,而這很多時候都是基於“分而治之”的思想實現的。問題規模太大,太復雜怎么辦?就是要分解,分就意味着更簡單,更靈活,更容易處理,我這里並不只是指算法設計,而是指解決很多實際的復雜問題,就像現在很火的大數據處理,一台機器根本無法完成這么大量的存儲和計算工作,就是需要通過“分”,把數據分到多台機器上存,把計算任務也分到多台機器上完成,才有了問題解決的可能。好吧,扯遠了~其實我只是想說作為程序員一定要理解分治的思想。回到正題,我們已經知道每個進程都有4G的地址空間,但是程序在運行的時候並不需要程序中所有的內容,需要的只是當前正在執行的一部分和相關的數據就可以,因此可以將整個地址空間分成一個個連續的大小固定的塊(比如4KB),只要那些需要的塊在內存中即可,這一個個的塊就是頁(page),好處就是當程序運行或者停止時,系統不用把整個進程的內容移進移出,而只要移動一個個頁就可以,這樣既提高了效率又節省了空間,才使得多個進程能夠同時存在於內存中。注意,這里的頁是指虛擬地址空間里連續的一段,而物理內存中這樣連續的段則稱為page frame,它們的大小相同,進程的頁放進內存中時就放到一個個的page frame中。
有了page的概念后,我們再來看系統是如何實現邏輯地址到物理地址的轉換的。Linux為每個進程維護了一個page table,這個表里的每一條記錄表示一個page,保存了以下信息:
每條記錄中通過一些位標識了這個頁是否存在於實際內存中,允許什么方式的訪問,是否被修改過,是否正在被使用,是否進行緩存等信息,其中最重要的就是page frame number,系統就是由此得到的該頁在實際內存中的位置。在32位的機器上,一個邏輯地址有32位,它可以分成兩部分,一部分用於表示頁號,用於在page table中查找該page的記錄,從而得到page frame number,也就是這個page在物理內存的起始位置,還有一部分是頁內偏移量,這兩者相加就得到了我們實際要訪問的物理地址。通過這種方式系統可以實現邏輯地址到物理地址的轉換,但是又帶來一個問題,由於在程序運行期間需要對page table進行查找,也就是說page table也要存在於內存中,假設一個頁有4KB,對於32位的機器,page table中剛好有2^20(1M)條的記錄,似乎還可以接受,但是對於64位的機器,則需要2^52條記錄,如果光是存個page table就用掉這么多內存,那程序也不用跑了,為了解決這個問題,Linux使用了多級索引技術:
Linux不是直接把整個page table放到內存中,而是根據頁的大小將整個page table 又分成一個個小的page table,然后通過索引的方式來進行訪問。具體方式如上圖,虛擬地址被分成了5個部分,global|upper|middle directory, page和offset,首先根據global directory在global目錄里進行查找,global目錄里每一條記錄保存了一個指向下一級目錄的指針,在取得一個指針后,根據這個指針定位到一個下一級的upper目錄,然后根據upper directory 又可以在upper目錄里得到一個指針來得到一個middle目錄,而middle目錄里得到的指針才是指向真正的page table,此時再根據page域取得一條page table中的記錄。那為什么這樣能省內存?假設一個page目錄里有n條記錄,那么根據gloabl目錄可以索引到n個upper目錄,而每個upper目錄又可以索引到n個middle目錄,以此類推,原來一個大的page table分成了n^3個小的page table,通過三級目錄一級級往下索引就可以得到我們最終需要的那個page table,而需要放進內存的就只有在查找過程中需要的三個目錄和最終的1個page table即可,這樣當然就省了很多內存咯。
2、頁的回收
在系統運行期間,隨着越來越多的程序的啟動和運行,越來越多的物理內存會被占用,而系統必須保證有空閑的內存來維持正常的運行,Linux使用了一種叫PFRA(page frame reclaiming algorithm) 的算法將一些暫時沒用的page frame釋放來騰出空間,的目標就是從內存中選出要釋放的page frame。首先linux將所有的page frame分為四類:unreclaimable, swappable, syncable, discardable。unreclaimable表示該頁的內容不能被取出,即新的page不能放到這里;swappable表示該頁的內容可以被取出內存但需要寫到磁盤,而syncable也表示可以取出內存但是只有當這個頁的內容被標記為已修改過的才需要寫會磁盤,discardable則表示該頁的內容可以直接被覆蓋而不需要保存到磁盤。這四個類代表了需要完成頁的置換操作的難易程度,在發生頁的置換時,系統會優先選擇容易完成頁進行置換。
linux在啟動的過程中會開啟一個后台進程,這個進程在系統運行過程中每隔一段時間就會對內存的使用情況進行檢查,如果它判斷當前的內存已經快要不過用了,就會開始運行PFRA算法。
linux將內存中所有page frame組織成兩個鏈表,active list和inactive list,這兩個鏈表也叫做LRU list,active list里的頁是最近被訪問過的,而inactive里的則是最近沒被問的,所以系統可以從inactive list里選擇page frame釋放。如上圖所示,每個page有2個標識位,編碼了4個狀態,這四個狀態之間可以進行轉換,從而導致了一個page會在active list和inactive list之間移動。觀察這個圖可以發現,當PG_active為0時,頁在inactive list上,當PG_active為1時,頁在active list上。對於一個在inactive list的頁,如果一開始處於狀態1,么該頁被訪問過一次后,它的referenced標識位會變為1,變成狀態2,而再被訪問一次后,它的active就變成1,referenced變為0(狀態3),這樣才從inactive 變成了 active,而如果在狀態2的時候,在經過一段特定的時間后還沒有再被訪問一次,則它又會自動回到狀態1,也就是說一個頁要從inactive變成active,中間需要經過一個中間狀態。為什么需要這個中間狀態呢,主要是考慮到下面這種情況,有些程序可能會周期性的訪問一個內存頁,比如說1小時訪問一次,那么在這1小時內,它是不需要再被訪問到的,如果沒有這個中間狀態的話,它會直接變成active,所以會一直保存在內存中,而有了這個中間狀態,如果這個也在一定時間內沒有再被訪問一次,它就仍然是inactive的,也就可以被取出內存。當然,有些時候系統可能會急需更多的內存,所以即使是存在active list里的頁,有時候也需要被取出內存,圖中的refill箭頭就表示了直接從active到inactive的狀態轉換。linux系統通過維護這些狀態變換,就可以選擇出合適的頁來將其取出內存,從而盡量減少發生page fault的機會。