轉 Linux內存管理原理

在用戶態,內核態邏輯地址專指下文說的線性偏移前的地址Linux內核虛擬3.夥伴算法和slab分配器
16個頁面RAM由於最大連續內存大小爲16個頁面
頁面最多16個頁面,因此16/2order(0)bimap有8個bit位兩個頁框page1 與page2組成與兩個頁框page3 與page4組成,這兩個塊之間有一個bit位
order(1)bimap有4個bit位order(2)bimap有4個bit位的2個頁面分配過程
當咱們須要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)空閒鏈表中[12,13]頁面塊返回給用戶。
order(0): 5, 10
order(1): 14 [14,15]
order(2):
order(3):
回收過程
當咱們回收頁面11(order 0)時,則執行如下步驟:
一、找到在order(0)夥伴位圖中表明頁面11的位,計算使用下面公示:
index = page_idx >> (order + 1)
= 11 >> (0 + 1)
= 5
 
buddy避免內部碎片的努力
另外,slab分配器:解決內部碎片問題
內核 緩存4.頁面回收/側重機制
關於頁面的使用
linux內核內存管理淺析linux slub分配器淺析linux內核文件讀寫淺析 linux內核內存管理淺析
頁面回收簡述
 
 
PFRA回收通常頁面
linux內核虛擬文件系統淺析
 
關於內存映射
 
linux內核文件讀寫淺析
 
哪些頁面該回收
 
 
 
 
肯定最近最少使用
 
 
 
 
反向映射
 
 
 
頁面換入換出
 
 
 
最後的必殺
5.內存管理架構
地址映射
虛擬地址管理
物理內存管理
創建地址映射
內核空間管理
頁面換入換出linux內核頁面回收淺析
用戶空間內存管理
用戶的棧 夥伴算法避免物理內存碎片化slab分配器linux slub分配器淺析linux內存管理淺析linux頁面回收淺析
 
 
 
探索 Linux 內存模型
理解 Linux 使用的內存模型是從更大程度上掌握 Linux 設計和實現的第一步,所以本文將概述 Linux 內存模型和管理。
Linux 使用的是單一總體式結構 (Monolithic),其中定義了一組原語或系統調用以實現操做系統的服務,例如在幾個模塊中以超級模式運行的進程管理、併發控制和內存管理服務。儘管出於兼容性考慮,Linux 依然將段控制單元模型 (segment control unit model) 保持一種符號表示,但實際上已經不多使用這種模型了。
與內存管理有關的主要問題有:
  • 虛擬內存的管理,這是介於應用程序請求與物理內存之間的一個邏輯層。
  • 物理內存的管理。
  • 內核虛擬內存的管理/內核內存分配器,這是一個用來知足對內存的請求的組件。這種對內存的請求可能來自於內核,也可能來自於用戶。
  • 虛擬地址空間的管理。
  • 交換和緩存。
本文探討了如下問題,能夠幫助您從操做系統中內存管理的角度來理解 Linux 的內幕:
  • 段控制單元模型,一般專用於 Linux
  • 分頁模型,一般專用於 Linux
  • 物理內存方面的知識
雖然本文並無詳細介紹 Linux 內核管理內存的方法,可是介紹了有關整個內存模型的知識以及系統的尋址方式,這些介紹可爲您進一步的學習提供一個框架。本文重點介紹的是 x86 架構,但本文中的知識對於其餘硬件實現一樣適用。
x86 內存架構
在 x86 架構中,內存被劃分紅 3 種類型的地址:
  • 邏輯地址 (logical address) 是存儲位置的地址,它可能直接對應於一個物理位置,也可能不直接對應於一個物理位置。邏輯地址一般在請求控制器中的信息時使用。
  • 線性地址 (linear address) (或稱爲 平面地址空間)是從 0 開始進行尋址的內存。以後的每一個字節均可順序使用下一數字來引用(0、一、二、3 等),直到內存末尾爲止。這就是大部分非 Intel CPU 的尋址方式。Intel® 架構使用了分段的地址空間,其中內存被劃分紅 64KB 的段,有一個段寄存器老是指向當前正在尋址的段的基址。這種架構中的 32 位模式被視爲平面地址空間,不過它也使用了段。
  • 物理地址 (physical address) 是使用物理地址總線中的位表示的地址。物理地址可能與邏輯地址不一樣,內存管理單元能夠將邏輯地址轉換成物理地址。
