windows 內存管理方式主要分爲:頁式管理,段式管理,段頁式管理。html
頁式管理的基本原理是將各進程的虛擬空間劃分爲若干個長度相等的頁;頁式管理把內存空間按照頁的大小劃分紅片或者頁面,而後把頁式虛擬地址與內存地址創建一一對應的頁表;並用相應的硬件地址變換機構來解決離散地址變換問題。頁式管理採用請求調頁或預調頁技術來實現內外存存儲器的統一管理。其優勢是沒有外碎片,每一個內碎片不超過頁的大小。缺點是,程序所有裝入內存,要求有相應的硬件支持。例如地址變換機構缺頁中斷的產生和選擇淘汰頁面等都要求有相應的硬件支持。這增長了機器成本,增長了系統開銷。node
段式管理的基本思想是把程序按照內容或過程函數關係分段,每段都有本身的名字。一個用戶做業或進程所包括的段對應一個二維線形虛擬空間,也就是一個二維虛擬存儲器。段式管理程序以段爲單位分配內存,而後經過地址映射機構把段式虛擬地址轉換爲實際內存物理地址。其優勢是能夠分別編寫和編譯,能夠針對不一樣類型的段採用不一樣的保護,能夠按段爲單位來進行共享,包括經過動態連接進行代碼共享。缺點是會產生碎片。linux
段頁式管理:爲了實現段頁式管理,系統必須爲每一個做業或進程創建一張段表以管理內存分配與釋放、缺段處理等。另外因爲一個段又被劃分紅了若干個頁。每一個段必須創建一張頁表以把段中的虛頁變換成內存中的實際頁面。顯然與頁式管理時相同,頁表中也要有相應的實現缺頁中斷處理和頁面保護等功能的表項。段頁式管理的段式管理與頁式管理方案結合而成的因此具備他們二者的優勢。但反過來講,因爲管理軟件的增長,複雜性和開銷也就隨之增長了。另外須要的硬件以及佔用的內存也有所增長。使得速度降下來。算法
轉載:http://www.cnblogs.com/zhaoyl/p/3695517.htmlwindows
本文以32位機器爲準,串講一些內存管理的知識點。數組
1. 虛擬地址、物理地址、邏輯地址、線性地址緩存
虛擬地址又叫線性地址。linux沒有采用分段機制,因此邏輯地址和虛擬地址(線性地址)(在用戶態,內核態邏輯地址專指下文說的線性偏移前的地址)是一個概念。物理地址自沒必要提。內核的虛擬地址和物理地址,大部分只差一個線性偏移量。用戶空間的虛擬地址和物理地址則採用了多級頁表進行映射,但仍稱之爲線性地址。安全
2. DMA/HIGH_MEM/NROMAL 分區架構
在x86結構中,Linux內核虛擬地址空間劃分0~3G爲用戶空間,3~4G爲內核空間(注意,內核可使用的線性地址只有1G)。內核虛擬空間(3G~4G)又劃分爲三種類型的區:
ZONE_DMA 3G以後起始的16MB
ZONE_NORMAL 16MB~896MB
ZONE_HIGHMEM 896MB ~1Gapp
因爲內核的虛擬和物理地址只差一個偏移量:物理地址 = 邏輯地址 – 0xC0000000。因此若是1G內核空間徹底用來線性映射,顯然物理內存也只能訪問到1G區間,這顯然是不合理的。HIGHMEM就是爲了解決這個問題,專門開闢的一塊沒必要線性映射,能夠靈活定製映射,以便訪問1G以上物理內存的區域。從網上扣來一圖,
高端內存的劃分,又以下圖,
內核直接映射空間 PAGE_OFFSET~VMALLOC_START,kmalloc和__get_free_page()分配的是這裏的頁面。兩者是藉助slab分配器,直接分配物理頁再轉換爲邏輯地址(物理地址連續)。適合分配小段內存。此區域 包含了內核鏡像、物理頁框表mem_map等資源。
內核動態映射空間 VMALLOC_START~VMALLOC_END,被vmalloc用到,可表示的空間大。
內核永久映射空間 PKMAP_BASE ~ FIXADDR_START,kmap
內核臨時映射空間 FIXADDR_START~FIXADDR_TOP,kmap_atomic
3.夥伴算法和slab分配器
夥伴Buddy算法解決了外部碎片問題.內核在每一個zone區管理着可用的頁面,按2的冪級(order)大小排成鏈表隊列,存放在free_area數組。
具體buddy管理基於位圖,其分配回收頁面的算法描述以下,
buddy算法舉例描述:
假設咱們的系統內存只有16個頁面RAM。由於RAM只有16個頁面,咱們只需用四個級別(orders)的夥伴位圖(由於最大連續內存大小爲16個頁面),以下圖所示。
order(0)bimap有8個bit位(頁面最多16個頁面,因此16/2)
order(1)bimap有4個bit位(order(0)bimap有8個bit位,因此8/2);
也就是order(1)第一塊由兩個頁框page1 與page2組成與order(1)第2塊由兩個頁框page3 與page4組成,這兩個塊之間有一個bit位
order(2)bimap有2個bit位(order(1)bimap有4個bit位,因此4/2)
order(3)bimap有1個bit位(order(2)bimap有4個bit位,因此2/2)
在order(0),第一個bit表示開始的2個頁面,第二個bit表示接下來的2個頁面,以此類推。由於頁面4已分配,而頁面5空閒,故第三個bit爲1。
一樣在order(1)中,bit3是1的緣由是一個夥伴徹底空閒(頁面8和9),和它對應的夥伴(頁面10和11)卻並不是如此,故之後回收頁面時,能夠合併。
分配過程
當咱們須要order(1)的空閒頁面塊時,則執行如下步驟:
一、初始空閒鏈表爲:
order(0): 5, 10
order(1): 8 [8,9]
order(2): 12 [12,13,14,15]
order(3):
二、從上面空閒鏈表中,咱們能夠看出,order(1)鏈表上,有一個空閒的頁面塊,把它分配給用戶,並從該鏈表中刪除。
三、當咱們再須要一個order(1)的塊時,一樣咱們從order(1)空閒鏈表上開始掃描。
四、若在order(1)上沒有空閒頁面塊,那麼咱們就到更高的級別(order)上找,order(2)。
五、此時(order(1)上沒有空閒頁面塊)有一個空閒頁面塊,該塊是從頁面12開始。該頁面塊被分割成兩個稍微小一些order(1)的頁面塊,[12,13]和[14,15]。[14,15]頁面塊加到order(1)空閒鏈表中,同時[12,13]頁面塊返回給用戶。
六、最終空閒鏈表爲:
order(0): 5, 10
order(1): 14 [14,15]
order(2):
order(3):
回收過程
當咱們回收頁面11(order 0)時,則執行如下步驟:
1、找到在order(0)夥伴位圖中表明頁面11的位,計算使用下面公示:
index = page_idx >> (order + 1)
= 11 >> (0 + 1)
= 5
二、檢查上面一步計算位圖中相應bit的值。若該bit值爲1,則和咱們臨近的,有一個空閒夥伴。Bit5的值爲1(注意是從bit0開始的,Bit5即爲第6bit),由於它的夥伴頁面10是空閒的。
三、如今咱們從新設置該bit的值爲0,由於此時兩個夥伴(頁面10和頁面11)徹底空閒。
四、咱們將頁面10,從order(0)空閒鏈表中摘除。
五、此時,咱們對2個空閒頁面(頁面10和11,order(1))進行進一步操做。
六、新的空閒頁面是從頁面10開始的,因而咱們在order(1)的夥伴位圖中找到它的索引,看是否有空閒的夥伴,以進一步進行合併操做。使用第一步中的計算公司,咱們獲得bit 2(第3位)。
七、Bit 2(order(1)位圖)一樣也是1,由於它的夥伴頁面塊(頁面8和9)是空閒的。
八、從新設置bit2(order(1)位圖)的值,而後在order(1)鏈表中刪除該空閒頁面塊。
九、如今咱們合併成了4頁面大小(從頁面8開始)的空閒塊,從而進入另外的級別。在order(2)中找到夥伴位圖對應的bit值,是bit1,且值爲1,需進一步合併(緣由同上)。
十、從oder(2)鏈表中摘除空閒頁面塊(從頁面12開始),進而將該頁面塊和前面合併獲得的頁面塊進一步合併。如今咱們獲得從頁面8開始,大小爲8個頁面的空閒頁面塊。
十一、咱們進入另一個級別,order(3)。它的位索引爲0,它的值一樣爲0。這意味着對應的夥伴不是所有空閒的,因此沒有再進一步合併的可能。咱們僅設置該bit爲1,而後將合併獲得的空閒頁面塊放入order(3)空閒鏈表中。
十二、最終咱們獲得大小爲8個頁面的空閒塊,
buddy避免內部碎片的努力
物理內存的碎片化一直是Linux操做系統的弱點之一,儘管已經有人提出了不少解決方法,可是沒有哪一個方法可以完全的解決,memory buddy分配就是解決方法之一。 咱們知道磁盤文件也有碎片化問題,可是磁盤文件的碎片化只會減慢系統的讀寫速度,並不會致使功能性錯誤,並且咱們還能夠在不影響磁盤功能的前提的下,進行磁盤碎片整理。而物理內存碎片則大相徑庭,物理內存和操做系統結合的太過於緊密,以致於咱們很難在運行時,進行物理內存的搬移(這一點上,磁盤碎片要容易的多;實際上mel gorman已經提交了內存緊縮的patch,只是尚未被主線內核接收)。 所以解決的方向主要放在預防碎片上。在2.6.24內核開發期間,防止碎片的內核功能加入了主線內核。在瞭解反碎片的基本原理前,先對內存頁面作個歸類:
1. 不可移動頁面 unmoveable:在內存中位置必須固定,沒法移動到其餘地方,核心內核分配的大部分頁面都屬於這一類。
2. 可回收頁面 reclaimable:不能直接移動,可是能夠回收,由於還能夠從某些源重建頁面,好比映射文件的數據屬於這種類別,kswapd會按照必定的規則,週期性的回收這類頁面。
3. 可移動頁面 movable:能夠隨意的移動。屬於用戶空間應用程序的頁屬於此類頁面,它們是經過頁表映射的,所以咱們只須要更新頁表項,並把數據複製到新位置就能夠了,固然要注意,一個頁面可能被多個進程共享,對應着多個頁表項。
防止碎片的方法就是把這三類page放在不一樣的鏈表上,避免不一樣類型頁面相互干擾。考慮這樣的情形,一個不可移動的頁面位於可移動頁面中間,那麼咱們移動或者回收這些頁面後,這個不可移動的頁面阻礙着咱們得到更大的連續物理空閒空間。
另外,每一個zone區都有一個本身的失活淨頁面隊列,與此對應的是兩個跨zone的全局隊列,失活髒頁隊列 和 活躍隊列。這些隊列都是經過page結構的lru指針鏈入的。
思考:失活隊列的意義是什麼(見<linux內核源代碼情景分析>)?
slab分配器:解決內部碎片問題
4.頁面回收/側重機制
關於頁面的使用
在以前的一些文章中,咱們瞭解到linux內核會在不少狀況下分配頁面。
一、內核代碼可能調用alloc_pages之類的函數,從管理物理頁面的夥伴系統(管理區zone上的free_area空閒鏈表)上直接分配頁面(見《linux內核內存管理淺析》)。好比:驅動程序可能用這種方式來分配緩存;建立進程時,內核也是經過這種方式分配連續的兩個頁面,做爲進程的thread_info結構和內核棧;等等。從夥伴系統分配頁面是最基本的頁面分配方式,其餘的內存分配都是基於這種方式的;
二、內核中的不少對象都是用slab機制來管理的(見《linux slub分配器淺析》)。slab就至關於對象池,它將頁面「格式化」成「對象」,存放在池中供人使用。當slab中的對象不足時,slab機制會自動從夥伴系統中分配頁面,並「格式化」成新的對象;
三、磁盤高速緩存(見《linux內核文件讀寫淺析》)。讀寫文件時,頁面被從夥伴系統分配並用於磁盤高速緩存,而後磁盤上的文件數據被載入到對應的磁盤高速緩存頁面中;
四、內存映射。這裏所謂的內存映射其實是指將內存頁面映射到用戶空間,供用戶進程使用。進程的task_struct->mm結構中的每個vma就表明着一個映射,而映射的真正實現則是在用戶程序訪問到對應的內存地址以後,由缺頁異常引發的頁面被分配和頁表被更新(見《linux內核內存管理淺析》);
頁面回收簡述
有頁面分配,就會有頁面回收。頁面回收的方法大致上可分爲兩種:
一是主動釋放。就像用戶程序經過free函數釋放曾經經過malloc函數分配的內存同樣,頁面的使用者明確知道頁面何時要被使用,何時又再也不須要了。
上面提到的前兩種分配方式,通常都是由內核程序主動釋放的。對於直接從夥伴系統分配的頁面,這是由使用者使用free_pages之類的函數主動釋放的,頁面釋放後被直接放歸夥伴系統;從slab中分配的對象(使用kmem_cache_alloc函數),也是由使用者主動釋放的(使用kmem_cache_free函數)。
另外一種頁面回收方式是經過linux內核提供的頁框回收算法(PFRA)進行回收。頁面的使用者通常將頁面看成某種緩存,以提升系統的運行效率。緩存一直存在當然好,可是若是緩存沒有了也不會形成什麼錯誤,僅僅是效率受影響而已。頁面的使用者不明確知道這些緩存頁面何時最好被保留,何時最好被回收,這些都交由PFRA來關心。
簡單來講,PFRA要作的事就是回收這些能夠被回收的頁面。爲了不繫統陷入頁面緊缺的困境,PFRA會在內核線程中週期性地被調用運行。或者因爲系統已經頁面緊缺,試圖分配頁面的內核執行流程由於得不到須要的頁面,而同步地調用PFRA。
上面提到的後兩種分配方式,通常是由PFRA來進行回收的(或者由相似刪除文件、進程退出、這樣的過程來同步回收)。
PFRA回收通常頁面
而對於上面提到的前兩種頁面分配方式(直接分配頁面和經過slab分配對象),也有可能須要經過PFRA來回收。
頁面的使用者能夠向PFRA註冊回調函數(使用register_shrink函數)。而後由PFRA在適當的時機來調用這些回調函數,以觸發對相應頁面或對象的回收。
其中較爲典型的是對dentry的回收。dentry是由slab分配的,用於表示虛擬文件系統目錄結構的對象。在dentry的引用記數被減爲0的時候,dentry並非直接被釋放,而是被放到一個LRU鏈表中緩存起來,便於後續的使用。(見《linux內核虛擬文件系統淺析》。)
而這個LRU鏈表中的dentry最終是須要被回收的,因而虛擬文件系統在初始化時,調用register_shrinker註冊了回收函數shrink_dcache_memory。
系統中全部文件系統的超級塊對象被存放在一個鏈表中,shrink_dcache_memory函數掃描這個鏈表,獲取每一個超級塊的未被使用dentry的LRU,而後從中回收一些最老的dentry。隨着dentry的釋放,對應的inode將被減引用,也可能引發inode被釋放。
inode被釋放後也是放在一個未使用鏈表中,虛擬文件系統在初始化時還調用register_shrinker註冊了回調函數shrink_icache_memory,用來回收這些未使用的inode,從而inode中關聯的磁盤高速緩存也將被釋放。
另外,隨着系統的運行,slab中可能會存在不少的空閒對象(好比在對某一對象的使用高峯事後)。PFRA中的cache_reap函數就用於回收這些多餘的空閒對象,若是某些空閒的對象正好可以還原成一個頁面,則這個頁面能夠被釋放回夥伴系統;
cache_reap函數要作的事情提及來很簡單。系統中全部存放對象池的kmem_cache結構連成一個鏈表,cache_reap函數掃描其中的每個對象池,而後尋找能夠回收的頁面,並將其回收。(固然,實際的過程要更復雜一點。)
關於內存映射
前面說到,磁盤高速緩存和內存映射通常由PFRA來進行回收。PFRA對這二者的回收是很相似的,實際上,磁盤高速緩存極可能就被映射到了用戶空間。下面簡單對內存映射作一些介紹:
內存映射分爲文件映射和匿名映射。
文件映射是指表明這個映射的vma對應到一個文件中的某個區域。這種映射方式相對較少被用戶態程序顯式地使用,用戶態程序通常習慣於open一個文件、而後read/write去讀寫文件。
而實際上,用戶程序也可使用mmap系統調用將一個文件的某個部分映射到內存上(對應到一個vma),而後以訪存的方式去讀寫文件。儘管用戶程序較少這樣使用,可是用戶進程中卻充斥着這樣的映射:進程正在執行的可執行代碼(包括可執行文件、lib庫文件)就是以這樣的方式被映射的。
在《linux內核文件讀寫淺析》一文中,咱們並無討論關於文件映射的實現。實際上,文件映射是將文件的磁盤高速緩存中的頁面直接映射到了用戶空間(可見,文件映射的頁面是磁盤高速緩存頁面的子集),用戶能夠0拷貝地對其進行讀寫。而使用read/write的話,則會在用戶空間的內存和磁盤高速緩存間發生一次拷貝。
匿名映射相對於文件映射,表明這個映射的vma沒有對應到文件。對於用戶空間普通的內存分配(堆空間、棧空間),都屬於匿名映射。
顯然,多個進程可能經過各自的文件映射來映射到同一個文件上(好比大多數進程都映射了libc庫的so文件);那匿名映射呢?實際上,多個進程也可能經過各自的匿名映射來映射到同一段物理內存上,這種狀況是因爲fork以後父子進程共享原來的物理內存(copy-on-write)而引發的。
文件映射又分爲共享映射和私有映射。私有映射時,若是進程對映射的地址空間進行寫操做,則映射對應的磁盤高速緩存並不會直接被寫。而是將原有內容複製一份,而後再寫這個複製品,而且當前進程的對應頁面映射將切換到這個複製品上去(寫時複製)。也就是說,寫操做是隻有本身可見的。而對於共享映射,寫操做則會影響到磁盤高速緩存,是你們均可見的。
哪些頁面該回收
至於回收,磁盤高速緩存的頁面(包括文件映射的頁面)都是能夠被丟棄並回收的。可是若是頁面是髒頁面,則丟棄以前必須將其寫回磁盤。
而匿名映射的頁面則都是不能夠丟棄的,由於頁面裏面存有用戶程序正在使用的數據,丟棄以後數據就無法還原了。相比之下,磁盤高速緩存頁面中的數據自己是保存在磁盤上的,能夠復現。
因而,要想回收匿名映射的頁面,只好先把頁面上的數據轉儲到磁盤,這就是頁面交換(swap)。顯然,頁面交換的代價相對更高一些。
匿名映射的頁面能夠被交換到磁盤上的交換文件或交換分區上(分區便是設備,設備即也是文件。因此下文統稱爲交換文件)。
因而,除非頁面被保留或被上鎖(頁面標記PG_reserved/PG_locked被置位。某些狀況下,內核須要暫時性地將頁面保留,避免被回收),全部的磁盤高速緩存頁面均可回收,全部的匿名映射頁面均可交換。
儘管能夠回收的頁面不少,可是顯然PFRA應當儘量少地去回收/交換(由於這些頁面要從磁盤恢復,須要很大的代價)。因此,PFRA僅當必要時纔回收/交換一部分不多被使用的頁面,每次回收的頁面數是一個經驗值:32。
因而,全部這些磁盤高速緩存頁面和匿名映射頁面都被放到了一組LRU裏面。(實際上,每一個zone就有一組這樣的LRU,頁面都被放到本身對應的zone的LRU中。)
一組LRU由幾對鏈表組成,有磁盤高速緩存頁面(包括文件映射頁面)的鏈表、匿名映射頁面的鏈表、等。一對鏈表其實是active和inactive兩個鏈表,前者是最近使用過的頁面、後者是最近未使用的頁面。
進行頁面回收的時候,PFRA要作兩件事情,一是將active鏈表中最近最少使用的頁面移動到inactive鏈表、二是嘗試將inactive鏈表中最近最少使用的頁面回收。
肯定最近最少使用
如今就有一個問題了,怎麼肯定active/inactive鏈表中哪些頁面是最近最少使用的呢?
一種方法是排序,當頁面被訪問時,將其移動到鏈表的尾部(假設回收從頭部開始)。可是這就意味着頁面在鏈表中的位置可能頻繁移動,而且移動以前還必須先上鎖(可能有多個CPU在同時訪問),這樣作對效率影響很大。
linux內核採用的是標記加順序的辦法。當頁面在active和inactive兩個鏈表之間移動時,老是將其放到鏈表的尾部(同上,假設回收從頭部開始)。
頁面沒有在鏈表間移動時,並不會調整它們的順序。而是經過訪問標記來表示頁面是否剛被訪問過。若是inactive鏈表中已設置訪問標記的頁面再被訪問,則將其移動到active鏈表中,而且清除訪問標記。(實際上,爲了不訪問衝突,頁面並不會直接從inactive鏈表移動到active鏈表,而是有一個pagevec中間結構用做緩衝,以免鎖鏈表。)
頁面的訪問標記有兩種狀況,一是放在page->flags中的PG_referenced標記,在頁面被訪問時該標記置位。對於磁盤高速緩存中(未被映射)的頁面,用戶進程經過read、write之類的系統調用去訪問它們,系統調用代碼中會將對應頁面的PG_referenced標記置位。
而對於內存映射的頁面,用戶進程能夠直接訪問它們(不通過內核),因此這種狀況下的訪問標記不是由內核來設置的,而是由mmu。在將虛擬地址映射成物理地址後,mmu會在對應的頁表項上置一個accessed標誌位,表示頁面被訪問。(一樣的道理,mmu會在被寫的頁面所對應的頁表項上置一個dirty標誌,表示頁面是髒頁面。)
頁面的訪問標記(包括上面兩種標記)將在PFRA處理頁面回收的過程當中被清除,由於訪問標記顯然是應該有有效期的,而PFRA的運行週期就表明這個有效期。page->flags中的PG_referenced標記能夠直接清除,而頁表項中的accessed位則須要經過頁面找到其對應的頁表項後才能清除(見下文的「反向映射」)。
那麼,回收過程又是怎樣掃描LRU鏈表的呢?
因爲存在多組LRU(系統中有多個zone,每一個zone又有多組LRU),若是PFRA每次回收都掃描全部的LRU找出其中最值得回收的若干個頁面的話,回收算法的效率顯然不夠理想。
linux內核PFRA使用的掃描方法是:定義一個掃描優先級,經過這個優先級換算出在每一個LRU上應該掃描的頁面數。整個回收算法以最低的優先級開始,先掃描每一個LRU中最近最少使用的幾個頁面,而後試圖回收它們。若是一遍掃描下來,已經回收了足夠數量的頁面,則本次回收過程結束。不然,增大優先級,再從新掃描,直到足夠數量的頁面被回收。而若是始終不能回收足夠數量的頁面,則優先級將增長到最大,也就是全部頁面將被掃描。這時,就算回收的頁面數量仍是不足,回收過程都會結束。
每次掃描一個LRU時,都從active鏈表和inactive鏈表獲取當前優先級對應數目的頁面,而後再對這些頁面作處理:若是頁面不能被回收(如被保留或被上鎖),則放回對應鏈表頭部(同上,假設回收從頭部開始);不然若是頁面的訪問標記置位,則清除該標記,並將頁面放回對應鏈表尾部(同上,假設回收從頭部開始);不然頁面將從active鏈表被移動到inactive鏈表、或從inactive鏈表被回收。
被掃描到的頁面根據訪問標記是否置位來決定其去留。那麼這個訪問標記是如何設置的呢?有兩個途徑,一是用戶經過read/write之類的系統調用訪問文件時,內核操做磁盤高速緩存中的頁面,會設置這些頁面的訪問標記(設置在page結構中);二是進程直接訪問已映射的頁面時,mmu會自動給對應的頁表項加上訪問標記(設置在頁表的pte中)。關於訪問標記的判斷就基於這兩個信息。(給定一個頁面,可能有多個pte引用到它。如何知道這些pte是否被設置了訪問標記呢?那就須要經過反向映射找到這些pte。下面會講到。)
PFRA不傾向於從active鏈表回收匿名映射的頁面,由於用戶進程使用的內存通常相對較少,且回收的話須要進行交換,代價較大。因此在內存剩餘較多、匿名映射所佔比例較少的狀況下,都不會去回收匿名映射對應的active鏈表中的頁面。(而若是頁面已經被放到inactive鏈表中,就再也不去管那麼多了。)
反向映射
像這樣,在PFRA處理頁面回收的過程當中,LRU的inactive鏈表中的某些頁面可能就要被回收了。
若是頁面沒有被映射,直接回收到夥伴系統便可(對於髒頁,先寫回、再回收)。不然,還有一件麻煩的事情要處理。由於用戶進程的某個頁表項正引用着這個頁面呢,在回收頁面以前,還必須給引用它的頁表項一個交待。
因而,問題就來了,內核怎麼知道這個頁面被哪些頁表項所引用呢?爲了作到這一點,內核創建了從頁面到頁表項的反向映射。
經過反向映射能夠找到一個被映射的頁面對應的vma,經過vma->vm_mm->pgd就能找到對應的頁表。而後經過page->index獲得頁面的虛擬地址。再經過虛擬地址從頁表中找到對應的頁表項。(前面說到的獲取頁表項中的accessed標記,就是經過反向映射實現的。)
頁面對應的page結構中,page->mapping若是最低位置位,則這是一個匿名映射頁面,page->mapping指向一個anon_vma結構;不然是文件映射頁面,page->mapping文件對應的address_space結構。(顯然,anon_vma結構和address_space結構在分配時,地址必需要對齊,至少保證最低位爲0。)
對於匿名映射的頁面,anon_vma結構做爲一個鏈表頭,將映射這個頁面的全部vma經過vma->anon_vma_node鏈表指針鏈接起來。每當一個頁面被(匿名)映射到一個用戶空間時,對應的vma就被加入這個鏈表。
對於文件映射的頁面,address_space結構除了維護了一棵用於存放磁盤高速緩存頁面的radix樹,還爲該文件映射到的全部vma維護了一棵優先搜索樹。由於這些被文件映射到的vma並不必定都是映射整個文件,極可能只映射了文件的一部分。因此,這棵優先搜索樹除了索引到全部被映射的vma,還要能知道文件的哪些區域是映射到哪些vma上的。每當一個頁面被(文件)映射到一個用戶空間時,對應的vma就被加入這個優先搜索樹。因而,給定磁盤高速緩存上的一個頁面,就能經過page->index獲得頁面在文件中的位置,就能經過優先搜索樹找出這個頁面映射到的全部vma。
上面兩步中,神奇的page->index作了兩件事,獲得頁面的虛擬地址、獲得頁面在文件磁盤高速緩存中的位置。
vma->vm_start記錄了vma的首虛擬地址,vma->vm_pgoff記錄了該vma在對應的映射文件(或共享內存)中的偏移,而page->index記錄了頁面在文件(或共享內存)中的偏移。
經過vma->vm_pgoff和page->index能獲得頁面在vma中的偏移,加上vma->vm_start就能獲得頁面的虛擬地址;而經過page->index就能獲得頁面在文件磁盤高速緩存中的位置。
頁面換入換出
在找到了引用待回收頁面的頁表項後,對於文件映射,能夠直接把引用該頁面的頁表項清空。等用戶再訪問這個地址的時候觸發缺頁異常,異常處理代碼再從新分配一個頁面,並去磁盤裏面把對應的數據讀出來就好了(說不定,頁面在對應的磁盤高速緩存裏面已經有了,由於其餘進程先訪問過)。這就跟頁面映射之後,第一次被訪問的情形同樣;
對於匿名映射,先將頁面寫回到交換文件,而後還得在頁表項中記錄該頁面在交換文件中的index。
頁表項中有一個present位,若是該位被清除,則mmu認爲頁表項無效。在頁表項無效的狀況下,其餘位不被mmu關心,能夠用來存儲其餘信息。這裏就用它們來存儲頁面在交換文件中的index了(其實是交換文件號+交換文件內的索引號)。
將匿名映射的頁面交換到交換文件的過程(換出過程)與將磁盤高速緩存中的髒頁寫回文件的過程很類似。
交換文件也有其對應的address_space結構,匿名映射的頁面在換出時先被放到這個address_space對應磁盤高速緩存中,而後跟髒頁寫回同樣,被寫回到交換文件中。寫回完成後,這個頁面才被釋放(記住,咱們的目的是要釋放這個頁面)。
那麼爲何不直接把頁面寫回到交換文件,而要通過磁盤高速緩存呢?由於,這個頁面可能被映射了屢次,不可能一次性把全部用戶進程的頁表中對應的頁表項都修改好(修改爲頁面在交換文件中的索引),因此在頁面被釋放的過程當中,頁面被暫時放在磁盤高速緩存上。
而並非全部頁表項的修改過程都是能成功的(好比在修改以前頁面又被訪問了,因而如今又不須要回收這個頁面了),因此頁面放到磁盤高速緩存的時間也可能會很長。
一樣,將匿名映射的頁面從交換文件讀出的過程(換入過程)也與將文件數據讀出的過程很類似。
先去對應的磁盤高速緩存上看看頁面在不在,不在的話再去交換文件裏面讀。文件裏的數據也是被讀到磁盤高速緩存中的,而後用戶進程的頁表中對應的頁表項將被改寫,直接指向這個頁面。
這個頁面可能不會立刻從磁盤高速緩存中拿下來,由於若是還有其餘用戶進程也映射到這個頁面(它們的對應頁表項已經被修改爲了交換文件的索引),他們也能夠引用到這裏。直到沒有其餘的頁表項再引用這個交換文件索引時,頁面才能夠從磁盤高速緩存中被取下來。
最後的必殺
前面說到,PFRA可能掃描了全部的LRU還沒辦法回收須要的頁面。一樣,在slab、dentry cache、inode cache、等地方,可能也沒法回收到頁面。
這時,若是某段內核代碼必定要得到頁面呢(沒有頁面,系統可能就要崩潰了)?PFRA只好使出最後的必殺技——OOM(out of memory)。所謂的OOM就是尋找一個最不重要的進程,而後將其殺死。經過釋放這個進程所佔有的內存頁面,以緩解系統壓力。
5.內存管理架構
針對上圖,說幾句,
[地址映射](圖:左中)
linux內核使用頁式內存管理,應用程序給出的內存地址是虛擬地址,它須要通過若干級頁表一級一級的變換,才變成真正的物理地址。
想一下,地址映射仍是一件很恐怖的事情。當訪問一個由虛擬地址表示的內存空間時,須要先通過若干次的內存訪問,獲得每一級頁表中用於轉換的頁表項(頁表是存放在內存裏面的),才能完成映射。也就是說,要實現一次內存訪問,實際上內存被訪問了N+1次(N=頁表級數),而且還須要作N次加法運算。
因此,地址映射必需要有硬件支持,mmu(內存管理單元)就是這個硬件。而且須要有cache來保存頁表,這個cache就是TLB(Translation lookaside buffer)。
儘管如此,地址映射仍是有着不小的開銷。假設cache的訪存速度是內存的10倍,命中率是40%,頁表有三級,那麼平均一次虛擬地址訪問大概就消耗了兩次物理內存訪問的時間。
因而,一些嵌入式硬件上可能會放棄使用mmu,這樣的硬件可以運行VxWorks(一個很高效的嵌入式實時操做系統)、linux(linux也有禁用mmu的編譯選項)、等系統。
可是使用mmu的優點也是很大的,最主要的是出於安全性考慮。各個進程都是相互獨立的虛擬地址空間,互不干擾。而放棄地址映射以後,全部程序將運行在同一個地址空間。因而,在沒有mmu的機器上,一個進程越界訪存,可能引發其餘進程莫名其妙的錯誤,甚至致使內核崩潰。
在地址映射這個問題上,內核只提供頁表,實際的轉換是由硬件去完成的。那麼內核如何生成這些頁表呢?這就有兩方面的內容,虛擬地址空間的管理和物理內存的管理。(實際上只有用戶態的地址映射才須要管理,內核態的地址映射是寫死的。)
[虛擬地址管理](圖:左下)
每一個進程對應一個task結構,它指向一個mm結構,這就是該進程的內存管理器。(對於線程來講,每一個線程也都有一個task結構,可是它們都指向同一個mm,因此地址空間是共享的。)
mm->pgd指向容納頁表的內存,每一個進程有自已的mm,每一個mm有本身的頁表。因而,進程調度時,頁表被切換(通常會有一個CPU寄存器來保存頁表的地址,好比X86下的CR3,頁表切換就是改變該寄存器的值)。因此,各個進程的地址空間互不影響(由於頁表都不同了,固然沒法訪問到別人的地址空間上。可是共享內存除外,這是故意讓不一樣的頁表可以訪問到相同的物理地址上)。
用戶程序對內存的操做(分配、回收、映射、等)都是對mm的操做,具體來講是對mm上的vma(虛擬內存空間)的操做。這些vma表明着進程空間的各個區域,好比堆、棧、代碼區、數據區、各類映射區、等等。
用戶程序對內存的操做並不會直接影響到頁表,更不會直接影響到物理內存的分配。好比malloc成功,僅僅是改變了某個vma,頁表不會變,物理內存的分配也不會變。
假設用戶分配了內存,而後訪問這塊內存。因爲頁表裏面並無記錄相關的映射,CPU產生一次缺頁異常。內核捕捉異常,檢查產生異常的地址是否是存在於一個合法的vma中。若是不是,則給進程一個"段錯誤",讓其崩潰;若是是,則分配一個物理頁,併爲之創建映射。
[物理內存管理](圖:右上)
那麼物理內存是如何分配的呢?
首先,linux支持NUMA(非均質存儲結構),物理內存管理的第一個層次就是介質的管理。pg_data_t結構就描述了介質。通常而言,咱們的內存管理介質只有內存,而且它是均勻的,因此能夠簡單地認爲系統中只有一個pg_data_t對象。
每一種介質下面有若干個zone。通常是三個,DMA、NORMAL和HIGH。
DMA:由於有些硬件系統的DMA總線比系統總線窄,因此只有一部分地址空間可以用做DMA,這部分地址被管理在DMA區域(這屬因而高級貨了);
HIGH:高端內存。在32位系統中,地址空間是4G,其中內核規定3~4G的範圍是內核空間,0~3G是用戶空間(每一個用戶進程都有這麼大的虛擬空間)(圖:中下)。前面提到過內核的地址映射是寫死的,就是指這3~4G的對應的頁表是寫死的,它映射到了物理地址的0~1G上。(實際上沒有映射1G,只映射了896M。剩下的空間留下來映射大於1G的物理地址,而這一部分顯然不是寫死的)。因此,大於896M的物理地址是沒有寫死的頁表來對應的,內核不能直接訪問它們(必需要創建映射),稱它們爲高端內存(固然,若是機器內存不足896M,就不存在高端內存。若是是64位機器,也不存在高端內存,由於地址空間很大很大,屬於內核的空間也不止1G了);
NORMAL:不屬於DMA或HIGH的內存就叫NORMAL。
在zone之上的zone_list表明了分配策略,即內存分配時的zone優先級。一種內存分配每每不是隻能在一個zone裏進行分配的,好比分配一個頁給內核使用時,最優先是從NORMAL裏面分配,不行的話就分配DMA裏面的好了(HIGH就不行,由於還沒創建映射),這就是一種分配策略。
每一個內存介質維護了一個mem_map,爲介質中的每個物理頁面創建了一個page結構與之對應,以便管理物理內存。
每一個zone記錄着它在mem_map上的起始位置。而且經過free_area串連着這個zone上空閒的page。物理內存的分配就是從這裏來的,從 free_area上把page摘下,就算是分配了。(內核的內存分配與用戶進程不一樣,用戶使用內存會被內核監督,使用不當就"段錯誤";而內核則無人監督,只能靠自覺,不是本身從free_area摘下的page就不要亂用。)
[創建地址映射]
內核須要物理內存時,不少狀況是整頁分配的,這在上面的mem_map中摘一個page下來就行了。好比前面說到的內核捕捉缺頁異常,而後須要分配一個page以創建映射。
說到這裏,會有一個疑問,內核在分配page、創建地址映射的過程當中,使用的是虛擬地址仍是物理地址呢?首先,內核代碼所訪問的地址都是虛擬地址,由於CPU指令接收的就是虛擬地址(地址映射對於CPU指令是透明的)。可是,創建地址映射時,內核在頁表裏面填寫的內容倒是物理地址,由於地址映射的目標就是要獲得物理地址。
那麼,內核怎麼獲得這個物理地址呢?其實,上面也提到了,mem_map中的page就是根據物理內存來創建的,每個page就對應了一個物理頁。
因而咱們能夠說,虛擬地址的映射是靠這裏page結構來完成的,是它們給出了最終的物理地址。然而,page結構顯然是經過虛擬地址來管理的(前面已經說過,CPU指令接收的就是虛擬地址)。那麼,page結構實現了別人的虛擬地址映射,誰又來實現page結構本身的虛擬地址映射呢?沒人可以實現。
這就引出了前面提到的一個問題,內核空間的頁表項是寫死的。在內核初始化時,內核的地址空間就已經把地址映射寫死了。page結構顯然存在於內核空間,因此它的地址映射問題已經經過「寫死」解決了。
因爲內核空間的頁表項是寫死的,又引出另外一個問題,NORMAL(或DMA)區域的內存可能被同時映射到內核空間和用戶空間。被映射到內核空間是顯然的,由於這個映射已經寫死了。而這些頁面也可能被映射到用戶空間的,在前面提到的缺頁異常的場景裏面就有這樣的可能。映射到用戶空間的頁面應該優先從HIGH區域獲取,由於這些內存被內核訪問起來很不方便,拿給用戶空間再合適不過了。可是HIGH區域可能會耗盡,或者可能由於設備上物理內存不足致使系統裏面根本就沒有HIGH區域,因此,將NORMAL區域映射給用戶空間是必然存在的。
可是NORMAL區域的內存被同時映射到內核空間和用戶空間並無問題,由於若是某個頁面正在被內核使用,對應的page應該已經從free_area被摘下,因而缺頁異常處理代碼中不會再將該頁映射到用戶空間。反過來也同樣,被映射到用戶空間的page天然已經從free_area被摘下,內核不會再去使用這個頁面。
[內核空間管理](圖:右下)
除了對內存整頁的使用,有些時候,內核也須要像用戶程序使用malloc同樣,分配一塊任意大小的空間。這個功能是由slab系統來實現的。
slab至關於爲內核中經常使用的一些結構體對象創建了對象池,好比對應task結構的池、對應mm結構的池、等等。
而slab也維護有通用的對象池,好比"32字節大小"的對象池、"64字節大小"的對象池、等等。內核中經常使用的kmalloc函數(相似於用戶態的malloc)就是在這些通用的對象池中實現分配的。
slab除了對象實際使用的內存空間外,還有其對應的控制結構。有兩種組織方式,若是對象較大,則控制結構使用專門的頁面來保存;若是對象較小,控制結構與對象空間使用相同的頁面。
除了slab,linux 2.6還引入了mempool(內存池)。其意圖是:某些對象咱們不但願它會由於內存不足而分配失敗,因而咱們預先分配若干個,放在mempool中存起來。正常狀況下,分配對象時是不會去動mempool裏面的資源的,照常經過slab去分配。到系統內存緊缺,已經沒法經過slab分配內存時,纔會使用 mempool中的內容。
[頁面換入換出](圖:左上)(圖:右上)
頁面換入換出又是一個很複雜的系統。內存頁面被換出到磁盤,與磁盤文件被映射到內存,是很類似的兩個過程(內存頁被換出到磁盤的動機,就是從此還要從磁盤將其載回內存)。因此swap複用了文件子系統的一些機制。
頁面換入換出是一件很費CPU和IO的事情,可是因爲內存昂貴這一歷史緣由,咱們只好拿磁盤來擴展內存。可是如今內存愈來愈便宜了,咱們能夠輕鬆安裝數G的內存,而後將swap系統關閉。因而swap的實現實在讓人難有探索的慾望,在這裏就不贅述了。(另見:《linux內核頁面回收淺析》)
[用戶空間內存管理]
malloc是libc的庫函數,用戶程序通常經過它(或相似函數)來分配內存空間。
libc對內存的分配有兩種途徑,一是調整堆的大小,二是mmap一個新的虛擬內存區域(堆也是一個vma)。
在內核中,堆是一個一端固定、一端可伸縮的vma(圖:左中)。可伸縮的一端經過系統調用brk來調整。libc管理着堆的空間,用戶調用malloc分配內存時,libc儘可能從現有的堆中去分配。若是堆空間不夠,則經過brk增大堆空間。
當用戶將已分配的空間free時,libc可能會經過brk減少堆空間。可是堆空間增大容易減少卻難,考慮這樣一種狀況,用戶空間連續分配了10塊內存,前9塊已經free。這時,未free的第10塊哪怕只有1字節大,libc也不可以去減少堆的大小。由於堆只有一端可伸縮,而且中間不能掏空。而第10塊內存就死死地佔據着堆可伸縮的那一端,堆的大小無法減少,相關資源也無法歸還內核。
當用戶malloc一塊很大的內存時,libc會經過mmap系統調用映射一個新的vma。由於對於堆的大小調整和空間管理仍是比較麻煩的,從新建一個vma會更方便(上面提到的free的問題也是緣由之一)。
那麼爲何不老是在malloc的時候去mmap一個新的vma呢?第一,對於小空間的分配與回收,被libc管理的堆空間已經可以知足須要,沒必要每次都去進行系統調用。而且vma是以page爲單位的,最小就是分配一個頁;第二,太多的vma會下降系統性能。缺頁異常、vma的新建與銷燬、堆空間的大小調整、等等狀況下,都須要對vma進行操做,須要在當前進程的全部vma中找到須要被操做的那個(或那些)vma。vma數目太多,必然致使性能降低。(在進程的vma較少時,內核採用鏈表來管理vma;vma較多時,改用紅黑樹來管理。)
[用戶的棧]
與堆同樣,棧也是一個vma(圖:左中),這個vma是一端固定、一端可伸(注意,不能縮)的。這個vma比較特殊,沒有相似brk的系統調用讓這個vma伸展,它是自動伸展的。
當用戶訪問的虛擬地址越過這個vma時,內核會在處理缺頁異常的時候將自動將這個vma增大。內核會檢查當時的棧寄存器(如:ESP),訪問的虛擬地址不能超過ESP加n(n爲CPU壓棧指令一次性壓棧的最大字節數)。也就是說,內核是以ESP爲基準來檢查訪問是否越界。
可是,ESP的值是能夠由用戶態程序自由讀寫的,用戶程序若是調整ESP,將棧劃得很大很大怎麼辦呢?內核中有一套關於進程限制的配置,其中就有棧大小的配置,棧只能這麼大,再大就出錯。
對於一個進程來講,棧通常是能夠被伸展得比較大(如:8MB)。然而對於線程呢?
首先線程的棧是怎麼回事?前面說過,線程的mm是共享其父進程的。雖然棧是mm中的一個vma,可是線程不能與其父進程共用這個vma(兩個運行實體顯然不用共用一個棧)。因而,在線程建立時,線程庫經過mmap新建了一個vma,以此做爲線程的棧(大於通常爲:2M)。
可見,線程的棧在某種意義上並非真正棧,它是一個固定的區域,而且容量頗有限。
參考文獻:《夥伴算法》
《slab分配器》