Linux 內存管理模型很是直接明瞭,由於 Linux 的這種機制使其具備可移植性而且可以在內存管理單元相差不大的機器下實現 Linux,下面咱們就來認識一下 Linux 內存管理是如何實現的。算法
每一個 Linux 進程都會有地址空間,這些地址空間由三個段區域組成:text 段、data 段、stack 段。下面是進程地址空間的示例。
數據段(data segment) 包含了程序的變量、字符串、數組和其餘數據的存儲。數據段分爲兩部分,已經初始化的數據和還沒有初始化的數據。其中還沒有初始化的數據就是咱們說的 BSS。數據段部分的初始化須要編譯就期肯定的常量以及程序啓動就須要一個初始值的變量。全部 BSS 部分中的變量在加載後被初始化爲 0 。shell
和 代碼段(Text segment) 不同,data segment 數據段能夠改變。程序老是修改它的變量。並且,許多程序須要在執行時動態分配空間。Linux 容許數據段隨着內存的分配和回收從而增大或者減少。爲了分配內存,程序能夠增長數據段的大小。在 C 語言中有一套標準庫 malloc 常常用於分配內存。進程地址空間描述符包含動態分配的內存區域稱爲 堆(heap)。數據庫
第三部分段是 棧段(stack segment)。在大部分機器上,棧段會在虛擬內存地址頂部地址位置處,並向低位置處(向地址空間爲 0 處)拓展。舉個例子來講,在 32 位 x86 架構的機器上,棧開始於 0xC0000000,這是用戶模式下進程容許可見的 3GB 虛擬地址限制。若是棧一直增大到超過棧段後,就會發生硬件故障並把頁面降低一個頁面。數組
當程序啓動時,棧區域並非空的,相反,它會包含全部的 shell 環境變量以及爲了調用它而向 shell 輸入的命令行。舉個例子,當你輸入緩存
cp cxuan lx1
時,cp 程序會運行並在棧中帶着字符串 cp cxuan lx ,這樣就可以找出源文件和目標文件的名稱。數據結構
當兩個用戶運行在相同程序中,例如編輯器(editor),那麼就會在內存中保持編輯器程序代碼的兩個副本,可是這種方式並不高效。Linux 系統支持共享文本段做爲替代。下面圖中咱們會看到 A 和 B 兩個進程,它們有着相同的文本區域。
數據段和棧段只有在 fork 以後纔會共享,共享也是共享未修改過的頁面。若是任何一個都須要變大可是沒有相鄰空間容納的話,也不會有問題,由於相鄰的虛擬頁面沒必要映射到相鄰的物理頁面上。多線程
除了動態分配更多的內存,Linux 中的進程能夠經過內存映射文件來訪問文件數據。這個特性可使咱們把一個文件映射到進程空間的一部分而該文件就能夠像位於內存中的字節數組同樣被讀寫。把一個文件映射進來使得隨機讀寫比使用 read 和 write 之類的 I/O 系統調用要容易得多。共享庫的訪問就是使用了這種機制。以下所示
咱們能夠看到兩個相同文件會被映射到相同的物理地址上,可是它們屬於不一樣的地址空間。架構
映射文件的優勢是,兩個或多個進程能夠同時映射到同一文件中,任意一個進程對文件的寫操做對其餘文件可見。經過使用映射臨時文件的方式,能夠爲多線程共享內存提供高帶寬,臨時文件在進程退出後消失。可是實際上,並無兩個相同的地址空間,由於每一個進程維護的打開文件和信號不一樣。框架
下面咱們探討一下關於內存管理的系統調用方式。事實上,POSIX 並無給內存管理指定任何的系統調用。然而,Linux 卻有本身的內存系統調用,主要系統調用以下
若是遇到錯誤,那麼 s 的返回值是 -1,a 和 addr 是內存地址,len 表示的是長度,prot 表示的是控制保護位,flags 是其餘標誌位,fd 是文件描述符,offset 是文件偏移量。編輯器
brk 經過給出超過數據段以外的第一個字節地址來指定數據段的大小。若是新的值要比原來的大,那麼數據區會變得愈來愈大,反之會愈來愈小。
mmap 和 unmap 系統調用會控制映射文件。mmp 的第一個參數 addr 決定了文件映射的地址。它必須是頁面大小的倍數。若是參數是 0,系統會分配地址並返回 a。第二個參數是長度,它告訴了須要映射多少字節。它也是頁面大小的倍數。prot 決定了映射文件的保護位,保護位能夠標記爲 可讀、可寫、可執行或者這些的結合。第四個參數 flags 可以控制文件是私有的仍是可讀的以及 addr 是必須的仍是隻是進行提示。第五個參數 fd 是要映射的文件描述符。只有打開的文件是能夠被映射的,所以若是想要進行文件映射,必須打開文件;最後一個參數 offset 會指示文件從何時開始,並不必定每次都要從零開始。
內存管理系統是操做系統最重要的部分之一。從計算機早期開始,咱們實際使用的內存都要比系統中實際存在的內存多。內存分配策略克服了這一限制,而且其中最有名的就是 虛擬內存(virtual memory)。經過在多個競爭的進程之間共享虛擬內存,虛擬內存得以讓系統有更多的內存。虛擬內存子系統主要包括下面這些概念。
大地址空間
操做系統使系統使用起來好像比實際的物理內存要大不少,那是由於虛擬內存要比物理內存大不少倍。
保護
系統中的每一個進程都會有本身的虛擬地址空間。這些虛擬地址空間彼此徹底分開,所以運行一個應用程序的進程不會影響另外一個。而且,硬件虛擬內存機制容許內存保護關鍵內存區域。
內存映射
內存映射用來向進程地址空間映射圖像和數據文件。在內存映射中,文件的內容直接映射到進程的虛擬空間中。
公平的物理內存分配
內存管理子系統容許系統中的每一個正在運行的進程公平分配系統的物理內存。
共享虛擬內存
儘管虛擬內存讓進程有本身的內存空間,可是有的時候你是須要共享內存的。例如幾個進程同時在 shell 中運行,這會涉及到 IPC 的進程間通訊問題,這個時候你須要的是共享內存來進行信息傳遞而不是經過拷貝每一個進程的副本獨立運行。
下面咱們就正式探討一下什麼是 虛擬內存
虛擬內存的抽象模型
在考慮 Linux 用於支持虛擬內存的方法以前,考慮一個不會被太多細節困擾的抽象模型是頗有用的。
處理器在執行指令時,會從內存中讀取指令並將其解碼(decode),在指令解碼時會獲取某個位置的內容並將他存到內存中。而後處理器繼續執行下一條指令。這樣,處理器老是在訪問存儲器以獲取指令和存儲數據。
在虛擬內存系統中,全部的地址空間都是虛擬的而不是物理的。可是實際存儲和提取指令的是物理地址,因此須要讓處理器根據操做系統維護的一張表將虛擬地址轉換爲物理地址。
爲了簡單的完成轉換,虛擬地址和物理地址會被分爲固定大小的塊,稱爲 頁(page)。這些頁有相同大小,若是頁面大小不同的話,那麼操做系統將很難管理。Alpha AXP系統上的 Linux 使用 8 KB 頁面,而 Intel x86 系統上的 Linux 使用 4 KB 頁面。每一個頁面都有一個惟一的編號,即頁面框架號(PFN)。
上面就是 Linux 內存映射模型了,在這個頁模型中,虛擬地址由兩部分組成:偏移量和虛擬頁框號。每次處理器遇到虛擬地址時都會提取偏移量和虛擬頁框號。處理器必須將虛擬頁框號轉換爲物理頁號,而後以正確的偏移量的位置訪問物理頁。
上圖中展現了兩個進程 A 和 B 的虛擬地址空間,每一個進程都有本身的頁表。這些頁表將進程中的虛擬頁映射到內存中的物理頁中。頁表中每一項均包含
有效標誌(valid flag): 代表此頁表條目是否有效
該條目描述的物理頁框號
訪問控制信息,頁面使用方式,是否可寫以及是否能夠執行代碼
要將處理器的虛擬地址映射爲內存的物理地址,首先須要計算虛擬地址的頁框號和偏移量。頁面大小爲 2 的次冪,能夠經過移位完成操做。
若是當前進程嘗試訪問虛擬地址,可是訪問不到的話,這種狀況稱爲 缺頁異常,此時虛擬操做系統的錯誤地址和頁面錯誤的緣由將通知操做系統。
經過以這種方式將虛擬地址映射到物理地址,虛擬內存能夠以任何順序映射到系統的物理頁面。
按需分頁
因爲物理內存要比虛擬內存少不少,所以操做系統須要注意儘可能避免直接使用低效的物理內存。節省物理內存的一種方式是僅加載執行程序當前使用的頁面(這未嘗不是一種懶加載的思想呢?)。例如,能夠運行數據庫來查詢數據庫,在這種狀況下,不是全部的數據都裝入內存,只裝載須要檢查的數據。這種僅僅在須要時纔將虛擬頁面加載進內中的技術稱爲按需分頁。
交換
若是某個進程須要將虛擬頁面傳入內存,可是此時沒有可用的物理頁面,那麼操做系統必須丟棄物理內存中的另外一個頁面來爲該頁面騰出空間。
若是頁面已經修改過,那麼操做系統必須保留該頁面的內容,以便之後能夠訪問它。這種類型的頁面被稱爲髒頁,當將其從內存中移除時,它會保存在稱爲交換文件的特殊文件中。相對於處理器和物理內存的速度,對交換文件的訪問很是慢,而且操做系統須要兼顧將頁面寫到磁盤的以及將它們保留在內存中以便再次使用。
Linux 使用最近最少使用(LRU)頁面老化技術來公平的選擇可能會從系統中刪除的頁面,這個方案涉及系統中的每一個頁面,頁面的年齡隨着訪問次數的變化而變化,若是某個頁面訪問次數多,那麼該頁就表示越 年輕,若是某個呃頁面訪問次數太少,那麼該頁越容易被換出。
物理和虛擬尋址模式
大多數多功能處理器都支持 物理地址模式和虛擬地址模式的概念。物理尋址模式不須要頁表,而且處理器不會在此模式下嘗試執行任何地址轉換。 Linux 內核被連接在物理地址空間中運行。
Alpha AXP 處理器沒有物理尋址模式。相反,它將內存空間劃分爲幾個區域,並將其中兩個指定爲物理映射的地址。此內核地址空間稱爲 KSEG 地址空間,它包含從 0xfffffc0000000000 向上的全部地址。爲了從 KSEG 中連接的代碼(按照定義,內核代碼)執行或訪問其中的數據,該代碼必須在內核模式下執行。連接到 Alpha 上的 Linux內核以從地址 0xfffffc0000310000 執行。
訪問控制
頁面表的每一項還包含訪問控制信息,訪問控制信息主要檢查進程是否應該訪問內存。
必要時須要對內存進行訪問限制。 例如包含可執行代碼的內存,天然是隻讀內存; 操做系統不該容許進程經過其可執行代碼寫入數據。 相比之下,包含數據的頁面能夠被寫入,可是嘗試執行該內存的指令將失敗。 大多數處理器至少具備兩種執行模式:內核態和用戶態。 你不但願訪問用戶執行內核代碼或內核數據結構,除非處理器之內核模式運行。
訪問控制信息被保存在上面的 Page Table Entry ,頁表項中,上面這幅圖是 Alpha AXP的 PTE。位字段具備如下含義
V
表示 valid ,是否有效位
FOR
讀取時故障,在嘗試讀取此頁面時出現故障
FOW
寫入時錯誤,在嘗試寫入時發生錯誤
FOE
執行時發生錯誤,在嘗試執行此頁面中的指令時,處理器都會報告頁面錯誤並將控制權傳遞給操做系統,
ASM
地址空間匹配,當操做系統但願清除轉換緩衝區中的某些條目時,將使用此選項。
GH
當在使用單個轉換緩衝區條目而不是多個轉換緩衝區條目映射整個塊時使用的提示。
KRE
內核模式運行下的代碼能夠讀取頁面
URE
用戶模式下的代碼能夠讀取頁面
KWE
之內核模式運行的代碼能夠寫入頁面
UWE
以用戶模式運行的代碼能夠寫入頁面
頁框號
對於設置了 V 位的 PTE,此字段包含此 PTE 的物理頁面幀號(頁面幀號)。對於無效的 PTE,若是此字段不爲零,則包含有關頁面在交換文件中的位置的信息。
除此以外,Linux 還使用了兩個位
PAGE_DIRTY
若是已設置,則須要將頁面寫出到交換文件中
PAGE_ACCESSED
Linux 用來將頁面標記爲已訪問。
上面的虛擬內存抽象模型能夠用來實施,可是效率不會過高。操做系統和處理器設計人員都嘗試提升性能。 可是除了提升處理器,內存等的速度以外,最好的方法就是維護有用信息和數據的高速緩存,從而使某些操做更快。在 Linux 中,使用不少和內存管理有關的緩衝區,使用緩衝區來提升效率。
緩衝區緩存
緩衝區高速緩存包含塊設備驅動程序使用的數據緩衝區。
還記得什麼是塊設備麼?這裏回顧下
塊設備是一個能存儲固定大小塊信息的設備,它支持以固定大小的塊,扇區或羣集讀取和(可選)寫入數據。每一個塊都有本身的物理地址。一般塊的大小在 512 - 65536 之間。全部傳輸的信息都會以連續的塊爲單位。塊設備的基本特徵是每一個塊都較爲對立,可以獨立的進行讀寫。常見的塊設備有 硬盤、藍光光盤、USB 盤
與字符設備相比,塊設備一般須要較少的引腳。
緩衝區高速緩存經過設備標識符和塊編號用於快速查找數據塊。 若是能夠在緩衝區高速緩存中找到數據,則無需從物理塊設備中讀取數據,這種訪問方式要快得多。
頁緩存
頁緩存用於加快對磁盤上圖像和數據的訪問
它用於一次一頁地緩存文件中的內容,而且能夠經過文件和文件中的偏移量進行訪問。當頁面從磁盤讀入內存時,它們被緩存在頁面緩存中。
交換區緩存
僅僅已修改(髒頁)被保存在交換文件中
只要這些頁面在寫入交換文件後沒有修改,則下次交換該頁面時,無需將其寫入交換文件,由於該頁面已在交換文件中。 能夠直接丟棄。 在大量交換的系統中,這節省了許多沒必要要的和昂貴的磁盤操做。
硬件緩存
處理器中一般使用一種硬件緩存。頁表條目的緩存。在這種狀況下,處理器並不老是直接讀取頁表,而是根據須要緩存頁的翻譯。 這些是轉換後備緩衝區 也被稱爲 TLB,包含來自系統中一個或多個進程的頁表項的緩存副本。
引用虛擬地址後,處理器將嘗試查找匹配的 TLB 條目。 若是找到,則能夠將虛擬地址直接轉換爲物理地址,並對數據執行正確的操做。 若是處理器找不到匹配的 TLB 條目, 它經過向操做系統發信號通知已發生 TLB 丟失得到操做系統的支持和幫助。系統特定的機制用於將該異常傳遞給能夠修復問題的操做系統代碼。 操做系統爲地址映射生成一個新的 TLB 條目。 清除異常後,處理器將再次嘗試轉換虛擬地址。此次可以執行成功。
使用緩存也存在缺點,爲了節省精力,Linux 必須使用更多的時間和空間來維護這些緩存,而且若是緩存損壞,系統將會崩潰。
Linux 頁表
Linux 假定頁表分爲三個級別。訪問的每一個頁表都包含下一級頁表
圖中的 PDG 表示全局頁表,當建立一個新的進程時,都要爲新進程建立一個新的頁面目錄,即 PGD。
要將虛擬地址轉換爲物理地址,處理器必須獲取每一個級別字段的內容,將其轉換爲包含頁表的物理頁的偏移量,並讀取下一級頁表的頁框號。這樣重複三次,直到找到包含虛擬地址的物理頁面的頁框號爲止。
Linux 運行的每一個平臺都必須提供翻譯宏,這些宏容許內核遍歷特定進程的頁表。這樣,內核無需知道頁表條目的格式或它們的排列方式。
頁分配和取消分配
對系統中物理頁面有不少需求。例如,當圖像加載到內存中時,操做系統須要分配頁面。
系統中全部物理頁面均由 mem_map 數據結構描述,這個數據結構是 mem_map_t 的列表。它包括一些重要的屬性
count :這是頁面的用戶數計數,當頁面在多個進程之間共享時,計數大於 1
age:這是描述頁面的年齡,用於肯定頁面是否適合丟棄或交換
map_nr :這是此mem_map_t描述的物理頁框號。
頁面分配代碼使用 free_area向量查找和釋放頁面,free_area 的每一個元素都包含有關頁面塊的信息。
頁面分配
Linux 的頁面分配使用一種著名的夥伴算法來進行頁面的分配和取消分配。頁面以 2 的冪爲單位進行塊分配。這就意味着它能夠分配 1頁、2 頁、4頁等等,只要系統中有足夠可用的頁面來知足需求就能夠。判斷的標準是nr_free_pages> min_free_pages,若是知足,就會在 free_area 中搜索所需大小的頁面塊完成分配。free_area 的每一個元素都有該大小的塊的已分配頁面和空閒頁面塊的映射。
分配算法會搜索請求大小的頁面塊。若是沒有任何請求大小的頁面塊可用的話,會搜尋一個是請求大小二倍的頁面塊,而後重複,直到一直搜尋完 free_area 找到一個頁面塊爲止。若是找到的頁面塊要比請求的頁面塊大,就會對找到的頁面塊進行細分,直到找到合適的大小塊爲止。
由於每一個塊都是 2 的次冪,因此拆分過程很容易,由於你只需將塊分紅兩半便可。空閒塊在適當的隊列中排隊,分配的頁面塊返回給調用者。
若是請求一個 2 個頁的塊,則 4 頁的第一個塊(從第 4 頁的框架開始)將被分紅兩個 2 頁的塊。第一個頁面(從第 4 頁的幀開始)將做爲分配的頁面返回給調用方,第二個塊(從第 6 頁的頁面開始)將做爲 2 頁的空閒塊排隊到 free_area 數組的元素 1 上。
頁面取消分配
上面的這種內存方式最形成一種後果,那就是內存的碎片化,會將較大的空閒頁面分紅較小的頁面。頁面解除分配代碼會盡量將頁面從新組合成爲更大的空閒塊。每釋放一個頁面,都會檢查相同大小的相鄰的塊,以查看是否空閒。若是是,則將其與新釋放的頁面塊組合以造成下一個頁面大小塊的新的自由頁面塊。 每次將兩個頁面塊從新組合爲更大的空閒頁面塊時,頁面釋放代碼就會嘗試將該頁面塊從新組合爲更大的空閒頁面。 經過這種方式,可用頁面的塊將盡量多地使用內存。
例如上圖,若是要釋放第 1 頁的頁面,則將其與已經空閒的第 0 頁頁面框架組合在一塊兒,並做爲大小爲 2頁的空閒塊排隊到 free_area 的元素 1 中
內存映射
內核有兩種類型的內存映射:共享型(shared) 和私有型(private)。私有型是當進程爲了只讀文件,而不寫文件時使用,這時,私有映射更加高效。 可是,任何對私有映射頁的寫操做都會致使內核中止映射該文件中的頁。因此,寫操做既不會改變磁盤上的文件,對訪問該文件的其它進程也是不可見的。
按需分頁
一旦可執行映像被內存映射到虛擬內存後,它就能夠被執行了。由於只將映像的開頭部分物理的拉入到內存中,所以它將很快訪問物理內存還沒有存在的虛擬內存區域。當進程訪問沒有有效頁表的虛擬地址時,操做系統會報告這項錯誤。
頁面錯誤描述頁面出錯的虛擬地址和引發的內存訪問(RAM)類型。
Linux 必須找到表明發生頁面錯誤的內存區域的 vm_area_struct 結構。因爲搜索 vm_area_struct 數據結構對於有效處理頁面錯誤相當重要,所以它們以 AVL(Adelson-Velskii和Landis)樹結構連接在一塊兒。若是引發故障的虛擬地址沒有 vm_area_struct 結構,則此進程已經訪問了非法地址,Linux 會向進程發出 SIGSEGV 信號,若是進程沒有用於該信號的處理程序,那麼進程將會終止。
而後,Linux 會針對此虛擬內存區域所容許的訪問類型,檢查發生的頁面錯誤類型。 若是該進程以非法方式訪問內存,例如寫入僅容許讀的區域,則還會發出內存訪問錯誤信號。
如今,Linux 已肯定頁面錯誤是合法的,所以必須對其進行處理。