CPU 使用兩種單元將邏輯地址轉換成物理地址。第一種稱爲分段單元 (segmented unit),另一種稱爲分頁單元 (paging unit)。
圖 1. 轉換地址空間使用的兩種單元
下面讓咱們來介紹一下段控制單元模型。
段控制單元模型概述
這種分段模型背後的基本思想是將內存分段管理。從本質上來講,每一個段就是本身的地址空間。段由兩個元素構成:
  • 基址 (base address) 包含某個物理內存位置的地址
  • 長度值 (length value) 指定該段的長度
分段地址還包括兩個組件 —— 段選擇器 (segment selector) 和段內偏移量 (offset into the segment)。段選擇器指定了要使用的段(即基址和長度值),而段內偏移量組件則指定了實際內存位置相對於基址的偏移量。實際內存位置的物理地址就是這個基址值與偏移量之和。若是偏移量超過了段的長度,系統就會生成一個保護違例錯誤。
上述內容可小結以下:
分段單元能夠表示成 -> 段: 偏移量 模型 也也可表示成 -> 段標識符: 偏移量
每一個段都是一個 16 位的字段,稱爲段標識符 (segment identifier) 或段選擇器 (segment selector)。x86 硬件包括幾個可編程的寄存器,稱爲 段寄存器 (segment register),段選擇器保存於其中。這些寄存器爲 cs(代碼段)、ds(數據段)和 ss(堆棧段)。每一個段標識符都表明一個使用 64 位(8 個字節)的段描述符 (segment descriptor) 表示的段。這些段描述符能夠存儲在一個 GDT(全局描述符表,global descriptor table)中,也能夠存儲在一個 LDT(本地描述符表,local descriptor table)中。
圖 2. 段描述符和段寄存器的相互關係
每次將段選擇器加載到段寄存器中時,對應的段描述符都會從內存加載到相匹配的不可編程 CPU 寄存器中。每一個段描述符長 8 個字節,表示內存中的一個段。這些都存儲到 LDT 或 GDT 中。段描述符條目中包含一個指針和一個 20 位的值(Limit 字段),前者指向由 Base 字段表示的相關段中的第一個字節,後者表示內存中段的大小。
其餘某些字段還包含一些特殊屬性,例如優先級和段的類型(cs 或 ds)。段的類型是由一個 4 位的 Type 字段表示的。
因爲咱們使用了不可編程寄存器,所以在將邏輯地址轉換成線性地址時不引用 GDT 或 LDT。這樣能夠加快內存地址的轉換速度。
段選擇器包含如下內容:
  • 一個 13 位的索引,用來標識 GDT 或 LDT 中包含的對應段描述符條目
  • TI (Table Indicator) 標誌指定段描述符是在 GDT 中仍是在 LDT 中,若是該值是 0,段描述符就在 GDT 中;若是該值是 1,段描述符就在 LDT 中。
  • RPL (request privilege level) 定義了在將對應的段選擇器加載到段寄存器中時 CPU 的當前特權級別。
因爲一個段描述符的大小是 8 個字節,所以它在 GDT 或 LDT 中的相對地址能夠這樣計算:段選擇器的高 13 位乘以 8。例如,若是 GDT 存儲在地址 0x00020000 處,而段選擇器的 Index 域是 2,那麼對應的段描述符的地址就等於 (2*8) + 0x00020000。GDT 中能夠存儲的段描述符的總數等於 (2^13 - 1),即 8191。
圖 3 展現了從邏輯地址得到線性地址。
圖 3. 從邏輯地址得到線性地址
那麼這在 Linux 環境下有什麼不一樣呢?
Linux 中的段控制單元
Linux 對這個模型稍微進行了修改。我注意到 Linux 以一種受限的方法來使用這種分段模型(主要是出於兼容性方面的考慮)。
在 Linux 中,全部的段寄存器都指向相同的段地址範圍 —— 換言之,每一個段寄存器都使用相同的線性地址。這使 Linux 所用的段描述符數量受限,從而可將全部描述符都保存在 GDT 之中。這種模型有兩個優勢:
  • 當全部的進程都使用相同的段寄存器值時(當它們共享相同的線性地址空間時),內存管理更爲簡單。
  • 在大部分架構上均可以實現可移植性。某些 RISC 處理器也可經過這種受限的方式支持分段。
