Linux Kernel:內存分頁機制核心原理
內存分頁機制是一種操作系統的內存管理技術,將進程的虛擬內存空間劃分爲固定大小的頁面,並在需要時將其映射到物理內存中。操作系統會將輔助存儲器(通常是磁盤)中的數據分區成固定大小的區塊,稱爲 “頁”。當不需要時,將分頁由主存(通常是內存)移到輔助存儲器;當需要時,再將數據取回,加載主存中。相對於分段,分頁允許存儲器存儲於不連續的區塊以維持文件系統的整齊。分頁是磁盤和內存間傳輸數據塊的最小單位。
分頁 / 虛擬內存能有助 “大大地” 降低整體及額外非必要的 I/O 次數,提高系統整體運作性能。因爲這能有助提高 RAM 的讀取命中率,也透過其內部的高效率算法來達到 I/O 數據流的預緩存工作,通過與之相關的等等手段也能很好地提高了 CPU 的使用效率,而擁有大物理內存的用戶更可能考慮利用如 Ramdisk、Supercache、SoftPerfect RAM Disk 等模擬出硬盤分區來同時將虛擬內存 / 系統臨時文件等設置其上以進一步加強系統性能,及達至保障硬盤的措施。分頁是虛擬內存技術中的重要部分。
一、分頁機制介紹
(1) 什麼是分頁機制
分頁機制是 80x86 內存管理機制的第二部分。它在分段機制的基礎上完成虛擬地址到物理地址的轉換過程。分段機制把邏輯地址轉換成線性地址,而分頁機制則把線性地址轉換成物理地址。分頁機制可用於任何一種分段模型。處理器分頁機制會把線性地址空間劃分成頁面,然後這些線性地址空間頁面被映射到物理地址空間的頁面上。分頁機制的幾種頁面級保護措施,可和分段機制保護措施和用或替代分段機制的保護措施。
(2) 分頁機制如何啓用
在我們進行程序開發的時候,一般情況下,是不需要管理內存的,也不需要操心內存夠不夠用,其實,這就是分頁機制給我們帶來的好處。它是實現虛擬存儲的關鍵,位於線性地址與物理地址之間,在使用這種內存分頁管理方法時,每個執行中的進程(任務)可以使用比實際內存容量大得多的連續地址空間。而且當系統內存實際上被分成很多凌亂的塊時,它可以建立一個大而連續的內存空間的映象,好讓程序不用操心和管理這些分散的內存塊。分頁機制增強了分段機制的性能。頁地址變換是建立在段變換基礎之上的。因爲,段管理機制對於 Intel 處理器來說是最基本的,任何時候都無法關閉。所以即使啓用了頁管理功能,分段機制依然是起作用的,段部件也依然工作。
分頁只能在保護模式(CR0.PE = 1)下使用。在保護模式下,是否開啓分頁,由 CR0. PG 位(位 31)決定:
-
當 CR0.PG = 0 時,未開啓分頁,線性地址等同於物理地址;
-
當 CR0.PG = 1 時,開啓分頁。
(3) 分頁機制線性地址到物理地址轉換過程
80x86 使用 4K 字節固定大小的頁面,每個頁面均是 4KB,並且對其於 4K 地址邊界處。這表示分頁機制把 2^32 字節 (4GB) 的線性地址空間劃分成 2^20(1M = 1048576)個頁面。分頁機制通過把線性地址空間中的頁面重新定位到物理地址空間中進行操作。由於 4K 大小的頁面作爲一個單元進行映射,並且對其於 4K 邊界,因此線性地址的低 12 位可做爲頁內偏移地量直接作爲物理地址的低 12 位。分頁機制執行的重定向功能可以看作是把線性地址的高 20 位轉換到對應物理地址的高 20 位。
線性到物理地址轉換功能,被擴展成允許一個線性地址被標註爲無效的,而非要讓其產生一個物理地址。以下兩種情況一個頁面可以被標註爲無效的:
-
- 操作系統不支持的線性地址。
-
- 對應的虛擬內存系統中的頁面在磁盤上而非在物理內存中。
在第一中情況下,產生無效地址的程序必須被終止,在第二種情況下,該無效地址實際上是請求 操作系統虛擬內存管理器 把對應的頁面從磁盤加載到物理內存中,以供程序訪問。因爲無效頁面通常與虛擬存儲系統相關,因此它們被稱爲不存在頁面,由頁表中稱爲存在的屬性來確定。
當使用分頁時,處理器會把線性地址空間劃分成固定大小的頁面(4KB),這些頁面可以映射到物理內存中或磁盤存儲空間中,當一個程序引用內存中的邏輯地址時,處理器會把該邏輯地址轉換成一個線性地址,然後使用分頁機制把該線性地址轉換成對應的物理地址。
如果包含線性地址的頁面不在當前物理內存中,處理器就會產生一個頁錯誤異常。頁錯誤異常處理程序就會讓操作系統從磁盤中把相應頁面加載到物理內存中(操作過程中可能會把物理內存中不同的頁面寫到磁盤上)。當頁面加載到物理內存之後,從異常處理過程的返回操作會使異常的指令被重新執行。處理器把用於線性地址轉換成物理地址和用於產生頁錯誤的信息包含在存儲與內存中的頁目錄與頁表中。
(4) 分頁機制與分段機制的不同
分頁與分段的最大的不同之處在於分頁使用了固定長度的頁面。段的長度通常與存放在其中的代碼或數據結構有相同的長度。與段不同,頁面有固定的長度。如果僅使用分段地址轉換,那麼存儲在物理內存中的一個數據結構將包含其所有的部分。如果使用了分頁,那麼一個數據結構就可以一部分存儲與物理內存中,而另一部分保存在磁盤中。
爲了減少地址轉換所要求的總線週期數量,最近訪問的頁目錄和頁表會被存放在處理器的一個叫做轉換查找緩衝區(TLB)的緩衝器件中。TLB 可以滿足大多數讀頁目錄和頁表的請求而無需使用總線週期。只有當 TLB 中不包含所要求的頁表項是纔會出現使用額外的總線週期從內存讀取頁表項。通常在一個頁表項很長時間沒有訪問過時纔會出現這種情況。
1.1 四級分頁機制
前面我們提到 Linux 內核僅使用了較少的分段機制,但是卻對分頁機制的依賴性很強,其使用一種適合 32 位和 64 位結構的通用分頁模型,該模型使用四級分頁機制,即
-
頁全局目錄(Page Global Directory)
-
頁上級目錄(Page Upper Directory)
-
頁中間目錄(Page Middle Directory)
-
頁表(Page Table)
頁全局目錄包含若干頁上級目錄的地址;
-
頁上級目錄又依次包含若干頁中間目錄的地址;
-
而頁中間目錄又包含若干頁表的地址;
-
每一個頁表項指向一個頁框。
因此線性地址因此被分成五個部分,而每一部分的大小與具體的計算機體系結構有關。
1.2 不同架構的分頁機制
對於不同的體系結構,Linux 採用的四級頁表目錄的大小有所不同:對於 i386 而言,僅採用二級頁表,即頁上層目錄和頁中層目錄長度爲 0;對於啓用 PAE 的 i386,採用了三級頁表,即頁上層目錄長度爲 0;對於 64 位體系結構,可以採用三級或四級頁表,具體選擇由硬件決定。
對於沒有啓用物理地址擴展的 32 位系統,兩級頁表已經足夠了。從本質上說 Linux 通過使 “頁上級目錄” 位和 “頁中間目錄” 位全爲 0,徹底取消了頁上級目錄和頁中間目錄字段。不過,頁上級目錄和頁中間目錄在指針序列中的位置被保留,以便同樣的代碼在 32 位系統和 64 位系統下都能使用。內核爲頁上級目錄和頁中間目錄保留了一個位置,這是通過把它們的頁目錄項數設置爲 1,並把這兩個目錄項映射到頁全局目錄的一個合適的目錄項而實現的。
啓用了物理地址擴展的 32 位系統使用了三級頁表。Linux 的頁全局目錄對應 80x86 的頁目錄指針表(PDPT),取消了頁上級目錄,頁中間目錄對應 80x86 的頁目錄,Linux 的頁表對應 80x86 的頁表。
最終,64 位系統使用三級還是四級分頁取決於硬件對線性地址的位的劃分。
爲什麼 linux 熱衷:分頁 > 分段
那麼,爲什麼 Linux 是如此地熱衷使用分頁技術而對分段機制表現得那麼地冷淡呢,因爲 Linux 的進程處理很大程度上依賴於分頁。事實上,線性地址到物理地址的自動轉換使下面的設計目標變得可行:
給每一個進程分配一塊不同的物理地址空間,這確保了可以有效地防止尋址錯誤。
區別頁(即一組數據)和頁框(即主存中的物理地址)之不同。這就允許存放在某個頁框中的一個頁,然後保存到磁盤上,以後重新裝入這同一頁時又被裝在不同的頁框中。這就是虛擬內存機制的基本要素。
每一個進程有它自己的頁全局目錄和自己的頁表集。當發生進程切換時,Linux 把 cr3 控制寄存器的內容保存在前一個執行進程的描述符中,然後把下一個要執行進程的描述符的值裝入 cr3 寄存器中。因此,當新進程重新開始在 CPU 上執行時,分頁單元指向一組正確的頁表。
把線性地址映射到物理地址雖然有點複雜,但現在已經成了一種機械式的任務。
二、linux 中頁表處理數據結構
分頁轉換功能由駐留在內存中的表來描述,該表稱爲頁表,存放在物理地址空間中。頁表可以看作是簡單的 2^20 物理地址數組。線性到物理地址的映射功能可以簡單地看作進行數組查找。線性地址的高 20 位構成這個數組的索引值,用於選擇對應頁面的物理(基)地址。線性地址的低 12 位給出了頁面中的偏移量,加上頁面的基地址最終形成對應的物理地址。由於頁面基地址對齊在 4K 邊界上,因此頁面基地址的低 12 爲肯定是 0 ,這意味着 高 20 位的頁面基地址 和 12 位偏移地址連接組合在一起就能得到對應的物理地址。
頁表中每個頁表項 大小爲 32 位,由於只需其中的 20 位來存放頁面的物理基地址,因此剩下的 12 位可用於存放諸如頁面是否存在等的屬性信息。如果線性地址索引的頁表被標註爲存在,則表示該項有效, 我們可以從中取得頁面的物理地址。如果項中表明不存在,那麼噹噹訪問對應物理界面時就會產生一個異常。
2.1 頁表類型定義
(1)pgd_t、pmd_t、pud_t 和 pte_t
Linux 分別採用 pgd_t、pmd_t、pud_t 和 pte_t 四種數據結構來表示頁全局目錄項、頁上級目錄項、頁中間目錄項和頁表項。這四種 數據結構本質上都是無符號長整型 unsigned long
Linux 爲了更嚴格數據類型檢查,將無符號長整型 unsigned long 分別封裝成四種不同的頁表項。如果不採用這種方法,那麼一個無符號長整型數據可以傳入任何一個與四種頁表相關的函數或宏中,這將大大降低程序的健壯性。
pgprot_t 是另一個 64 位(PAE 激活時)或 32 位(PAE 禁用時)的數據類型,它表示與一個單獨表項相關的保護標誌。
首先我們查看一下子這些類型是如何定義的
(1)pteval_t,pmdval_t,pudval_t,pgdval_t
參照 arch/x86/include/asm/pgtable_64_types.h
#ifndef __ASSEMBLY__
#include <linux/types.h>
/*
* These are used to make use of C type-checking..
*/
typedef unsigned long pteval_t;
typedef unsigned long pmdval_t;
typedef unsigned long pudval_t;
typedef unsigned long pgdval_t;
typedef unsigned long pgprotval_t;
typedef struct { pteval_t pte; } pte_t;
#endif /* !__ASSEMBLY__ */
(2)pgd_t、pmd_t、pud_t 和 pte_t
參照 /arch/x86/include/asm/pgtable_types.h
typedef struct { pgdval_t pgd; } pgd_t;
static inline pgd_t native_make_pgd(pgdval_t val)
{
return (pgd_t) { val };
}
static inline pgdval_t native_pgd_val(pgd_t pgd)
{
return pgd.pgd;
}
static inline pgdval_t pgd_flags(pgd_t pgd)
{
return native_pgd_val(pgd) & PTE_FLAGS_MASK;
}
#if CONFIG_PGTABLE_LEVELS > 3
typedef struct { pudval_t pud; } pud_t;
static inline pud_t native_make_pud(pmdval_t val)
{
return (pud_t) { val };
}
static inline pudval_t native_pud_val(pud_t pud)
{
return pud.pud;
}
#else
#include <asm-generic/pgtable-nopud.h>
static inline pudval_t native_pud_val(pud_t pud)
{
return native_pgd_val(pud.pgd);
}
#endif
#if CONFIG_PGTABLE_LEVELS > 2
typedef struct { pmdval_t pmd; } pmd_t;
static inline pmd_t native_make_pmd(pmdval_t val)
{
return (pmd_t) { val };
}
static inline pmdval_t native_pmd_val(pmd_t pmd)
{
return pmd.pmd;
}
#else
#include <asm-generic/pgtable-nopmd.h>
static inline pmdval_t native_pmd_val(pmd_t pmd)
{
return native_pgd_val(pmd.pud.pgd);
}
#endif
static inline pudval_t pud_pfn_mask(pud_t pud)
{
if (native_pud_val(pud) & _PAGE_PSE)
return PHYSICAL_PUD_PAGE_MASK;
else
return PTE_PFN_MASK;
}
static inline pudval_t pud_flags_mask(pud_t pud)
{
return ~pud_pfn_mask(pud);
}
static inline pudval_t pud_flags(pud_t pud)
{
return native_pud_val(pud) & pud_flags_mask(pud);
}
static inline pmdval_t pmd_pfn_mask(pmd_t pmd)
{
if (native_pmd_val(pmd) & _PAGE_PSE)
return PHYSICAL_PMD_PAGE_MASK;
else
return PTE_PFN_MASK;
}
static inline pmdval_t pmd_flags_mask(pmd_t pmd)
{
return ~pmd_pfn_mask(pmd);
}
static inline pmdval_t pmd_flags(pmd_t pmd)
{
return native_pmd_val(pmd) & pmd_flags_mask(pmd);
}
static inline pte_t native_make_pte(pteval_t val)
{
return (pte_t) { .pte = val };
}
static inline pteval_t native_pte_val(pte_t pte)
{
return pte.pte;
}
static inline pteval_t pte_flags(pte_t pte)
{
return native_pte_val(pte) & PTE_FLAGS_MASK;
}
(3)xxx_val 和__xxx
參照 / arch/x86/include/asm/pgtable.h
五個類型轉換宏(_ pte、_ pmd、_ pud、_ pgd 和__ pgprot)把一個無符號整數轉換成所需的類型。
另外的五個類型轉換宏(pte_val,pmd_val, pud_val, pgd_val 和 pgprot_val)執行相反的轉換,即把上面提到的四種特殊的類型轉換成一個無符號整數。
#define pgd_val(x) native_pgd_val(x)
#define __pgd(x) native_make_pgd(x)
#ifndef __PAGETABLE_PUD_FOLDED
#define pud_val(x) native_pud_val(x)
#define __pud(x) native_make_pud(x)
#endif
#ifndef __PAGETABLE_PMD_FOLDED
#define pmd_val(x) native_pmd_val(x)
#define __pmd(x) native_make_pmd(x)
#endif
#define pte_val(x) native_pte_val(x)
#define __pte(x) native_make_pte(x)
這裏需要區別指向頁表項的指針和頁表項所代表的數據。以 pgd_t 類型爲例子,如果已知一個 pgd_t 類型的指針 pgd,那麼通過 pgd_val(*pgd) 即可獲得該頁表項 (也就是一個無符號長整型數據),這裏利用了面向對象的思想。
2.2 頁表描述宏
參照 arch/x86/include/asm/pgtable_64
linux 中使用下列宏簡化了頁表處理,對於每一級頁表都使用有以下三個關鍵描述宏:
我們的四級頁表,對應的宏分別由 PAGE,PMD,PUD,PGDIR
1.PAGE 宏–頁表 (Page Table)
定義如下,在/arch/x86/include/asm/page_types.h
文件中
/* PAGE_SHIFT determines the page size */
#define PAGE_SHIFT 12
#define PAGE_SIZE (_AC(1,UL) << PAGE_SHIFT)
#define PAGE_MASK (~(PAGE_SIZE-1))
當用於 80x86 處理器時,PAGE_SHIFT 返回的值爲 12,由於頁內所有地址都必須放在 Offset 字段, 因此 80x86 系統的頁的大小 PAGE_SIZE 是2^12=4096
字節,PAGE_MASK 宏產生的值爲 0xfffff000,用以屏蔽 Offset 字段的所有位。
2.PMD-Page Middle Directory (頁目錄)
當 PAE 被禁用時,PMD_SHIFT 產生的值爲 22(來自 Offset 的 12 位加上來自 Table 的 10 位),PMD_SIZE 產生的值爲 222 或 4 MB,PMD_MASK 產生的值爲 0xffc00000。
相反,當 PAE 被激活時,PMD_SHIFT 產生的值爲 21 (來自 Offset 的 12 位加上來自 Table 的 9 位),PMD_SIZE 產生的值爲 2^21 或 2 MB PMD_MASK 產生的值爲 0xffe00000。
大型頁不使用最後一級頁表,所以產生大型頁尺寸的 LARGE_PAGE_SIZE 宏等於 PMD_SIZE(2PMD_SHIFT),而在大型頁地址中用於屏蔽 Offset 字段和 Table 字段的所有位的 LARGE_PAGE_MASK 宏,就等於 PMD_MASK。
3.PUD_SHIFT - 頁上級目錄 (Page Upper Directory)
在 80x86 處理器上,PUD_SHIFT 總是等價於 PMD_SHIFT,而 PUD_SIZE 則等於 4MB 或 2MB。
4.PGDIR_SHIFT - 頁全局目錄 (Page Global Directory)
當 PAE 被禁止時,PGDIR_SHIFT 產生的值爲 22(與 PMD_SHIFT 和 PUD_SHIFT 產生的值相同),PGDIR_SIZE 產生的值爲 222 或 4 MB,PGDIR_MASK 產生的值爲 0xffc00000。
相反,當 PAE 被激活時,PGDIR_SHIFT 產生的值爲 30 (12 位 Offset 加 9 位 Table 再加 9 位 Middle Air),PGDIR_SIZE 產生的值爲 230 或 1 GBPGDIR_MASK 產生的值爲 0xc0000000
PTRS_PER_PTE, PTRS_PER_PMD, PTRS_PER_PUD 以及 PTRS_PER_PGD 用於計算頁表、頁中間目錄、頁上級目錄和頁全局目錄表中表項的個數。當 PAE 被禁止時,它們產生的值分別爲 1024,1,1 和 1024。當 PAE 被激活時,產生的值分別爲 512,512,1 和 4。
2.3 頁表處理函數
內核還提供了許多宏和函數用於讀或修改頁表表項:
-
如果相應的表項值爲 0,那麼,宏 pte_none、pmd_none、pud_none 和 pgd_none 產生的值爲 1,否則產生的值爲 0。
-
宏 pte_clear、pmd_clear、pud_clear 和 pgd_clear 清除相應頁表的一個表項,由此禁止進程使用由該頁表項映射的線性地址。ptep_get_and_clear( ) 函數清除一個頁表項並返回前一個值。
-
set_pte,set_pmd,set_pud 和 set_pgd 向一個頁表項中寫入指定的值。set_pte_atomic 與 set_pte 作用相同,但是當 PAE 被激活時它同樣能保證 64 位的值能被原子地寫入。
-
如果 a 和 b 兩個頁表項指向同一頁並且指定相同訪問優先級,pte_same(a,b) 返回 1,否則返回 0。
-
如果頁中間目錄項指向一個大型頁(2MB 或 4MB),pmd_large(e) 返回 1,否則返回 0。
宏 pmd_bad 由函數使用並通過輸入參數傳遞來檢查頁中間目錄項。如果目錄項指向一個不能使用的頁表,也就是說,如果至少出現以下條件中的一個,則這個宏產生的值爲 1:
-
頁不在主存中(Present 標誌被清除)。
-
頁只允許讀訪問 (Read/Write 標誌被清除)。
-
Acessed 或者 Dirty 位被清除(對於每個現有的頁表,Linux 總是
強制設置這些標誌)。
pud_bad 宏和 pgd_bad 宏總是產生 0。沒有定義 pte_bad 宏,因爲頁表項引用一個不在主存中的頁,一個不可寫的頁或一個根本無法訪問的頁都是合法的。
如果一個頁表項的 Present 標誌或者 Page Size 標誌等於 1,則 pte_present 宏產生的值爲 1,否則爲 0。
前面講過頁表項的 Page Size 標誌對微處理器的分頁部件來講沒有意義,然而,對於當前在主存中卻又沒有讀、寫或執行權限的頁,內核將其 Present 和 Page Size 分別標記爲 0 和 1。
這樣,任何試圖對此類頁的訪問都會引起一個缺頁異常,因爲頁的 Present 標誌被清 0,而內核可以通過檢查 Page Size 的值來檢測到產生異常並不是因爲缺頁。
如果相應表項的 Present 標誌等於 1,也就是說,如果對應的頁或頁表被裝載入主存,pmd_present 宏產生的值爲 1。pud_present 宏和 pgd_present 宏產生的值總是 1。
(1) 查詢頁表項中任意一個標誌的當前值
下表中列出的函數用來查詢頁表項中任意一個標誌的當前值;除了 pte_file() 外,其他函數只有在 pte_present 返回 1 的時候,才能正常返回頁表項中任意一個標誌。
-
pte_user( ):讀 User/Supervisor 標誌
-
pte_read( ):讀 User/Supervisor 標誌(表示 80x86 處理器上的頁不受讀的保護)
-
pte_write( ):讀 Read/Write 標誌
-
pte_exec( ):讀 User/Supervisor 標誌( 80x86 處理器上的頁不受代碼執行的保護)
-
pte_dirty( ):讀 Dirty 標誌
-
pte_young( ):讀 Accessed 標誌
-
pte_file( ):讀 Dirty 標誌(當 Present 標誌被清除而 Dirty 標誌被設置時,頁屬於一個非線性磁盤文件映射)
(2) 設置頁表項中各標誌的值
下表列出的另一組函數用於設置頁表項中各標誌的值
-
mk_pte_huge( ):設置頁表項中的 Page Size 和 Present 標誌
-
pte_wrprotect( ):清除 Read/Write 標誌
-
pte_rdprotect( ):清除 User/Supervisor 標誌
-
pte_exprotect( ):清除 User/Supervisor 標誌
-
pte_mkwrite( ):設置 Read/Write 標誌
-
pte_mkread( ):設置 User/Supervisor 標誌
-
pte_mkexec( ):設置 User/Supervisor 標誌
-
pte_mkclean( ):清除 Dirty 標誌
-
pte_mkdirty( ):設置 Dirty 標誌
-
pte_mkold( ):清除 Accessed 標誌(把此頁標記爲未訪問)
-
pte_mkyoung( ):設置 Accessed 標誌(把此頁標記爲訪問過)
-
pte_modify(p,v):把頁表項 p 的所有訪問權限設置爲指定的值
-
ptep_set_wrprotect():與 pte_wrprotect() 類似,但作用於指向頁表項的指針
-
ptep_set_access_flags():如果 Dirty 標誌被設置爲 1 則將頁的訪問權設置爲指定的值,並調用 flush_tlb_page() 函數 ptep_mkdirty() 與 pte_mkdirty( ) 類似,但作用於指向頁表項的指針。
-
ptep_test_and_clear_dirty():與 pte_mkclean() 類似,但作用於指向頁表項的指針並返回 Dirty 標誌的舊值
-
ptep_test_and_clear_young():與 pte_mkold() 類似,但作用於指向頁表項的指針並返回 Accessed 標誌的舊值
(3) 宏函數 - 把一個頁地址和一組保護標誌組合成頁表項,或者執行相反的操作
現在,我們來討論下表中列出的宏,它們把一個頁地址和一組保護標誌組合成頁表項,或者執行相反的操作,從一個頁表項中提取出頁地址。請注意這其中的一些宏對頁的引用是通過 “頁描述符” 的線性地址,而不是通過該頁本身的線性地址。
-
pgd_index(addr):找到線性地址 addr 對應的的目錄項在頁全局目錄中的索引(相對位置)
-
pgd_offset(mm, addr):接收內存描述符地址 mm 和線性地址 addr 作爲參數。這個宏產生地址 addr 在頁全局目錄中相應表項的線性地址;通過內存描述符 mm 內的一個指針可以找到這個頁全局目錄 pgd_offset_k(addr) 產生主內核頁全局目錄中的某個項的線性地址,該項對應於地址
-
addrpgd_page(pgd):通過頁全局目錄項 pgd 產生頁上級目錄所在頁框的頁描述符地址。在兩級或三級分頁系統中,該宏等價於 pud_page() ,後者應用於頁上級目錄項
-
pud_offset(pgd, addr):參數爲指向頁全局目錄項的指針 pgd 和線性地址 addr 。這個宏產生頁上級目錄中目錄項 addr 對應的線性地址。在兩級或三級分頁系統中,該宏產生 pgd ,即一個頁全局目錄項的地址
-
pud_page(pud):通過頁上級目錄項 pud 產生相應的頁中間目錄的線性地址。在兩級分頁系統中,該宏等價於 pmd_page() ,後者應用於頁中間目錄項
-
pmd_index(addr):產生線性地址 addr 在頁中間目錄中所對應目錄項的索引(相對位置)
-
pmd_offset(pud, addr):接收指向頁上級目錄項的指針 pud 和線性地址 addr 作爲參數。這個宏產生目錄項 addr 在頁中間目錄中的偏移地址。在兩級或三級分頁系統中,它產生 pud ,即頁全局目錄項的地址
-
pmd_page(pmd):通過頁中間目錄項 pmd 產生相應頁表的頁描述符地址。在兩級或三級分頁系統中, pmd 實際上是頁全局目錄中的一項 mk_pte(p,prot) 接收頁描述符地址 p 和一組訪問權限 prot 作爲參數,並創建相應的頁表項
-
pte_index(addr):產生線性地址 addr 對應的表項在頁表中的索引(相對位置)
-
pte_offset_kernel(dir,addr):線性地址 addr 在頁中間目錄 dir 中有一個對應的項,該宏就產生這個對應項,即頁表的線性地址。另外,該宏只在主內核頁表上使用
-
pte_offset_map(dir, addr):接收指向一個頁中間目錄項的指針 dir 和線性地址 addr 作爲參數,它產生與線性地址 addr 相對應的頁表項的線性地址。如果頁表被保存在高端存儲器中,那麼內核建立一個臨時內核映射,並用 pte_unmap 對它進行釋放。pte_offset_map_nested 宏和 pte_unmap_nested 宏是相同的,但它們使用不同的臨時內核映射
-
pte_page(x):返回頁表項 x 所引用頁的描述符地址
-
pte_to_pgoff(pte):從一個頁表項的 pte 字段內容中提取出文件偏移量,這個偏移量對應着一個非線性文件內存映射所在的頁
-
pgoff_to_pte(offset):爲非線性文件內存映射所在的頁創建對應頁表項的內容
(4) 簡化頁表項的創建和撤消
下面我們羅列最後一組函數來簡化頁表項的創建和撤消。當使用兩級頁表時,創建或刪除一個頁中間目錄項是不重要的。如本節前部分所述,頁中間目錄僅含有一個指向下屬頁表的目錄項。所以,頁中間目錄項只是頁全局目錄中的一項而已。然而當處理頁表時,創建一個頁表項可能很複雜,因爲包含頁表項的那個頁表可能就不存在。在這樣的情況下,有必要分配一個新頁框,把它填寫爲 0 ,並把這個表項加入。
如果 PAE 被激活,內核使用三級頁表。當內核創建一個新的頁全局目錄時,同時也分配四個相應的頁中間目錄;只有當父頁全局目錄被釋放時,這四個頁中間目錄才得以釋放。當使用兩級或三級分頁時,頁上級目錄項總是被映射爲頁全局目錄中的一個單獨項。與以往一樣,下表中列出的函數描述是針對 80x86 構架的。
三、線性地址轉換
3.1 分頁模式下的的線性地址轉換
線性地址、頁表和頁表項線性地址不管系統採用多少級分頁模型,線性地址本質上都是索引 + 偏移量的形式,甚至你可以將整個線性地址看作 N+1 個索引的組合,N 是系統採用的分頁級數。在四級分頁模型下,線性地址被分爲 5 部分,如下圖:
在線性地址中,每個頁表索引即代表線性地址在對應級別的頁表中中關聯的頁表項。正是這種索引與頁表項的對應關係形成了整個頁表映射機制。
(1) 頁表
多個頁表項的集合則爲頁表,一個頁表內的所有頁表項是連續存放的。頁表本質上是一堆數據,因此也是以頁爲單位存放在主存中的。因此,在虛擬地址轉化物理物理地址的過程中,每訪問一級頁表就會訪問一次內存。
(2) 頁表項
頁表項從四種頁表項的數據結構可以看出,每個頁表項其實就是一個無符號長整型數據。每個頁表項分兩大類信息:頁框基地址和頁的屬性信息。在 x86-32 體系結構中,每個頁表項的結構圖如下:
這個圖是一個通用模型,其中頁表項的前 20 位是物理頁的基地址。由於 32 位的系統採用 4kb 大小的 頁,因此每個頁表項的後 12 位均爲 0。內核將後 12 位充分利用,每個位都表示對應虛擬頁的相關屬性。
不管是那一級的頁表,它的功能就是建立虛擬地址和物理地址之間的映射關係,一個頁和一個頁框之間的映射關係體現在頁表項中。上圖中的物理頁基地址是 個抽象的說明,如果當前的頁表項位於頁全局目錄中,這個物理頁基址是指頁上級目錄所在物理頁的基地址;如果當前頁表項位於頁表中,這個物理頁基地址是指最 終要訪問數據所在物理頁的基地址。
(3) 地址轉換過程
地址轉換過程有了上述的基本知識,就很好理解四級頁表模式下如何將虛擬地址轉化爲邏輯地址了。基本過程如下:
從 CR3 寄存器中讀取頁目錄所在物理頁面的基址 (即所謂的頁目錄基址),從線性地址的第一部分獲取頁目錄項的索引,兩者相加得到頁目錄項的物理地址。
第一次讀取內存得到 pgd_t 結構的目錄項,從中取出物理頁基址取出 (具體位數與平臺相關,如果是 32 系統,則爲 20 位),即頁上級頁目錄的物理基地址。
從線性地址的第二部分中取出頁上級目錄項的索引,與頁上級目錄基地址相加得到頁上級目錄項的物理地址。
第二次讀取內存得到 pud_t 結構的目錄項,從中取出頁中間目錄的物理基地址。
從線性地址的第三部分中取出頁中間目錄項的索引,與頁中間目錄基址相加得到頁中間目錄項的物理地址。
第三次讀取內存得到 pmd_t 結構的目錄項,從中取出頁表的物理基地址。
從線性地址的第四部分中取出頁表項的索引,與頁表基址相加得到頁表項的物理地址。
第四次讀取內存得到 pte_t 結構的目錄項,從中取出物理頁的基地址。
從線性地址的第五部分中取出物理頁內偏移量,與物理頁基址相加得到最終的物理地址。
第五次讀取內存得到最終要訪問的數據。
整個過程是比較機械的,每次轉換先獲取物理頁基地址,再從線性地址中獲取索引,合成物理地址後再訪問內存。不管是頁表還是要訪問的數據都是以頁爲單 位存放在主存中的,因此每次訪問內存時都要先獲得基址,再通過索引 (或偏移) 在頁內訪問數據,因此可以將線性地址看作是若干個索引的集合。
3.2 Linux 中通過 4 級頁表訪問物理內存
linux 中每個進程有它自己的 PGD(Page Global Directory),它是一個物理頁,幷包含一個 pgd_t 數組。
進程的 pgd_t 數據見 task_struct -> mm_struct -> pgd_t * pgd;
PTEs, PMDs 和 PGDs 分別由 pte_t, pmd_t 和 pgd_t 來描述。爲了存儲保護位,pgprot_t 被定義,它擁有相關的 flags 並經常被存儲在 page table entry 低位 (lower bits),其具體的存儲方式依賴於 CPU 架構。
前面我們講了頁表處理的大多數函數信息,在上面我們又講了線性地址如何轉換爲物理地址,其實就是不斷索引的過程。
通過如下幾個函數,不斷向下索引,就可以從進程的頁表中搜索特定地址對應的頁面對象:
-
pgd_offset 根據當前虛擬地址和當前進程的 mm_struct 獲取 pgd 項
-
pud_offset 參數爲指向頁全局目錄項的指針 pgd 和線性地址 addr 。這個宏產生頁上級目錄中目錄項 addr 對應的線性地址。在兩級或三級分頁系統中,該宏產生 pgd ,即一個頁全局目錄項的地址
-
pmd_offset 根據通過 pgd_offset 獲取的 pgd 項和虛擬地址,獲取相關的 pmd 項 (即 pte 表的起始地址)
-
pte_offset 根據通過 pmd_offset 獲取的 pmd 項和虛擬地址,獲取相關的 pte 項 (即物理頁的起始地址)
根據虛擬地址獲取物理頁的示例代碼詳見 mm/memory.c 中的函數 follow_page
不同的版本可能有所不同,早起內核中存在 follow_page,而後來的內核中被 follow_page_mask 替代,目前最新的發佈 4.4 中爲查找到此函數
我們從早期的 linux-3.8 的源代碼中, 截取的代碼如下
/**
* follow_page - look up a page descriptor from a user-virtual address
* @vma: vm_area_struct mapping @address
* @address: virtual address to look up
* @flags: flags modifying lookup behaviour
*
* @flags can have FOLL_ flags set, defined in <linux/mm.h>
*
* Returns the mapped (struct page *), %NULL if no mapping exists, or
* an error pointer if there is a mapping to something not represented
* by a page descriptor (see also vm_normal_page()).
*/
struct page *follow_page(struct vm_area_struct *vma, unsigned long address,
unsigned int flags)
{
pgd_t *pgd;
pud_t *pud;
pmd_t *pmd;
pte_t *ptep, pte;
spinlock_t *ptl;
struct page *page;
struct mm_struct *mm = vma->vm_mm;
page = follow_huge_addr(mm, address, flags & FOLL_WRITE);
if (!IS_ERR(page)) {
BUG_ON(flags & FOLL_GET);
goto out;
}
page = NULL;
pgd = pgd_offset(mm, address);
if (pgd_none(*pgd) || unlikely(pgd_bad(*pgd)))
goto no_page_table;
pud = pud_offset(pgd, address);
if (pud_none(*pud))
goto no_page_table;
if (pud_huge(*pud) && vma->vm_flags & VM_HUGETLB) {
BUG_ON(flags & FOLL_GET);
page = follow_huge_pud(mm, address, pud, flags & FOLL_WRITE);
goto out;
}
if (unlikely(pud_bad(*pud)))
goto no_page_table;
pmd = pmd_offset(pud, address);
if (pmd_none(*pmd))
goto no_page_table;
if (pmd_huge(*pmd) && vma->vm_flags & VM_HUGETLB) {
BUG_ON(flags & FOLL_GET);
page = follow_huge_pmd(mm, address, pmd, flags & FOLL_WRITE);
goto out;
}
if (pmd_trans_huge(*pmd)) {
if (flags & FOLL_SPLIT) {
split_huge_page_pmd(mm, pmd);
goto split_fallthrough;
}
spin_lock(&mm->page_table_lock);
if (likely(pmd_trans_huge(*pmd))) {
if (unlikely(pmd_trans_splitting(*pmd))) {
spin_unlock(&mm->page_table_lock);
wait_split_huge_page(vma->anon_vma, pmd);
} else {
page = follow_trans_huge_pmd(mm, address,
pmd, flags);
spin_unlock(&mm->page_table_lock);
goto out;
}
} else
spin_unlock(&mm->page_table_lock);
/* fall through */
}
split_fallthrough:
if (unlikely(pmd_bad(*pmd)))
goto no_page_table;
ptep = pte_offset_map_lock(mm, pmd, address, &ptl);
pte = *ptep;
if (!pte_present(pte))
goto no_page;
if ((flags & FOLL_WRITE) && !pte_write(pte))
goto unlock;
page = vm_normal_page(vma, address, pte);
if (unlikely(!page)) {
if ((flags & FOLL_DUMP) ||
!is_zero_pfn(pte_pfn(pte)))
goto bad_page;
page = pte_page(pte);
}
if (flags & FOLL_GET)
get_page(page);
if (flags & FOLL_TOUCH) {
if ((flags & FOLL_WRITE) &&
!pte_dirty(pte) && !PageDirty(page))
set_page_dirty(page);
/*
* pte_mkyoung() would be more correct here, but atomic care
* is needed to avoid losing the dirty bit: it is easier to use
* mark_page_accessed().
*/
mark_page_accessed(page);
}
if ((flags & FOLL_MLOCK) && (vma->vm_flags & VM_LOCKED)) {
/*
* The preliminary mapping check is mainly to avoid the
* pointless overhead of lock_page on the ZERO_PAGE
* which might bounce very badly if there is contention.
*
* If the page is already locked, we don't need to
* handle it now - vmscan will handle it later if and
* when it attempts to reclaim the page.
*/
if (page->mapping && trylock_page(page)) {
lru_add_drain(); /* push cached pages to LRU */
/*
* Because we lock page here and migration is
* blocked by the pte's page reference, we need
* only check for file-cache page truncation.
*/
if (page->mapping)
mlock_vma_page(page);
unlock_page(page);
}
}
unlock:
pte_unmap_unlock(ptep, ptl);
out:
return page;
bad_page:
pte_unmap_unlock(ptep, ptl);
return ERR_PTR(-EFAULT);
no_page:
pte_unmap_unlock(ptep, ptl);
if (!pte_none(pte))
return page;
no_page_table:
/*
* When core dumping an enormous anonymous area that nobody
* has touched so far, we don't want to allocate unnecessary pages or
* page tables. Return error instead of NULL to skip handle_mm_fault,
* then get_dump_page() will return NULL to leave a hole in the dump.
* But we can only make this optimization where a hole would surely
* be zero-filled if handle_mm_fault() actually did handle it.
*/
if ((flags & FOLL_DUMP) &&
(!vma->vm_ops || !vma->vm_ops->fault))
return ERR_PTR(-EFAULT);
return page;
}
以上代碼可以精簡爲:
unsigned long v2p(int pid unsigned long va)
{
unsigned long pa = 0;
struct task_struct *pcb_tmp = NULL;
pgd_t *pgd_tmp = NULL;
pud_t *pud_tmp = NULL;
pmd_t *pmd_tmp = NULL;
pte_t *pte_tmp = NULL;
printk(KERN_INFO"PAGE_OFFSET = 0x%lx\n",PAGE_OFFSET);
printk(KERN_INFO"PGDIR_SHIFT = %d\n",PGDIR_SHIFT);
printk(KERN_INFO"PUD_SHIFT = %d\n",PUD_SHIFT);
printk(KERN_INFO"PMD_SHIFT = %d\n",PMD_SHIFT);
printk(KERN_INFO"PAGE_SHIFT = %d\n",PAGE_SHIFT);
printk(KERN_INFO"PTRS_PER_PGD = %d\n",PTRS_PER_PGD);
printk(KERN_INFO"PTRS_PER_PUD = %d\n",PTRS_PER_PUD);
printk(KERN_INFO"PTRS_PER_PMD = %d\n",PTRS_PER_PMD);
printk(KERN_INFO"PTRS_PER_PTE = %d\n",PTRS_PER_PTE);
printk(KERN_INFO"PAGE_MASK = 0x%lx\n",PAGE_MASK);
//if(!(pcb_tmp = find_task_by_pid(pid)))
if(!(pcb_tmp = findTaskByPid(pid)))
{
printk(KERN_INFO"Can't find the task %d .\n",pid);
return 0;
}
printk(KERN_INFO"pgd = 0x%p\n",pcb_tmp->mm->pgd);
/* 判斷給出的地址va是否合法(va<vm_end)*/
if(!find_vma(pcb_tmp->mm,va))
{
printk(KERN_INFO"virt_addr 0x%lx not available.\n",va);
return 0;
}
pgd_tmp = pgd_offset(pcb_tmp->mm,va);
printk(KERN_INFO"pgd_tmp = 0x%p\n",pgd_tmp);
printk(KERN_INFO"pgd_val(*pgd_tmp) = 0x%lx\n",pgd_val(*pgd_tmp));
if(pgd_none(*pgd_tmp))
{
printk(KERN_INFO"Not mapped in pgd.\n");
return 0;
}
pud_tmp = pud_offset(pgd_tmp,va);
printk(KERN_INFO"pud_tmp = 0x%p\n",pud_tmp);
printk(KERN_INFO"pud_val(*pud_tmp) = 0x%lx\n",pud_val(*pud_tmp));
if(pud_none(*pud_tmp))
{
printk(KERN_INFO"Not mapped in pud.\n");
return 0;
}
pmd_tmp = pmd_offset(pud_tmp,va);
printk(KERN_INFO"pmd_tmp = 0x%p\n",pmd_tmp);
printk(KERN_INFO"pmd_val(*pmd_tmp) = 0x%lx\n",pmd_val(*pmd_tmp));
if(pmd_none(*pmd_tmp))
{
printk(KERN_INFO"Not mapped in pmd.\n");
return 0;
}
/*在這裏,把原來的pte_offset_map()改成了pte_offset_kernel*/
pte_tmp = pte_offset_kernel(pmd_tmp,va);
printk(KERN_INFO"pte_tmp = 0x%p\n",pte_tmp);
printk(KERN_INFO"pte_val(*pte_tmp) = 0x%lx\n",pte_val(*pte_tmp));
if(pte_none(*pte_tmp))
{
printk(KERN_INFO"Not mapped in pte.\n");
return 0;
}
if(!pte_present(*pte_tmp)){
printk(KERN_INFO"pte not in RAM.\n");
return 0;
}
pa = (pte_val(*pte_tmp) & PAGE_MASK) | (va & ~PAGE_MASK);
printk(KERN_INFO"virt_addr 0x%lx in RAM is 0x%lx t .\n",va,pa);
printk(KERN_INFO"contect in 0x%lx is 0x%lx\n", pa, *(unsigned long *)((char *)pa + PAGE_OFFSET)
}
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/vOC5zLtBgVeyIrmAIrlu8w