1. 程式人生 > >真香!Linux 原來是這麼管理記憶體的

真香!Linux 原來是這麼管理記憶體的

Linux 記憶體管理模型非常直接明瞭,因為 Linux 的這種機制使其具有可移植性並且能夠在記憶體管理單元相差不大的機器下實現 Linux,下面我們就來認識一下 Linux 記憶體管理是如何實現的。 ## 基本概念 每個 Linux 程序都會有地址空間,這些地址空間由三個段區域組成:**text 段、data 段、stack 段**。下面是程序地址空間的示例。 ![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200728133651709-919756569.png) `資料段(data segment)` 包含了程式的變數、字串、陣列和其他資料的儲存。資料段分為兩部分,已經初始化的資料和尚未初始化的資料。其中`尚未初始化的資料`就是我們說的 BSS。資料段部分的初始化需要編譯就期確定的常量以及程式啟動就需要一個初始值的變數。所有 BSS 部分中的變數在載入後被初始化為 0 。 和 `程式碼段(Text segment)` 不一樣,data segment 資料段可以改變。程式總是修改它的變數。而且,許多程式需要在執行時動態分配空間。Linux 允許資料段隨著記憶體的分配和回收從而增大或者減小。為了分配記憶體,程式可以增加資料段的大小。在 C 語言中有一套標準庫 `malloc` 經常用於分配記憶體。程序地址空間描述符包含動態分配的記憶體區域稱為 `堆(heap)`。 第三部分段是 `棧段(stack segment)`。在大部分機器上,棧段會在虛擬記憶體地址頂部地址位置處,並向低位置處(向地址空間為 0 處)拓展。舉個例子來說,在 32 位 x86 架構的機器上,棧開始於 `0xC0000000`,這是使用者模式下程序允許可見的 3GB 虛擬地址限制。如果棧一直增大到超過棧段後,就會發生硬體故障並把頁面下降一個頁面。 當程式啟動時,棧區域並不是空的,相反,它會包含所有的 shell 環境變數以及為了呼叫它而向 shell 輸入的命令列。舉個例子,當你輸入 ```shell cp cxuan lx ``` 時,cp 程式會執行並在棧中帶著字串 `cp cxuan lx` ,這樣就能夠找出原始檔和目標檔案的名稱。 當兩個使用者執行在相同程式中,例如`編輯器(editor)`,那麼就會在記憶體中保持編輯器程式程式碼的兩個副本,但是這種方式並不高效。Linux 系統支援`共享文字段作`為替代。下面圖中我們會看到 A 和 B 兩個程序,它們有著相同的文字區域。 ![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200728133702324-1546156177.png) 資料段和棧段只有在 fork 之後才會共享,共享也是共享未修改過的頁面。如果任何一個都需要變大但是沒有相鄰空間容納的話,也不會有問題,因為相鄰的虛擬頁面不必對映到相鄰的物理頁面上。 除了動態分配更多的記憶體,Linux 中的程序可以通過`記憶體對映檔案`來訪問檔案資料。這個特性可以使我們把一個檔案對映到程序空間的一部分而該檔案就可以像位於記憶體中的位元組陣列一樣被讀寫。把一個檔案對映進來使得隨機讀寫比使用 read 和 write 之類的 I/O 系統呼叫要容易得多。共享庫的訪問就是使用了這種機制。如下所示 ![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200728133727554-1657318902.png) 我們可以看到兩個相同檔案會被對映到相同的實體地址上,但是它們屬於不同的地址空間。 對映檔案的優點是,兩個或多個程序可以同時對映到同一檔案中,任意一個程序對檔案的寫操作對其他檔案可見。通過使用對映臨時檔案的方式,可以為多執行緒共享記憶體`提供高頻寬`,臨時檔案在程序退出後消失。但是實際上,並沒有兩個相同的地址空間,因為每個程序維護的開啟檔案和訊號不同。 ## Linux 記憶體管理系統呼叫 下面我們探討一下關於記憶體管理的系統呼叫方式。事實上,POSIX 並沒有給記憶體管理指定任何的系統呼叫。然而,Linux 卻有自己的記憶體系統呼叫,主要系統呼叫如下 | 系統呼叫 | 描述 | | --------------------------------------- | -------------- | | s = brk(addr) | 改變資料段大小 | | a = mmap(addr,len,prot,flags,fd,offset) | 進行對映 | | s = unmap(addr,len) | 取消對映 | 如果遇到錯誤,那麼 s 的返回值是 -1,a 和 addr 是記憶體地址,len 表示的是長度,prot 表示的是控制保護位,flags 是其他標誌位,fd 是檔案描述符,offset 是檔案偏移量。 `brk` 通過給出超過資料段之外的第一個位元組地址來指定資料段的大小。如果新的值要比原來的大,那麼資料區會變得越來越大,反之會越來越小。 `mmap` 和 `unmap` 系統呼叫會控制對映檔案。mmp 的第一個引數 addr 決定了檔案對映的地址。它必須是頁面大小的倍數。如果引數是 0,系統會分配地址並返回 a。第二個引數是長度,它告訴了需要對映多少位元組。它也是頁面大小的倍數。prot 決定了對映檔案的保護位,保護位可以標記為 **可讀、可寫、可執行或者這些的結合**。第四個引數 flags 能夠控制檔案是私有的還是可讀的以及 addr 是必須的還是隻是進行提示。第五個引數 fd 是要對映的檔案描述符。只有開啟的檔案是可以被對映的,因此如果想要進行檔案對映,必須開啟檔案;最後一個引數 offset 會指示檔案從什麼時候開始,並不一定每次都要從零開始。 ## Linux 記憶體管理實現 記憶體管理系統是作業系統最重要的部分之一。從計算機早期開始,我們實際使用的記憶體都要比系統中實際存在的記憶體多。`記憶體分配策略`克服了這一限制,並且其中最有名的就是 `虛擬記憶體(virtual memory)`。通過在多個競爭的程序之間共享虛擬記憶體,虛擬記憶體得以讓系統有更多的記憶體。虛擬記憶體子系統主要包括下面這些概念。 **大地址空間** 作業系統使系統使用起來好像比實際的實體記憶體要大很多,那是因為虛擬記憶體要比實體記憶體大很多倍。 **保護** 系統中的每個程序都會有自己的虛擬地址空間。這些虛擬地址空間彼此完全分開,因此執行一個應用程式的程序不會影響另一個。並且,硬體虛擬記憶體機制允許記憶體保護關鍵記憶體區域。 **記憶體對映** 記憶體對映用來向程序地址空間對映影象和資料檔案。在記憶體對映中,檔案的內容直接對映到程序的虛擬空間中。 **公平的實體記憶體分配** 記憶體管理子系統允許系統中的每個正在執行的程序公平分配系統的實體記憶體。 **共享虛擬記憶體** 儘管虛擬記憶體讓程序有自己的記憶體空間,但是有的時候你是需要共享記憶體的。例如幾個程序同時在 shell 中執行,這會涉及到 IPC 的程序間通訊問題,這個時候你需要的是共享記憶體來進行資訊傳遞而不是通過拷貝每個程序的副本獨立執行。 下面我們就正式探討一下什麼是 `虛擬記憶體` ### 虛擬記憶體的抽象模型 在考慮 Linux 用於支援虛擬記憶體的方法之前,考慮一個不會被太多細節困擾的抽象模型是很有用的。 處理器在執行指令時,會從記憶體中讀取指令並將其`解碼(decode)`,在指令解碼時會獲取某個位置的內容並將他存到記憶體中。然後處理器繼續執行下一條指令。這樣,處理器總是在訪問儲存器以獲取指令和儲存資料。 在虛擬記憶體系統中,所有的地址空間都是虛擬的而不是物理的。但是實際儲存和提取指令的是實體地址,所以需要讓處理器根據作業系統維護的一張表將虛擬地址轉換為實體地址。 為了簡單的完成轉換,虛擬地址和實體地址會被分為固定大小的塊,稱為 `頁(page)`。這些頁有相同大小,如果頁面大小不一樣的話,那麼作業系統將很難管理。Alpha AXP系統上的 Linux 使用 8 KB 頁面,而 Intel x86 系統上的 Linux 使用 4 KB 頁面。每個頁面都有一個唯一的編號,即`頁面框架號(PFN)`。 ![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200728133751561-1211884450.png) 上面就是 Linux 記憶體對映模型了,在這個頁模型中,虛擬地址由兩部分組成:**偏移量和虛擬頁框號**。每次處理器遇到虛擬地址時都會提取偏移量和虛擬頁框號。處理器必須將虛擬頁框號轉換為物理頁號,然後以正確的偏移量的位置訪問物理頁。 上圖中展示了兩個程序 A 和 B 的虛擬地址空間,每個程序都有自己的頁表。這些頁表將程序中的虛擬頁對映到記憶體中的物理頁中。頁表中每一項均包含 * `有效標誌(valid flag)`: 表明此頁表條目是否有效 * 該條目描述的物理頁框號 * 訪問控制資訊,頁面使用方式,是否可寫以及是否可以執行程式碼 要將處理器的虛擬地址對映為記憶體的實體地址,首先需要計算虛擬地址的頁框號和偏移量。頁面大小為 2 的次冪,可以通過移位完成操作。 如果當前程序嘗試訪問虛擬地址,但是訪問不到的話,這種情況稱為 `缺頁異常`,此時虛擬作業系統的錯誤地址和頁面錯誤的原因將通知作業系統。 通過以這種方式將虛擬地址對映到實體地址,虛擬記憶體可以以任何順序對映到系統的物理頁面。 #### 按需分頁 由於實體記憶體要比虛擬記憶體少很多,因此作業系統需要注意儘量避免直接使用`低效`的實體記憶體。節省實體記憶體的一種方式是僅載入執行程式當前使用的頁面(這何嘗不是一種懶載入的思想呢?)。例如,可以執行資料庫來查詢資料庫,在這種情況下,不是所有的資料都裝入記憶體,只裝載需要檢查的資料。這種僅僅在需要時才將虛擬頁面載入進內中的技術稱為按需分頁。 #### 交換 如果某個程序需要將虛擬頁面傳入記憶體,但是此時沒有可用的物理頁面,那麼作業系統必須丟棄實體記憶體中的另一個頁面來為該頁面騰出空間。 如果頁面已經修改過,那麼作業系統必須保留該頁面的內容,以便以後可以訪問它。這種型別的頁面被稱為髒頁,當將其從記憶體中移除時,它會儲存在稱為`交換檔案`的特殊檔案中。相對於處理器和實體記憶體的速度,對交換檔案的訪問非常慢,並且作業系統需要兼顧將頁面寫到磁碟的以及將它們保留在記憶體中以便再次使用。 Linux 使用`最近最少使用(LRU)`頁面老化技術來公平的選擇可能會從系統中刪除的頁面,這個方案涉及系統中的每個頁面,頁面的年齡隨著訪問次數的變化而變化,如果某個頁面訪問次數多,那麼該頁就表示越 `年輕`,如果某個呃頁面訪問次數太少,那麼該頁越容易被`換出`。 #### 物理和虛擬定址模式 大多數多功能處理器都支援 `實體地址`模式和`虛擬地址`模式的概念。物理定址模式不需要頁表,並且處理器不會在此模式下嘗試執行任何地址轉換。 Linux 核心被連結在實體地址空間中執行。 Alpha AXP 處理器沒有物理定址模式。相反,它將記憶體空間劃分為幾個區域,並將其中兩個指定為物理對映的地址。此核心地址空間稱為 KSEG 地址空間,它包含從 0xfffffc0000000000 向上的所有地址。為了從 KSEG 中連結的程式碼(按照定義,核心程式碼)執行或訪問其中的資料,該程式碼必須在核心模式下執行。連結到 Alpha 上的 Linux核心以從地址 0xfffffc0000310000 執行。 #### 訪問控制 頁面表的每一項還包含訪問控制資訊,訪問控制資訊主要檢查程序是否應該訪問記憶體。 必要時需要對記憶體進行`訪問限制`。 例如包含可執行程式碼的記憶體,自然是隻讀記憶體; 作業系統不應允許程序通過其可執行程式碼寫入資料。 相比之下,包含資料的頁面可以被寫入,但是嘗試執行該記憶體的指令將失敗。 大多數處理器至少具有兩種執行模式:核心態和使用者態。 你不希望訪問使用者執行核心程式碼或核心資料結構,除非處理器以核心模式執行。 ![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200728133804983-495935122.png) 訪問控制資訊被儲存在上面的 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 盤** 與字元裝置相比,塊裝置通常需要較少的引腳。 ![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200728133816523-1027773626.png) 緩衝區快取記憶體通過`裝置識別符號`和塊編號用於快速查詢資料塊。 如果可以在緩衝區快取記憶體中找到資料,則無需從物理塊裝置中讀取資料,這種訪問方式要快得多。 #### 頁快取 頁快取用於加快對磁碟上影象和資料的訪問 它用於一次一頁地快取檔案中的內容,並且可以通過檔案和檔案中的偏移量進行訪問。當頁面從磁碟讀入記憶體時,它們被快取在頁面快取中。 #### 交換區快取 僅僅已修改(髒頁)被儲存在交換檔案中 只要這些頁面在寫入交換檔案後沒有修改,則下次交換該頁面時,無需將其寫入交換檔案,因為該頁面已在交換檔案中。 可以直接丟棄。 在大量交換的系統中,這節省了許多不必要的和昂貴的磁碟操作。 #### 硬體快取 處理器中通常使用一種硬體快取。頁表條目的快取。在這種情況下,處理器並不總是直接讀取頁表,而是根據需要快取頁的翻譯。 這些是`轉換後備緩衝區` 也被稱為 `TLB`,包含來自系統中一個或多個程序的頁表項的快取副本。 引用虛擬地址後,處理器將嘗試查詢匹配的 TLB 條目。 如果找到,則可以將虛擬地址直接轉換為實體地址,並對資料執行正確的操作。 如果處理器找不到匹配的 TLB 條目, 它通過向作業系統發訊號通知已發生 TLB 丟失獲得作業系統的支援和幫助。系統特定的機制用於將該異常傳遞給可以修復問題的作業系統程式碼。 作業系統為地址對映生成一個新的 TLB 條目。 清除異常後,處理器將再次嘗試轉換虛擬地址。這次能夠執行成功。 使用快取也存在缺點,為了節省精力,Linux 必須使用更多的時間和空間來維護這些快取,並且如果快取損壞,系統將會崩潰。 ### Linux 頁表 Linux 假定頁表分為三個級別。訪問的每個頁表都包含下一級頁表 ![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200728133825853-598324404.png) 圖中的 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 的次冪,所以拆分過程很容易,因為你只需將塊分成兩半即可。空閒塊在適當的佇列中排隊,分配的頁面塊返回給呼叫者。 ![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200728133835405-81416188.png) 如果請求一個 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 已確定頁面錯誤是合法的,因此必須對其進行處理。 ![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200728134013343-756642487.png) ![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200728134556172-11484159