圖 4 展現了對模型的修改。
圖 4. 在 Linux 中,段寄存器指向相同的地址集
段描述符
Linux 使用如下段描述符:
  • 內核代碼段
  • 內核數據段
  • 用戶代碼段
  • 用戶數據段
  • TSS 段
  • 默認 LDT 段
下面詳細介紹這些段寄存器。
GDT 中的內核代碼段 (kernel code segment) 描述符中的值以下:
  • Base = 0x00000000
  • Limit = 0xffffffff (2^32 -1) = 4GB
  • G(粒度標誌)= 1,表示段的大小是以頁爲單位表示的
  • S = 1,表示普通代碼或數據段
  • Type = 0xa,表示能夠讀取或執行的代碼段
  • DPL 值 = 0,表示內核模式
與這個段相關的線性地址是 4 GB,S = 1 和 type = 0xa 表示代碼段。選擇器在 cs 寄存器中。Linux 中用來訪問這個段選擇器的宏是_KERNEL_CS。
內核數據段 (kernel data segment) 描述符的值與內核代碼段的值相似,唯一不一樣的就是 Type 字段值爲 2。這表示此段爲數據段,選擇器存儲在ds 寄存器中。Linux 中用來訪問這個段選擇器的宏是 _KERNEL_DS。
用戶代碼段 (user code segment) 由處於用戶模式中的全部進程共享。存儲在 GDT 中的對應段描述符的值以下:
  • Base = 0x00000000
  • Limit = 0xffffffff
  • G = 1
  • S = 1
  • Type = 0xa,表示能夠讀取和執行的代碼段
  • DPL = 3,表示用戶模式
在 Linux 中,咱們能夠經過 _USER_CS 宏來訪問此段選擇器。
在 用戶數據段 (user data segment) 描述符中,唯一不一樣的字段就是 Type,它被設置爲 2,表示將此數據段定義爲可讀取和寫入。Linux 中用來訪問此段選擇器的宏是 _USER_DS。
除了這些段描述符以外,GDT 還包含了另外兩個用於每一個建立的進程的段描述符 —— TSS 和 LDT 段。
每一個 TSS 段 (TSS segment) 描述符都表明一個不一樣的進程。TSS 中保存了每一個 CPU 的硬件上下文信息,它有助於有效地切換上下文。例如,在 U->K 模式的切換中,x86 CPU 就是從 TSS 中獲取內核模式堆棧的地址。
每一個進程都有本身在 GDT 中存儲的對應進程的 TSS 描述符。這些描述符的值以下:
  • Base = &tss (對應進程描述符的 TSS 字段的地址;例如 &tss_struct)這是在 Linux 內核的 schedule.h 文件中定義的
  • Limit = 0xeb (TSS 段的大小是 236 字節)
  • Type = 9 或 11
  • DPL = 0。用戶模式不能訪問 TSS。G 標誌被清除
全部進程共享默認 LDT 段。默認狀況下,其中會包含一個空的段描述符。這個默認 LDT 段描述符存儲在 GDT 中。Linux 所生成的 LDT 的大小是 24 個字節。默認有 3 個條目:
計算任務
要計算 GDT 中最多能夠存儲多少條目,必須先理解 NR_TASKS(這個變量決定了 Linux 可支持的併發進程數 —— 內核源代碼中的默認值是 512,最多容許有 256 個到同一實例的併發鏈接)。
GDT 中可存儲的條目總數可經過如下公式肯定:
在這 8192 個段描述符中,Linux 要使用 6 個段描述符,另外還有 4 個描述符將用於 APM 特性(高級電源管理特性),在 GDT 中還有 4 個條目保留未用。所以,GDT 中的條目數等於 8192 - 14,也就是 8180。
任何狀況下,GDT 中的條目數 8180,所以:
2 * NR_TASKS = 8180 
NR_TASKS = 8180/2 = 4090
(爲何使用 2 * NR_TASKS?由於對於所建立的每一個進程,都不只要加載一個 TSS 描述符 —— 用來維護上下文切換的內容,另外還要加載一個 LDT 描述符。)
這種 x86 架構中進程數量的限制是 Linux 2.2 中的一個組件,但自 2.4 版的內核開始,這個問題已經不存在了,部分緣由是使用了硬件上下文切換(這不可避免地要使用 TSS),並將其替換爲進程切換。
接下來,讓咱們瞭解一下分頁模型。
分頁模型概述
分頁單元負責將線性地址轉換成物理地址(請參見圖 1)。線性地址會被分組成頁的形式。這些線性地址實際上都是連續的 —— 分頁單元將這些連續的內存映射成對應的連續物理地址範圍(稱爲 頁框)。注意,分頁單元會直觀地將 RAM 劃分紅固定大小的頁框。
正因如此,分頁具備如下優勢:
  • 爲一個頁定義的訪問權限中保存了構成該頁的整組線性地址的權限
  • 頁的大小等於頁框的大小
將這些頁映射成頁框的數據結構稱爲頁表 (page table)。頁表存儲在主存儲器中,可由內核在啓用分頁單元以前對其進行恰當的初始化。圖 5 展現了頁表。
圖 5. 頁表將頁轉換成頁框
注意,上圖 Page1 中包含的地址集正好與 Page Frame1 中包含的地址集匹配。
在 Linux 中,分頁單元的使用多於分段單元。前面介紹 Linux 分段模型時已提到,每一個分段描述符都使用相同的地址集進行線性尋址,從而儘量下降使用分段單元將邏輯地址轉換成線性地址的須要。經過更多地使用分頁單元而非分段單元,Linux 能夠極大地促進內存管理及其在不一樣硬件平臺之間的可移植性。
分頁過程當中使用的字段
下面讓咱們來介紹一下用於在 x86 架構中指定分頁的字段,這些字段有助於在 Linux 中實現分頁功能。分頁單元進入做爲分段單元輸出結果的線性字段,而後進一步將其劃分紅如下 3 個字段:
  • Directory 以 10 MSB 表示(Most Significant Bit,也就是二進制數字中值最大的位的位置 —— MSB 有時稱爲最左位)。
  • Table 以中間的 10 位表示。
  • Offset 以 12 LSB 表示。(Least Significant Bit,也就是二進制整數中給定單元值的位的位置,即肯定這個數字是奇數仍是偶數。LSB 有時稱爲最右位。這與數字權重最輕的數字相似,它是最右邊位置處的數字。)
線性地址到對應物理位置的轉換的過程包含兩個步驟。第一步使用了一個稱爲頁目錄 (Page Directory) 的轉換表(從頁目錄轉換成頁表),第二步使用了一個稱爲頁表 (Page Table) 的轉換表(即頁表加偏移量再加頁框)。圖 6 展現了此過程。
圖 6. 分頁字段
開始時,首先將頁目錄的物理地址加載到 cr3 寄存器中。線性地址中的 Directory 字段肯定頁目錄中指向恰當的頁表條目。Table 字段中的地址肯定包含頁的頁框物理地址所在頁表中的條目。Offset 字段肯定了頁框中的相對位置。因爲 Offset 字段爲 12 位,所以每一個頁中都包含有 4 KB 數據。
下面小結物理地址的計算:
  1. cr3 + Page Directory (10 MSB) = 指向 table_base
  2. table_base + Page Table (10 中間位) = 指向 page_base
  3. page_base + Offset = 物理地址 (得到頁框)
因爲 Page Directory 字段和 Page Table 段都是 10 位,所以其可尋址上限爲 1024*1024 KB,Offset 可尋址的範圍最大爲 2^12(4096 字節)。所以,頁目錄的可尋址上限爲 1024*1024*4096(等於 2^32 個內存單元,即 4 GB)。所以在 x86 架構上,總可尋址上限是 4 GB。
擴展分頁
擴展分頁是經過刪除頁錶轉換表實現的;此後線性地址的劃分便可在頁目錄 (10 MSB) 和偏移量 (22 LSB) 之間完成了。
22 LSB 構成了頁框的 4 MB 邊界(2^22)。擴展分頁能夠與普通的分頁模型一塊兒使用,並可用於將大型的連續線性地址映射爲對應的物理地址。操做系統中刪除頁表以提供擴展頁表。這能夠經過設置 PSE (page size extension) 實現。
36 位的 PSE 擴展了 36 位的物理地址,能夠支持 4 MB 頁,同時維護一個 4 字節的頁目錄條目,這樣就能夠提供一種對超過 4 GB 的物理內存進行尋址的方法,而不須要對操做系統進行太大的修改。這種方法對於按需分頁來講具備一些實際的限制。
Linux 中的分頁模型
雖然 Linux 中的分頁與普通的分頁相似,可是 x86 架構引入了一種三級頁表機制,包括:
  • 頁全局目錄 (Page Global Directory),即 pgd,是多級頁表的抽象最高層。每一級的頁表都處理不一樣大小的內存 —— 這個全局目錄能夠處理 4 MB 的區域。每項都指向一個更小目錄的低級表,所以 pgd 就是一個頁表目錄。當代碼遍歷這個結構時(有些驅動程序就要這樣作),就稱爲是在「遍歷」頁表。
  • 頁中間目錄 (Page Middle Directory),即 pmd,是頁表的中間層。在 x86 架構上,pmd 在硬件中並不存在,可是在內核代碼中它是與 pgd 合併在一塊兒的。
  • 頁表條目 (Page Table Entry),即 pte,是頁表的最低層,它直接處理頁(參看 PAGE_SIZE),該值包含某頁的物理地址,還包含了說明該條目是否有效及相關頁是否在物理內存中的位。
爲了支持大內存區域,Linux 也採用了這種三級分頁機制。在不須要爲大內存區域時,便可將 pmd 定義成「1」,返回兩級分頁機制。
分頁級別是在編譯時進行優化的,咱們能夠經過啓用或禁用中間目錄來啓用兩級和三級分頁(使用相同的代碼)。32 位處理器使用的是 pmd 分頁,而 64 位處理器使用的是 pgd 分頁。
圖 7. 三級分頁
如您所知,在 64 位處理器中:
  • 21 MSB 保留未用
  • 13 LSB 由頁面偏移量表示
  • 其他的 30 位分爲:
  • 10 位用於頁表
  • 10 位用於頁全局目錄
  • 10 位用於頁中間目錄
咱們能夠從架構中看到,實際上使用了 43 位進行尋址。所以在 64 位處理器中,能夠有效使用的內存是 2 的 43 次方。
每一個進程都有本身的頁目錄和頁表。爲了引用一個包含實際用戶數據的頁框,操做系統(在 x86 架構上)首先將 pgd 加載到 cr3 寄存器中。Linux 將 cr3 寄存器的內容存儲到 TSS 段中。此後只要在 CPU 上執行新進程,就從 TSS 段中將另一個值加載到 cr3 寄存器中。從而使分頁單元引用一組正確的頁表。
pgd 表中的每一條目都指向一個頁框,其中中包含了一組 pmd 條目;pdm 表中的每一個條目又指向一個頁框,其中包含一組 pte 條目;pde 表中的每一個條目再指向一個頁框,其中包含的是用戶數據。若是正在查找的頁已轉出,那麼就會在 pte 表中存儲一個交換條目,(在缺頁的狀況下)以定位將哪一個頁框從新加載到內存中。
圖 8 說明咱們連續爲各級頁表添加偏移量來映射對應的頁框條目。咱們經過進入做爲分段單元輸出的線性地址,再劃分該地址來得到偏移量。要將線性地址劃分紅對應的每一個頁表元素,須要在內核中使用不一樣的宏。本文不詳細介紹這些宏,下面咱們經過圖 8 來簡單看一下線性地址的劃分方式。
圖 8. 具備不一樣地址長度的線性地址
預留頁框
Linux 爲內核代碼和數據結構預留了幾個頁框。這些頁永遠不會 被轉出到磁盤上。從 0x0 到 0xc0000000 (PAGE_OFFSET) 的線性地址可由用戶代碼和內核代碼進行引用。從 PAGE_OFFSET 到 0xffffffff 的線性地址只能由內核代碼進行訪問。
這意味着在 4 GB 的內存空間中,只有 3 GB 能夠用於用戶應用程序。
如何啓用分頁
Linux 進程使用的分頁機制包括兩個階段:
  • 在啓動時,系統爲 8 MB 的物理內存設置頁表。
  • 而後,第二個階段完成對其他物理地址的映射。
在啓動階段,startup_32() 調用負責對分頁機制進行初始化。這是在 arch/i386/kernel/head.S 文件中實現的。這 8 MB 的映射發生在PAGE_OFFSET 之上的地址中。這種初始化是經過一個靜態定義的編譯時數組 (swapper_pg_dir) 開始的。在編譯時它被放到一個特定的地址(0x00101000)。
這種操做爲在代碼中靜態定義的兩個頁 —— pg0 和 pg1 —— 創建頁表。這些頁框的大小默認爲 4 KB,除非咱們設置了頁大小擴展位(有關 PSE 的更多內容,請參閱  擴展分頁 一節)。這個全局數組所指向的數據地址存儲在 cr3 寄存器中,我認爲這是爲 Linux 進程設置分頁單元的第一階段。其他的頁項是在第二階段中完成的。
第二階段由方法調用 paging_init() 來完成。
在 32 位的 x86 架構上,RAM 映射到 PAGE_OFFSET 和由 4GB 上限 (0xFFFFFFFF) 表示的地址之間。這意味着大約有 1 GB 的 RAM 能夠在 Linux 啓動時進行映射,這種操做是默認進行的。然而,若是有人設置了 HIGHMEM_CONFIG,那麼就能夠將超過 1 GB 的內存映射到內核上 —— 切記這是一種臨時的安排。能夠經過調用 kmap() 實現。
物理內存區域
我已經向您展現了(32 位架構上的) Linux 內核按照 3:1 的比率來劃分虛擬內存:3 GB 的虛擬內存用於用戶空間,1 GB 的內存用於內核空間。內核代碼及其數據結構都必須位於這 1 GB 的地址空間中,可是對於此地址空間而言,更大的消費者是物理地址的虛擬映射。
之因此出現這種問題,是由於若一段內存沒有映射到本身的地址空間中,那麼內核就不能操做這段內存。所以,內核能夠處理的最大內存總量就是能夠映射到內核的虛擬地址空間減去須要映射到內核代碼自己上的空間。結果,一個基於 x86 的 Linux 系統最大可使用略低於 1 GB 的物理內存。
爲了迎合大量用戶的須要,支持更多內存、提升性能,並創建一種獨立於架構的內存描述方法,Linux 內存模型就必須進行改進。爲了實現這些目標,新模型將內存劃分紅分配給每一個 CPU 的空間。每一個空間都稱爲一個 節點;每一個節點都被劃分紅一些 區域。區域(表示內存中的範圍)能夠進一步劃分爲如下類型:
  • ZONE_DMA(0-16 MB):包含 ISA/PCI 設備須要的低端物理內存區域中的內存範圍。
  • ZONE_NORMAL(16-896 MB):由內核直接映射到高端範圍的物理內存的內存範圍。全部的內核操做都只能使用這個內存區域來進行,所以這是對性能相當重要的區域。
  • ZONE_HIGHMEM(896 MB 以及更高的內存):系統中內核不能映像到的其餘可用內存。
節點的概念在內核中是使用 struct pglist_data 結構來實現的。區域是使用 struct zone_struct 結構來描述的。物理頁框是使用struct Page 結構來表示的,全部這些 Struct 都保存在全局結構數組 struct mem_map 中,這個數組存儲在 NORMAL_ZONE 的開頭。節點、區域和頁框之間的基本關係如圖 9 所示。
圖 9. 節點、區域和頁框之間的關係
當實現了對 Pentium II 的虛擬內存擴展的支持(在 32 位系統上使用 PAE —— Physical Address Extension —— 能夠訪問 64 GB 的內存)和對 4 GB 的物理內存(一樣是在 32 位系統上)的支持時,高端內存區域就會出如今內核內存管理中了。這是在 x86 和 SPARC 平臺上引用的一個概念。一般這 4 GB 的內存能夠經過使用 kmap() 將 ZONE_HIGHMEM 映射到 ZONE_NORMAL 來進行訪問。請注意在 32 位的架構上使用超過 16 GB 的內存是不明智的,即便啓用了 PAE 也是如此。
(PAE 是 Intel 提供的內存地址擴展機制,它經過在宿主操做系統中使用 Address Windowing Extensions API 爲應用程序提供支持,從而讓處理器將能夠用來尋址物理內存的位數從 32 位擴展爲 36 位。)
這個物理內存區域的管理是經過一個 區域分配器(zone allocator) 實現的。它負責將內存劃分爲不少區域;它能夠將每一個區域做爲一個分配單元使用。每一個特定的分配請求都利用了一組區域,內核能夠從這些位置按照從高到低的順序來進行分配。
例如:
  • 對於某個用戶頁面的請求能夠首先從「普通」區域中來知足(ZONE_NORMAL);
  • 若是失敗,就從 ZONE_HIGHMEM 開始嘗試;
  • 若是這也失敗了,就從 ZONE_DMA 開始嘗試。
這種分配的區域列表依次包括 ZONE_NORMAL、ZONE_HIGHMEM 和 ZONE_DMA 區域。另外一方面,對於 DMA 頁的請求可能只能從 DMA 區域中獲得知足,所以這種請求的區域列表就只包含 DMA 區域。
結束語
內存管理是一組很是龐大、複雜且耗時的任務,也是一個很是難以實現的任務,由於咱們須要精雕細琢出一個模型,設計好系統如何在真實的多程序的環境中進行操做,這是一項很是艱難的工做。諸如調度、分頁行爲和多進程的交互組件都向咱們提出了至關難度的挑戰。我但願本文能夠幫助您瞭解接受 Linux 內存管理挑戰所須要的一些基本知識,併爲您提供一個起點。
 
 
1 物理地址,MMU相關概念
Intel X86存在IO空間,相對於內存空間,經過IN OUT指令訪問。大多數ARM PowerPC僅有內存空間。內存空間經過地址,指針訪問。程序,程序運行中使用的變量都在內存空間。
物理地址
unsigned char *p = (unsigned char*) 0xF000FF00; *p = 1;
    • 0xF000FF00這個地址對於x86是16bit段地址+16bit偏移地址,即,0xF000 * 16 + 0xFF00 = 0xF0000 + 0xFF00 = 0xFFF00 
      地址對於ARM等爲採用段地址的處理器,就是空間0xF000FF00。
      x86處理器用實際地址作第一步跳轉(軟重啓):
      typedef void (*lpFunction) ();//define a function pointer type lpFunction lpReset = (lpFunction)0xF000FFF0; //get a pointer that point to the addr lpRest(); //go to the function at addr
      • MMU 
        memory management unit,輔助內存管理,提供虛擬和物理地址映射、內存訪問權限保護、Cache緩存控制。 
        Kernel藉助MMU讓用戶感受可使用很大的內存空間,而讓開發者在寫程序的時候能夠不考慮物理實際容量。
        TLB:Translation Lookaside Buffer,轉換旁路緩存。是MMU的核心部件,緩存少許的虛擬–物理關係,是轉換表的Cache,也稱爲「快表」。 
        TTW:Translation Table walk,轉換表漫遊。當TLB沒有須要的轉換對,經過內存中的轉換表(經常是多級頁表,從頁表記地址寄存器找到頁表,一層一層直到代碼頁。)訪問獲得虛擬–物理關係,TTW成功就寫入TLB。 
        寫入以後若是權限正確將訪問Cache或者內存找到相應的數據。若是不容許,MMU會向ARM發送一個存儲器異常。
        Linux三級頁表
        • PGD,Page Global Directory (頁目錄);
        • PMD,Page Middle Directory (頁目錄); 
        *前二者內部的成爲PDE,頁目錄項,Page Directory Entry。
        • PTE,Page Table Entry (頁表項,每個表項對應一個物理頁)。
        相關宏可見下圖: 
        通常由虛擬地址三級查詢獲得PTE的頁表的過程(page table walk):
        • 有描述進程佔有資源的和須要訪問的虛擬地址經過獲得一級頁表入口
        • 經過獲得二級頁表入口
        • 經過獲得目標頁表項
        更多詳細可查看reference [1]
        注:Linux 2.6支持不帶MMU的處理器。其爲了兼容嵌入式系統,融合了uClinux,來支持MMU-Less系統。
        2 Linux內存管理
        包含MMU的處理器可使進程的訪問空間達到4G,0-3G是User Space,3-4G是Kernel Space。爲3G末尾,即0x86的。 
        每一個進程有本身的頁表,相互獨立。內核空間由內核負責映射,固定不隨進程變化。而1G的Kernel Space劃分爲:
        • 物理內存映射區(0-896MB),線性映射,常規內存。當物理內存大於896MB,超出部分稱爲高端內存。
        • 896MB以後的區域: 
        • vmalloc分配器區(先後有隔離帶,地址VMALLOC_START ~ VMALLOC_END)
        • 高端內存映射區(高端內存只能以映射在這裏)(PKMAP_BASE) 更多關於 高端內存
        • 專用頁面映射區(實際中FIXADDR_START ~ FIXADDR_TOP)這部分須要配置。
        • 保留區域(實際中FIXADDR_TOP ~ 4G區域)
        當內存超過4G,須要使用CPU擴展分頁(PAE)模式提供的64bit也目錄項才能訪問到更高物理內存,須要CPU支持。
        3 內存存取
        內存申請
        下面會提到的內存申請:
        • malloc - free: 用戶空間
        • kmalloc - kfree: 內核空間,物理連續
        • __get_free_pages - free_pages: 內核空間,物理連續
        • vmalloc - vfree: 內核空間,物理不連續,虛擬連續
        • slab: kmem_cache_create - kmem_cache_destory
        1 用戶空間內存動態申請 
        申請的空間在heap上,須要申請者用釋放。注意儘可能成對出現,避免內存泄漏。注:C Linux的malloc經常使用和系統調用實現。
        2 內核空間內存動態申請
        kmalloc() 
        申請內存位於物理內存映射區,物理上也連續。和真實物理地址只有一個固定的offset。 
        size是大小,flag是標誌,GFP_KERNEL表示在內核空間進程中申請內存。其底層依賴實現。使用這個flag後,若是不能知足,進程會睡眠等待頁,可能會引發阻塞。所以不能在++中斷上下文,spin lock++中使用GFP_KERNEL申請內存。 
        在++中斷處理函數,tasklet,內核定時器++非進程上下文不能阻塞,應該用GFP_ATOMIC申請內存,不存在空閒會直接返回。 
        相應其餘標誌位定義於:include/linux/gfp.h。 
        釋放空間。
        __get_free_pages() 
        Linux Kernel最底層使用的獲取空間的方法。底層以page的2^n爲單位管理空閒內存,因此內存頁的申請是以page爲單位。 
        指向一個清零的新page。 
        指向新頁但不清零,其實是用了order爲0的下一個函數。 
        獲取多個pages數量是2^order,不清零。order最大是10或11,硬件相關。 
        前面三個函數的實現實際上是調用了,該函數能夠在用戶空間,也能夠在內核空間使用。返回。 
        釋放: 
         
         特別注意order先後要一致。
        vmalloc() 
         
        在虛擬空間獲得一塊連續區域,在vmalloc專用區,物理內存不必定連續。用於較大的順序緩衝區分配內存,開銷遠大於GFP,新的頁表要被創建。若是是用它申請少許內存,是不妥的。 
        釋放slab 
        以page爲單位容易產生內部碎片(internal fragmentation)。同時設想若是能讓先後兩次相同對象的分配在同一塊內存,並且已經保留的數據結構,就能提升效率。獲得slab概念,駐留任意數目,一樣大小的後背緩存。 
        建立:
        struct kmem_cache *kmem_cache_create( const char *name, size_t size, //size爲每一個等大結構的大小byte size_t align, unsigned long flags, viod (*ctor)(void*, struct kmem_cache *, unsigned long), void (*dtor)(void*, struct kmem_cache *, unsigned long));`
        • 分配slab緩存:
          void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags); //在以前分配的slab中分出一塊並返回首指針
          • 釋放slab緩存:
            void kmem_cache_free(struct kmem_cache *cachep, void *objp);
            • 回收整個slab:
              int kmem_cache_destroy(struct kmem_cache *cachep);
              • 能夠獲知slab的分配使用狀況。
                注意slab底層也依賴於,只是分割了小單元減小內部碎片方便管理。
                內存池 
                也是用與分配大量小對象的後背緩存技術。 
                相關函數有,,,。
                虛擬地址和物理地址
                使用實現內核虛擬向物理地址的轉化,函數實現和體系結構相關。物理向虛擬。僅僅適用於常規內存區域。

                reference 
                [2] 高端內存, http://ilinuxkernel.com/?p=1013 
                notification
                source: 《Linux設備驅動開發詳解》(第二版),內容爲讀書筆記和網絡資料,有些資料原始來源不詳,分享爲了方便本身和他人查閱。若有侵權請及時告知,對於帶來的不便很是抱歉。轉載請註明來源。Terrence Zhou.
相關文章
相關標籤/搜索