Linux內(nèi)存分頁機制:解鎖高效內(nèi)存管理方式
在Linux操作系統(tǒng)復雜而精妙的架構(gòu)體系中,內(nèi)存管理堪稱其中的核心與關鍵。想象一下,系統(tǒng)如同一個繁忙的大型工廠,內(nèi)存則是工廠里至關重要的原材料倉庫,各個進程就像不同的生產(chǎn)線,它們都對內(nèi)存資源有著迫切的需求。如何高效地管理這些內(nèi)存,讓每一條生產(chǎn)線都能順暢運轉(zhuǎn),避免資源浪費與沖突,成了決定系統(tǒng)整體性能的關鍵因素。
在早期,分段機制曾在操作系統(tǒng)的內(nèi)存管理領域占據(jù)主導地位。但隨著技術的飛速發(fā)展,新的挑戰(zhàn)不斷涌現(xiàn),內(nèi)存碎片問題日益凸顯,就如同倉庫中的原材料被零散放置,難以被高效取用,這嚴重影響了內(nèi)存的使用效率與系統(tǒng)性能。為了應對這一難題,分頁機制應運而生,它如同一位聰明的倉庫管理員,重新規(guī)劃了內(nèi)存的存儲方式,將內(nèi)存分割成固定大小的小片,也就是內(nèi)存頁,以此來提升內(nèi)存的空間利用率,逐漸成為現(xiàn)代操作系統(tǒng)內(nèi)存管理的主流選擇。今天,就讓我們一同深入探索 Linux 內(nèi)存分頁機制的奧秘,解鎖這一高效內(nèi)存管理方式背后的關鍵技術 。
一、內(nèi)存分頁機制
在 Linux 的世界里,內(nèi)存分頁機制就像是一位有條不紊的大管家,精心管理著系統(tǒng)的內(nèi)存資源。簡單來說,內(nèi)存分頁機制就是把物理內(nèi)存和虛擬內(nèi)存分割成固定大小的小塊,這些小塊被稱作 “頁” ,每個頁的大小一般為 4KB 或者 8KB。就好比你有一個巨大的倉庫(內(nèi)存),為了更好地管理里面的貨物(數(shù)據(jù)),你把倉庫劃分成了一個個大小相同的小隔間(頁)。
1.1什么是分頁機制
分頁機制是 80x86 內(nèi)存管理機制的第二部分。它在分段機制的基礎上完成虛擬地址到物理地址的轉(zhuǎn)換過程。分段機制把邏輯地址轉(zhuǎn)換成線性地址,而分頁機制則把線性地址轉(zhuǎn)換成物理地址。分頁機制可用于任何一種分段模型。處理器分頁機制會把線性地址空間劃分成頁面,然后這些線性地址空間頁面被映射到物理地址空間的頁面上。分頁機制的幾種頁面級保護措施,可和分段機制保護措施或用或替代分段機制的保護措施。
1.2分頁機制如何啟用
在我們進行程序開發(fā)的時候,一般情況下,是不需要管理內(nèi)存的,也不需要操心內(nèi)存夠不夠用,其實,這就是分頁機制給我們帶來的好處。它是實現(xiàn)虛擬存儲的關鍵,位于線性地址與物理地址之間,在使用這種內(nèi)存分頁管理方法時,每個執(zhí)行中的進程(任務)可以使用比實際內(nèi)存容量大得多的連續(xù)地址空間。而且當系統(tǒng)內(nèi)存實際上被分成很多凌亂的塊時,它可以建立一個大而連續(xù)的內(nèi)存空間的映象,好讓程序不用操心和管理這些分散的內(nèi)存塊。分頁機制增強了分段機制的性能。頁地址變換是建立在段變換基礎之上的。因為,段管理機制對于Intel處理器來說是最基本的,任何時候都無法關閉。所以即使啟用了頁管理功能,分段機制依然是起作用的,段部件也依然工作。
分頁只能在保護模式(CR0.PE = 1)下使用。在保護模式下,是否開啟分頁,由 CR0. PG 位(位 31)決定:
- 當 CR0.PG = 0 時,未開啟分頁,線性地址等同于物理地址;
- 當 CR0.PG = 1 時,開啟分頁。
1.3分頁機制線性地址到物理地址轉(zhuǎn)換過程
80x86使用 4K 字節(jié)固定大小的頁面,每個頁面均是 4KB,并且對其于 4K 地址邊界處。這表示分頁機制把 2^32字節(jié)(4GB)的線性地址空間劃分成 2^20(1M = 1048576)個頁面。分頁機制通過把線性地址空間中的頁面重新定位到物理地址空間中進行操作。由于 4K 大小的頁面作為一個單元進行映射,并且對其于 4K 邊界,因此線性地址的低 12 位可做為頁內(nèi)偏移地量直接作為物理地址的低 12 位。分頁機制執(zhí)行的重定向功能可以看作是把線性地址的高 20 位轉(zhuǎn)換到對應物理地址的高 20 位。
線性到物理地址轉(zhuǎn)換功能,被擴展成允許一個線性地址被標注為無效的,而非要讓其產(chǎn)生一個物理地址。以下兩種情況一個頁面可以被標注為無效的:
1. 操作系統(tǒng)不支持的線性地址。
2. 對應的虛擬內(nèi)存系統(tǒng)中的頁面在磁盤上而非在物理內(nèi)存中。
在第一中情況下,產(chǎn)生無效地址的程序必須被終止,在第二種情況下,該無效地址實際上是請求 操作系統(tǒng)虛擬內(nèi)存管理器 把對應的頁面從磁盤加載到物理內(nèi)存中,以供程序訪問。因為無效頁面通常與虛擬存儲系統(tǒng)相關,因此它們被稱為不存在頁面,由頁表中稱為存在的屬性來確定。
當使用分頁時,處理器會把線性地址空間劃分成固定大小的頁面(4KB),這些頁面可以映射到物理內(nèi)存中或磁盤存儲空間中,當一個程序引用內(nèi)存中的邏輯地址時,處理器會把該邏輯地址轉(zhuǎn)換成一個線性地址,然后使用分頁機制把該線性地址轉(zhuǎn)換成對應的物理地址。
如果包含線性地址的頁面不在當前物理內(nèi)存中,處理器就會產(chǎn)生一個頁錯誤異常。頁錯誤異常處理程序就會讓操作系統(tǒng)從磁盤中把相應頁面加載到物理內(nèi)存中(操作過程中可能會把物理內(nèi)存中不同的頁面寫到磁盤上)。當頁面加載到物理內(nèi)存之后,從異常處理過程的返回操作會使異常的指令被重新執(zhí)行。處理器把用于線性地址轉(zhuǎn)換成物理地址和用于產(chǎn)生頁錯誤的信息包含在存儲與內(nèi)存中的頁目錄與頁表中。
1.4分頁機制與分段機制的不同
分頁與分段的最大的不同之處在于分頁使用了固定長度的頁面。段的長度通常與存放在其中的代碼或數(shù)據(jù)結(jié)構(gòu)有相同的長度。與段不同,頁面有固定的長度。如果僅使用分段地址轉(zhuǎn)換,那么存儲在物理內(nèi)存中的一個數(shù)據(jù)結(jié)構(gòu)將包含其所有的部分。如果使用了分頁,那么一個數(shù)據(jù)結(jié)構(gòu)就可以一部分存儲與物理內(nèi)存中,而另一部分保存在磁盤中。
為了減少地址轉(zhuǎn)換所要求的總線周期數(shù)量,最近訪問的頁目錄和頁表會被存放在處理器的一個叫做轉(zhuǎn)換查找緩沖區(qū)(TLB)的緩沖器件中。TLB 可以滿足大多數(shù)讀頁目錄和頁表的請求而無需使用總線周期。只有當 TLB 中不包含所要求的頁表項是才會出現(xiàn)使用額外的總線周期從內(nèi)存讀取頁表項。通常在一個頁表項很長時間沒有訪問過時才會出現(xiàn)這種情況。
二、分頁機制類型
2.1四級分頁機制
前面我們提到Linux內(nèi)核僅使用了較少的分段機制,但是卻對分頁機制的依賴性很強,其使用一種適合32位和64位結(jié)構(gòu)的通用分頁模型,該模型使用四級分頁機制,即
- 頁全局目錄(Page Global Directory)
- 頁上級目錄(Page Upper Directory)
- 頁中間目錄(Page Middle Directory)
- 頁表(Page Table)
頁全局目錄包含若干頁上級目錄的地址:
- 頁上級目錄又依次包含若干頁中間目錄的地址;
- 而頁中間目錄又包含若干頁表的地址;
- 每一個頁表項指向一個頁框。
因此線性地址因此被分成五個部分,而每一部分的大小與具體的計算機體系結(jié)構(gòu)有關。
2.2不同架構(gòu)的分頁機制
對于不同的體系結(jié)構(gòu),Linux采用的四級頁表目錄的大小有所不同:對于i386而言,僅采用二級頁表,即頁上層目錄和頁中層目錄長度為0;對于啟用PAE的i386,采用了三級頁表,即頁上層目錄長度為0;對于64位體系結(jié)構(gòu),可以采用三級或四級頁表,具體選擇由硬件決定。
對于沒有啟用物理地址擴展的32位系統(tǒng),兩級頁表已經(jīng)足夠了。從本質(zhì)上說Linux通過使“頁上級目錄”位和“頁中間目錄”位全為0,徹底取消了頁上級目錄和頁中間目錄字段。不過,頁上級目錄和頁中間目錄在指針序列中的位置被保留,以便同樣的代碼在32位系統(tǒng)和64位系統(tǒng)下都能使用。內(nèi)核為頁上級目錄和頁中間目錄保留了一個位置,這是通過把它們的頁目錄項數(shù)設置為1,并把這兩個目錄項映射到頁全局目錄的一個合適的目錄項而實現(xiàn)的。
啟用了物理地址擴展的32 位系統(tǒng)使用了三級頁表。Linux 的頁全局目錄對應80x86 的頁目錄指針表(PDPT),取消了頁上級目錄,頁中間目錄對應80x86的頁目錄,Linux的頁表對應80x86的頁表。
最終,64位系統(tǒng)使用三級還是四級分頁取決于硬件對線性地址的位的劃分。
為什么linux熱衷:分頁>分段
那么,為什么Linux是如此地熱衷使用分頁技術而對分段機制表現(xiàn)得那么地冷淡呢,因為Linux的進程處理很大程度上依賴于分頁。事實上,線性地址到物理地址的自動轉(zhuǎn)換使下面的設計目標變得可行:
給每一個進程分配一塊不同的物理地址空間,這確保了可以有效地防止尋址錯誤。
區(qū)別頁(即一組數(shù)據(jù))和頁框(即主存中的物理地址)之不同。這就允許存放在某個頁框中的一個頁,然后保存到磁盤上,以后重新裝入這同一頁時又被裝在不同的頁框中。這就是虛擬內(nèi)存機制的基本要素。
每一個進程有它自己的頁全局目錄和自己的頁表集。當發(fā)生進程切換時,Linux把cr3控制寄存器的內(nèi)容保存在前一個執(zhí)行進程的描述符中,然后把下一個要執(zhí)行進程的描述符的值裝入cr3寄存器中。因此,當新進程重新開始在CPU上執(zhí)行時,分頁單元指向一組正確的頁表。
把線性地址映射到物理地址雖然有點復雜,但現(xiàn)在已經(jīng)成了一種機械式的任務。
三、分頁機制的工作原理
3.1分頁技術核心思想
分頁技術的核心思想,是把虛擬地址空間和物理內(nèi)存都劃分成固定大小的頁,然后通過頁表來建立虛擬頁到物理頁框的映射關系。打個比方,你有一本厚厚的字典(虛擬地址空間),為了快速找到某個字(數(shù)據(jù)),你給字典的每一頁(虛擬頁)都編了號,同時在另一張紙上(頁表)記錄了每個編號對應的實際頁碼(物理頁框)。
以 x86_64 架構(gòu)的 4 級頁表結(jié)構(gòu)為例,虛擬地址被分成了多個部分 。其中,[47-39] 位表示頁全局目錄(PGD),[38-30] 位表示上層頁目錄(PUD),[29-21] 位表示中間頁目錄(PMD),[20-12] 位表示頁表項(PTE),最后的 [11-0] 位則是頁內(nèi)偏移 。CPU 在訪問內(nèi)存時,會先通過 CR3 寄存器找到頁全局目錄,然后根據(jù)虛擬地址中的 PGD 部分找到對應的上層頁目錄,依此類推,逐級查詢,最終找到物理頁框號,再結(jié)合頁內(nèi)偏移,就能得到準確的物理地址。這個過程就像是你按照索引在多層書架上找一本書,每一層索引都能幫你縮小查找范圍,最終準確找到目標書籍。
3.2多級頁表設計動機與優(yōu)勢
在早期的內(nèi)存管理中,曾采用過單級頁表,即通過一個頁表直接將虛擬地址映射到物理地址。然而,這種方式存在明顯的弊端。隨著虛擬地址空間的不斷增大,例如在 64 位系統(tǒng)中,若使用單級頁表來映射所有虛擬地址,所需的頁表空間將變得極為龐大。以 48 位地址空間為例,若每個頁表項大小為 8 字節(jié),計算可知單級頁表需要 256TB 的內(nèi)存空間,這在實際應用中幾乎是不可行的,不僅會占用大量的內(nèi)存資源,而且管理和維護如此巨大的頁表也會面臨諸多困難。
為了解決單級頁表的空間占用問題,多級頁表應運而生。多級頁表的設計采用了稀疏存儲的策略,它并不是為整個虛擬地址空間都分配頁表,而是僅在需要時才分配實際使用的頁表項。這樣一來,大大節(jié)省了內(nèi)存空間。
以 Linux 的四級頁表為例,當一個進程只使用 1GB 內(nèi)存時,若采用四級頁表,其總大小約為 (1GB/2MB × 4 × 8B = 16KB) 。這是因為在四級頁表結(jié)構(gòu)中,每一級頁表都起到了篩選和定位的作用。當進程訪問某個虛擬地址時,首先通過 CR3 寄存器找到頁全局目錄(PGD),PGD 根據(jù)虛擬地址中的相應部分,找到上層頁目錄(PUD),若該 PUD 對應的頁表項存在且有效,則繼續(xù)通過 PUD 找到中間頁目錄(PMD),依此類推。如果在某一級發(fā)現(xiàn)對應的頁表項不存在,說明該虛擬地址尚未被映射到物理內(nèi)存,系統(tǒng)會根據(jù)需要動態(tài)分配頁表項,而不是預先分配整個頁表。
這種按需分配的方式,使得頁表僅占用實際使用的內(nèi)存區(qū)域,避免了為未使用的虛擬地址空間分配頁表所造成的內(nèi)存浪費 。同時,多級頁表的結(jié)構(gòu)也使得內(nèi)存管理更加靈活和高效,能夠更好地適應不同進程對內(nèi)存的需求。
四、linux中頁表處理數(shù)據(jù)結(jié)構(gòu)
分頁轉(zhuǎn)換功能由駐留在內(nèi)存中的表來描述,該表稱為頁表,存放在物理地址空間中。頁表可以看作是簡單的 2^20 物理地址數(shù)組。線性到物理地址的映射功能可以簡單地看作進行數(shù)組查找。線性地址的高 20 位構(gòu)成這個數(shù)組的索引值,用于選擇對應頁面的物理(基)地址。線性地址的低 12 位給出了頁面中的偏移量,加上頁面的基地址最終形成對應的物理地址。由于頁面基地址對齊在 4K 邊界上,因此頁面基地址的低 12 為肯定是 0 ,這意味著 高 20 位的頁面基地址 和 12 位偏移地址連接組合在一起就能得到對應的物理地址。
頁表中每個頁表項 大小為 32 位,由于只需其中的 20 位來存放頁面的物理基地址,因此剩下的 12 位可用于存放諸如頁面是否存在等的屬性信息。如果線性地址索引的頁表被標注為存在,則表示該項有效, 我們可以從中取得頁面的物理地址。如果項中表明不存在,那么當當訪問對應物理界面時就會產(chǎn)生一個異常。
4.1 頁表類型定義
(1)pgd_t、pmd_t、pud_t和pte_t
Linux分別采用pgd_t、pmd_t、pud_t和pte_t四種數(shù)據(jù)結(jié)構(gòu)來表示頁全局目錄項、頁上級目錄項、頁中間目錄項和頁表項。這四種 數(shù)據(jù)結(jié)構(gòu)本質(zhì)上都是無符號長整型unsigned long
Linux為了更嚴格數(shù)據(jù)類型檢查,將無符號長整型unsigned long分別封裝成四種不同的頁表項。如果不采用這種方法,那么一個無符號長整型數(shù)據(jù)可以傳入任何一個與四種頁表相關的函數(shù)或宏中,這將大大降低程序的健壯性。
pgprot_t是另一個64位(PAE激活時)或32位(PAE禁用時)的數(shù)據(jù)類型,它表示與一個單獨表項相關的保護標志。首先我們查看一下子這些類型是如何定義的:
①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__ */
②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;
}
③xxx_val和__xxx
參照/arch/x86/include/asm/pgtable.h
五個類型轉(zhuǎn)換宏(_ pte、_ pmd、_ pud、_ pgd和__ pgprot)把一個無符號整數(shù)轉(zhuǎn)換成所需的類型。
另外的五個類型轉(zhuǎn)換宏(pte_val,pmd_val, pud_val, pgd_val和pgprot_val)執(zhí)行相反的轉(zhuǎn)換,即把上面提到的四種特殊的類型轉(zhuǎn)換成一個無符號整數(shù)。
#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)
這里需要區(qū)別指向頁表項的指針和頁表項所代表的數(shù)據(jù)。以pgd_t類型為例子,如果已知一個pgd_t類型的指針pgd,那么通過pgd_val(*pgd)即可獲得該頁表項(也就是一個無符號長整型數(shù)據(jù)),這里利用了面向?qū)ο蟮乃枷搿?/span>
4.2頁表描述宏
參照arch/x86/include/asm/pgtable_64
linux中使用下列宏簡化了頁表處理,對于每一級頁表都使用有以下三個關鍵描述宏:
宏字段 | 描述 |
XXX_SHIFT | 指定Offset字段的位數(shù) |
XXX_SIZE | 頁的大小 |
XXX_MASK | 用以屏蔽Offset字段的所有位 |
我們的四級頁表,對應的宏分別由PAGE,PMD,PUD,PGDIR
宏字段前綴 | 描述 |
PGDIR | 頁全局目錄(Page Global Directory) |
PUD | 頁上級目錄(Page Upper Directory) |
PMD | 頁中間目錄(Page Middle Directory) |
PAGE | 頁表(Page Table) |
PAGE宏–頁表(Page Table)
字段 | 描述 |
PAGE_MASK | 用以屏蔽Offset字段的所有位 |
PAGE_SHIFT | 指定Offset字段的位數(shù) |
PAGE_SIZE | 頁的大小 |
定義如下,在/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,由于頁內(nèi)所有地址都必須放在Offset字段, 因此80x86系統(tǒng)的頁的大小PAGE_SIZE是2^12=4096
字節(jié),PAGE_MASK宏產(chǎn)生的值為0xfffff000,用以屏蔽Offset字段的所有位。
PMD-Page Middle Directory (頁目錄)
字段 | 描述 |
PMD_SHIFT | 指定線性地址的Offset和Table字段的總位數(shù);換句話說,是頁中間目錄項可以映射的區(qū)域大小的對數(shù) |
PMD_SIZE | 用于計算由頁中間目錄的一個單獨表項所映射的區(qū)域大小,也就是一個頁表的大小 |
PMD_MASK | 用于屏蔽Offset字段與Table字段的所有位 |
當PAE 被禁用時,PMD_SHIFT 產(chǎn)生的值為22(來自Offset 的12 位加上來自Table 的10 位),PMD_SIZE 產(chǎn)生的值為222 或 4 MB,PMD_MASK產(chǎn)生的值為 0xffc00000。
相反,當PAE被激活時,PMD_SHIFT 產(chǎn)生的值為21 (來自Offset的12位加上來自Table的9位),PMD_SIZE 產(chǎn)生的值為2^21 或2 MB PMD_MASK產(chǎn)生的值為 0xffe00000。
大型頁不使用最后一級頁表,所以產(chǎn)生大型頁尺寸的LARGE_PAGE_SIZE 宏等于PMD_SIZE(2PMD_SHIFT),而在大型頁地址中用于屏蔽Offset字段和Table字段的所有位的LARGE_PAGE_MASK宏,就等于PMD_MASK。
PUD_SHIFT-頁上級目錄(Page Upper Directory)
字段 | 描述 |
PUD_SHIFT | 確定頁上級目錄項能映射的區(qū)域大小的位數(shù) |
PUD_SIZE | 用于計算頁全局目錄中的一個單獨表項所能映射的區(qū)域大小。 |
PUD_MASK | 用于屏蔽Offset字段,Table字段,Middle Air字段和Upper Air字段的所有位 |
在80x86處理器上,PUD_SHIFT總是等價于PMD_SHIFT,而PUD_SIZE則等于4MB或2MB。
PGDIR_SHIFT-頁全局目錄(Page Global Directory)
字段 | 描述 |
PGDIR_SHIFT | 確定頁全局頁目錄項能映射的區(qū)域大小的位數(shù) |
PGDIR_SIZE | 用于計算頁全局目錄中一個單獨表項所能映射區(qū)域的大小 |
PGDIR_MASK | 用于屏蔽Offset, Table,Middle Air及Upper Air的所有位 |
當PAE 被禁止時,PGDIR_SHIFT 產(chǎn)生的值為22(與PMD_SHIFT 和PUD_SHIFT 產(chǎn)生的值相同),PGDIR_SIZE 產(chǎn)生的值為 222 或 4 MB,PGDIR_MASK 產(chǎn)生的值為 0xffc00000。
相反,當PAE被激活時,PGDIR_SHIFT 產(chǎn)生的值為30 (12 位Offset 加 9 位Table再加 9位 Middle Air),PGDIR_SIZE 產(chǎn)生的值為230 或 1 GBPGDIR_MASK產(chǎn)生的值為0xc0000000
PTRS_PER_PTE, PTRS_PER_PMD, PTRS_PER_PUD以及PTRS_PER_PGD用于計算頁表、頁中間目錄、頁上級目錄和頁全局目錄表中表項的個數(shù)。當PAE被禁止時,它們產(chǎn)生的值分別為1024,1,1和1024。當PAE被激活時,產(chǎn)生的值分別為512,512,1和4。
4.3頁表處理函數(shù)
內(nèi)核還提供了許多宏和函數(shù)用于讀或修改頁表表項:
- 如果相應的表項值為0,那么,宏pte_none、pmd_none、pud_none和 pgd_none產(chǎn)生的值為1,否則產(chǎn)生的值為0。
- 宏pte_clear、pmd_clear、pud_clear和 pgd_clear清除相應頁表的一個表項,由此禁止進程使用由該頁表項映射的線性地址。ptep_get_and_clear( )函數(shù)清除一個頁表項并返回前一個值。
- set_pte,set_pmd,set_pud和set_pgd向一個頁表項中寫入指定的值。set_pte_atomic與set_pte作用相同,但是當PAE被激活時它同樣能保證64位的值能被原子地寫入。
- 如果a和b兩個頁表項指向同一頁并且指定相同訪問優(yōu)先級,pte_same(a,b)返回1,否則返回0。
- 如果頁中間目錄項指向一個大型頁(2MB或4MB),pmd_large(e)返回1,否則返回0。
宏pmd_bad由函數(shù)使用并通過輸入?yún)?shù)傳遞來檢查頁中間目錄項。如果目錄項指向一個不能使用的頁表,也就是說,如果至少出現(xiàn)以下條件中的一個,則這個宏產(chǎn)生的值為1:
- 頁不在主存中(Present標志被清除)。
- 頁只允許讀訪問(Read/Write標志被清除)。
- Acessed或者Dirty位被清除(對于每個現(xiàn)有的頁表,Linux總是強制設置這些標志)。
pud_bad宏和pgd_bad宏總是產(chǎn)生0。沒有定義pte_bad宏,因為頁表項引用一個不在主存中的頁,一個不可寫的頁或一個根本無法訪問的頁都是合法的。
如果一個頁表項的Present標志或者Page Size標志等于1,則pte_present宏產(chǎn)生的值為1,否則為0。
前面講過頁表項的Page Size標志對微處理器的分頁部件來講沒有意義,然而,對于當前在主存中卻又沒有讀、寫或執(zhí)行權限的頁,內(nèi)核將其Present和Page Size分別標記為0和1。
這樣,任何試圖對此類頁的訪問都會引起一個缺頁異常,因為頁的Present標志被清0,而內(nèi)核可以通過檢查Page Size的值來檢測到產(chǎn)生異常并不是因為缺頁。
如果相應表項的Present標志等于1,也就是說,如果對應的頁或頁表被裝載入主存,pmd_present宏產(chǎn)生的值為1。pud_present宏和pgd_present宏產(chǎn)生的值總是1。
(1)查詢頁表項中任意一個標志的當前值
下表中列出的函數(shù)用來查詢頁表項中任意一個標志的當前值;除了pte_file()外,其他函數(shù)只有在pte_present返回1的時候,才能正常返回頁表項中任意一個標志。
- pte_user( ):讀 User/Supervisor 標志
- pte_read( ):讀 User/Supervisor 標志(表示 80x86 處理器上的頁不受讀的保護)
- pte_write( ):讀 Read/Write 標志
- pte_exec( ):讀 User/Supervisor 標志( 80x86 處理器上的頁不受代碼執(zhí)行的保護)
- pte_dirty( ):讀 Dirty 標志
- pte_young( ):讀 Accessed 標志
- pte_file( ):讀 Dirty 標志(當 Present 標志被清除而 Dirty 標志被設置時,頁屬于一個非線性磁盤文件映射)
(2)設置頁表項中各標志的值
下表列出的另一組函數(shù)用于設置頁表項中各標志的值
- 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 則將頁的訪問權設置為指定的值,并調(diào)用flush_tlb_page() 函數(shù)ptep_mkdirty()與 pte_mkdirty( ) 類似,但作用于指向頁表項的指針。
- ptep_test_and_clear_dirty( ):與 pte_mkclean( ) 類似,但作用于指向頁表項的指針并返回 Dirty 標志的舊值
- ptep_test_and_clear_young( ):與 pte_mkold( ) 類似,但作用于指向頁表項的指針并返回 Accessed標志的舊值
(3)宏函數(shù)-把一個頁地址和一組保護標志組合成頁表項,或者執(zhí)行相反的操作
現(xiàn)在,我們來討論下表中列出的宏,它們把一個頁地址和一組保護標志組合成頁表項,或者執(zhí)行相反的操作,從一個頁表項中提取出頁地址。請注意這其中的一些宏對頁的引用是通過 “頁描述符”的線性地址,而不是通過該頁本身的線性地址。
- pgd_index(addr):找到線性地址 addr 對應的的目錄項在頁全局目錄中的索引(相對位置)
- pgd_offset(mm, addr):接收內(nèi)存描述符地址 mm 和線性地址 addr 作為參數(shù)。這個宏產(chǎn)生地址addr 在頁全局目錄中相應表項的線性地址;通過內(nèi)存描述符 mm 內(nèi)的一個指針可以找到這個頁全局目錄pgd_offset_k(addr)產(chǎn)生主內(nèi)核頁全局目錄中的某個項的線性地址,該項對應于地址
- addrpgd_page(pgd):通過頁全局目錄項 pgd 產(chǎn)生頁上級目錄所在頁框的頁描述符地址。在兩級或三級分頁系統(tǒng)中,該宏等價于 pud_page() ,后者應用于頁上級目錄項
- pud_offset(pgd, addr):參數(shù)為指向頁全局目錄項的指針 pgd 和線性地址 addr 。這個宏產(chǎn)生頁上級目錄中目錄項 addr 對應的線性地址。在兩級或三級分頁系統(tǒng)中,該宏產(chǎn)生 pgd ,即一個頁全局目錄項的地址
- pud_page(pud):通過頁上級目錄項 pud 產(chǎn)生相應的頁中間目錄的線性地址。在兩級分頁系統(tǒng)中,該宏等價于 pmd_page() ,后者應用于頁中間目錄項
- pmd_index(addr):產(chǎn)生線性地址 addr 在頁中間目錄中所對應目錄項的索引(相對位置)
- pmd_offset(pud, addr):接收指向頁上級目錄項的指針 pud 和線性地址 addr 作為參數(shù)。這個宏產(chǎn)生目錄項 addr 在頁中間目錄中的偏移地址。在兩級或三級分頁系統(tǒng)中,它產(chǎn)生 pud ,即頁全局目錄項的地址
- pmd_page(pmd):通過頁中間目錄項 pmd 產(chǎn)生相應頁表的頁描述符地址。在兩級或三級分頁系統(tǒng)中, pmd 實際上是頁全局目錄中的一項mk_pte(p,prot)接收頁描述符地址 p 和一組訪問權限 prot 作為參數(shù),并創(chuàng)建相應的頁表項
- pte_index(addr):產(chǎn)生線性地址 addr 對應的表項在頁表中的索引(相對位置)
- pte_offset_kernel(dir,addr):線性地址 addr 在頁中間目錄 dir 中有一個對應的項,該宏就產(chǎn)生這個對應項,即頁表的線性地址。另外,該宏只在主內(nèi)核頁表上使用
- pte_offset_map(dir, addr):接收指向一個頁中間目錄項的指針 dir 和線性地址 addr 作為參數(shù),它產(chǎn)生與線性地址 addr 相對應的頁表項的線性地址。如果頁表被保存在高端存儲器中,那么內(nèi)核建立一個臨時內(nèi)核映射,并用 pte_unmap 對它進行釋放。pte_offset_map_nested 宏和 pte_unmap_nested 宏是相同的,但它們使用不同的臨時內(nèi)核映射
- pte_page( x ):返回頁表項 x 所引用頁的描述符地址
- pte_to_pgoff( pte ):從一個頁表項的 pte 字段內(nèi)容中提取出文件偏移量,這個偏移量對應著一個非線性文件內(nèi)存映射所在的頁
- pgoff_to_pte(offset ):為非線性文件內(nèi)存映射所在的頁創(chuàng)建對應頁表項的內(nèi)容
(4)簡化頁表項的創(chuàng)建和撤消
下面我們羅列最后一組函數(shù)來簡化頁表項的創(chuàng)建和撤消。當使用兩級頁表時,創(chuàng)建或刪除一個頁中間目錄項是不重要的。如本節(jié)前部分所述,頁中間目錄僅含有一個指向下屬頁表的目錄項。所以,頁中間目錄項只是頁全局目錄中的一項而已。然而當處理頁表時,創(chuàng)建一個頁表項可能很復雜,因為包含頁表項的那個頁表可能就不存在。在這樣的情況下,有必要分配一個新頁框,把它填寫為 0 ,并把這個表項加入。
如果 PAE 被激活,內(nèi)核使用三級頁表。當內(nèi)核創(chuàng)建一個新的頁全局目錄時,同時也分配四個相應的頁中間目錄;只有當父頁全局目錄被釋放時,這四個頁中間目錄才得以釋放。當使用兩級或三級分頁時,頁上級目錄項總是被映射為頁全局目錄中的一個單獨項。與以往一樣,下表中列出的函數(shù)描述是針對 80x86 構(gòu)架的。
五、線性地址轉(zhuǎn)換
5.1分頁模式下的的線性地址轉(zhuǎn)換
線性地址、頁表和頁表項線性地址不管系統(tǒng)采用多少級分頁模型,線性地址本質(zhì)上都是索引+偏移量的形式,甚至你可以將整個線性地址看作N+1個索引的組合,N是系統(tǒng)采用的分頁級數(shù)。在四級分頁模型下,線性地址被分為5部分,如下圖:
在線性地址中,每個頁表索引即代表線性地址在對應級別的頁表中中關聯(lián)的頁表項。正是這種索引與頁表項的對應關系形成了整個頁表映射機制。
(1)頁表
多個頁表項的集合則為頁表,一個頁表內(nèi)的所有頁表項是連續(xù)存放的。頁表本質(zhì)上是一堆數(shù)據(jù),因此也是以頁為單位存放在主存中的。因此,在虛擬地址轉(zhuǎn)化物理物理地址的過程中,每訪問一級頁表就會訪問一次內(nèi)存。
(2)頁表項
頁表項從四種頁表項的數(shù)據(jù)結(jié)構(gòu)可以看出,每個頁表項其實就是一個無符號長整型數(shù)據(jù)。每個頁表項分兩大類信息:頁框基地址和頁的屬性信息。在x86-32體系結(jié)構(gòu)中,每個頁表項的結(jié)構(gòu)圖如下:
這個圖是一個通用模型,其中頁表項的前20位是物理頁的基地址。由于32位的系統(tǒng)采用4kb大小的 頁,因此每個頁表項的后12位均為0。內(nèi)核將后12位充分利用,每個位都表示對應虛擬頁的相關屬性。
不管是那一級的頁表,它的功能就是建立虛擬地址和物理地址之間的映射關系,一個頁和一個頁框之間的映射關系體現(xiàn)在頁表項中。上圖中的物理頁基地址是 個抽象的說明,如果當前的頁表項位于頁全局目錄中,這個物理頁基址是指頁上級目錄所在物理頁的基地址;如果當前頁表項位于頁表中,這個物理頁基地址是指最 終要訪問數(shù)據(jù)所在物理頁的基地址。
(3)地址轉(zhuǎn)換過程
地址轉(zhuǎn)換過程有了上述的基本知識,就很好理解四級頁表模式下如何將虛擬地址轉(zhuǎn)化為邏輯地址了?;具^程如下:
從CR3寄存器中讀取頁目錄所在物理頁面的基址(即所謂的頁目錄基址),從線性地址的第一部分獲取頁目錄項的索引,兩者相加得到頁目錄項的物理地址。
第一次讀取內(nèi)存得到pgd_t結(jié)構(gòu)的目錄項,從中取出物理頁基址取出(具體位數(shù)與平臺相關,如果是32系統(tǒng),則為20位),即頁上級頁目錄的物理基地址。
從線性地址的第二部分中取出頁上級目錄項的索引,與頁上級目錄基地址相加得到頁上級目錄項的物理地址。
第二次讀取內(nèi)存得到pud_t結(jié)構(gòu)的目錄項,從中取出頁中間目錄的物理基地址。
從線性地址的第三部分中取出頁中間目錄項的索引,與頁中間目錄基址相加得到頁中間目錄項的物理地址。
第三次讀取內(nèi)存得到pmd_t結(jié)構(gòu)的目錄項,從中取出頁表的物理基地址。
從線性地址的第四部分中取出頁表項的索引,與頁表基址相加得到頁表項的物理地址。
第四次讀取內(nèi)存得到pte_t結(jié)構(gòu)的目錄項,從中取出物理頁的基地址。
從線性地址的第五部分中取出物理頁內(nèi)偏移量,與物理頁基址相加得到最終的物理地址。
第五次讀取內(nèi)存得到最終要訪問的數(shù)據(jù)。
整個過程是比較機械的,每次轉(zhuǎn)換先獲取物理頁基地址,再從線性地址中獲取索引,合成物理地址后再訪問內(nèi)存。不管是頁表還是要訪問的數(shù)據(jù)都是以頁為單 位存放在主存中的,因此每次訪問內(nèi)存時都要先獲得基址,再通過索引(或偏移)在頁內(nèi)訪問數(shù)據(jù),因此可以將線性地址看作是若干個索引的集合。
5.2 Linux中通過4級頁表訪問物理內(nèi)存
linux中每個進程有它自己的PGD( Page Global Directory),它是一個物理頁,并包含一個pgd_t數(shù)組。
進程的pgd_t數(shù)據(jù)見 task_struct -> mm_struct -> pgd_t * pgd;
PTEs, PMDs和PGDs分別由pte_t, pmd_t 和pgd_t來描述。為了存儲保護位,pgprot_t被定義,它擁有相關的flags并經(jīng)常被存儲在page table entry低位(lower bits),其具體的存儲方式依賴于CPU架構(gòu)。
前面我們講了頁表處理的大多數(shù)函數(shù)信息,在上面我們又講了線性地址如何轉(zhuǎn)換為物理地址,其實就是不斷索引的過程。
通過如下幾個函數(shù),不斷向下索引,就可以從進程的頁表中搜索特定地址對應的頁面對象:
- pgd_offset根據(jù)當前虛擬地址和當前進程的mm_struct獲取pgd項
- pud_offset參數(shù)為指向頁全局目錄項的指針 pgd 和線性地址 addr 。這個宏產(chǎn)生頁上級目錄中目錄項 addr 對應的線性地址。在兩級或三級分頁系統(tǒng)中,該宏產(chǎn)生 pgd ,即一個頁全局目錄項的地址
- pmd_offset根據(jù)通過pgd_offset獲取的pgd 項和虛擬地址,獲取相關的pmd項(即pte表的起始地址)
- pte_offset根據(jù)通過pmd_offset獲取的pmd項和虛擬地址,獲取相關的pte項(即物理頁的起始地址)
根據(jù)虛擬地址獲取物理頁的示例代碼詳見mm/memory.c中的函數(shù)follow_page
不同的版本可能有所不同,早起內(nèi)核中存在follow_page,而后來的內(nèi)核中被follow_page_mask替代,目前最新的發(fā)布4.4中為查找到此函數(shù)
我們從早期的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)
}
六、Linux 分頁機制的顯著優(yōu)勢
6.1內(nèi)存保護與隔離
在 Linux 的內(nèi)存管理體系中,分頁機制就像是一位忠誠的衛(wèi)士,為系統(tǒng)的安全穩(wěn)定運行保駕護航,而其中頁表項權限位則是這位衛(wèi)士手中的 “秘密武器”,在內(nèi)存保護和隔離方面發(fā)揮著關鍵作用。
每個進程都擁有屬于自己的獨立頁表,這就如同每個進程都有一個專屬的 “房間”,而頁表則是這個房間的 “門鎖” 和 “管家”。頁表項中的權限位,如讀(Read)權限位、寫(Write)權限位和執(zhí)行(Execute)權限位等,就像是門鎖上的不同 “密碼”,嚴格控制著對內(nèi)存頁的訪問權限 。例如,當一個進程試圖訪問另一個進程的內(nèi)存空間時,由于其頁表中對應的權限位不允許這樣的訪問,系統(tǒng)就會立即檢測到這種違規(guī)行為,并觸發(fā)一個頁錯誤(Page Fault) 。這就好比有人拿著錯誤的密碼試圖打開別人房間的門,門鎖會立刻發(fā)出警報。
操作系統(tǒng)在接收到頁錯誤后,會迅速采取相應措施,比如記錄錯誤信息,終止違規(guī)進程,以防止惡意進程的攻擊和破壞,確保每個進程的數(shù)據(jù)都能安全地存放在自己的 “房間” 里,實現(xiàn)了不同進程之間內(nèi)存的有效隔離 。這種基于頁表項權限位的內(nèi)存保護機制,為 Linux 系統(tǒng)的安全性和穩(wěn)定性提供了堅實的保障,讓各個進程能夠在相互隔離的環(huán)境中穩(wěn)定運行,避免了數(shù)據(jù)泄露和非法訪問帶來的風險。
6.2高效內(nèi)存利用
分頁機制在提高內(nèi)存利用率方面,有著出色的表現(xiàn),堪稱內(nèi)存管理的 “效率大師”。它通過將內(nèi)存劃分為固定大小的頁,巧妙地解決了內(nèi)存碎片這一棘手問題。
在傳統(tǒng)的內(nèi)存分配方式中,由于進程對內(nèi)存的需求大小不一,頻繁的分配和釋放內(nèi)存很容易產(chǎn)生內(nèi)存碎片。就像你有一塊大蛋糕,每次根據(jù)不同的需求切下大小不一的塊,時間久了,就會剩下很多小塊的 “邊角料”,這些 “邊角料” 因為太小而無法被充分利用,造成了內(nèi)存的浪費。而分頁機制則像是把蛋糕切成了大小相同的小塊,無論進程需要多少內(nèi)存,都可以通過分配相應數(shù)量的頁來滿足需求 。當進程結(jié)束后,釋放的頁又可以被系統(tǒng)重新分配,大大減少了內(nèi)存碎片的產(chǎn)生,提高了內(nèi)存的使用效率。
同時,分頁機制還支持動態(tài)內(nèi)存分配,能夠根據(jù)進程的實際需求,靈活地分配和回收內(nèi)存頁。當一個進程需要更多的內(nèi)存時,系統(tǒng)可以迅速為其分配新的頁;當進程不再需要某些內(nèi)存時,這些頁又能及時被回收,重新投入到其他需要的地方。這種動態(tài)分配的方式,就像一個智能的資源分配器,能夠根據(jù)實際情況,合理地調(diào)配內(nèi)存資源,避免了內(nèi)存的閑置和浪費,使得系統(tǒng)能夠在有限的內(nèi)存條件下,高效地運行多個進程 。
例如,在一個同時運行多個程序的 Linux 系統(tǒng)中,分頁機制可以根據(jù)每個程序的實時內(nèi)存需求,動態(tài)地分配和回收內(nèi)存頁,確保每個程序都能獲得足夠的內(nèi)存資源,同時又不會造成內(nèi)存的過度占用,極大地提高了內(nèi)存的利用率,提升了系統(tǒng)的整體性能。
七、分頁機制的應用場景
7.1服務器場景
在服務器環(huán)境中,通常會同時運行多個不同的進程,這些進程各自承擔著不同的任務,比如 Web 服務器進程負責處理網(wǎng)頁請求,數(shù)據(jù)庫服務器進程負責數(shù)據(jù)的存儲和讀取,郵件服務器進程負責郵件的收發(fā)等 。以一個典型的電商網(wǎng)站服務器為例,它可能同時運行著 Web 服務進程、數(shù)據(jù)庫服務進程和緩存服務進程等。Web 服務進程需要處理大量用戶的頁面訪問請求,數(shù)據(jù)庫服務進程則要頻繁地進行數(shù)據(jù)的查詢和更新操作,緩存服務進程則負責存儲和管理常用的數(shù)據(jù),以提高訪問速度 。
在這種多進程并發(fā)運行的情況下,內(nèi)存資源的管理變得至關重要。分頁機制就像是一位高效的資源調(diào)度員,為每個進程分配獨立的虛擬內(nèi)存空間,確保它們在各自的 “小天地” 里運行,互不干擾。當 Web 服務進程收到用戶的頁面請求時,它會在自己的虛擬內(nèi)存空間中查找和處理相關數(shù)據(jù),而不會影響到數(shù)據(jù)庫服務進程和緩存服務進程的正常運行 。同時,分頁機制通過頁表的映射,能夠快速地將虛擬地址轉(zhuǎn)換為物理地址,提高內(nèi)存訪問的效率。當數(shù)據(jù)庫服務進程需要讀取或?qū)懭霐?shù)據(jù)時,分頁機制可以迅速地定位到物理內(nèi)存中的數(shù)據(jù)位置,減少數(shù)據(jù)訪問的延遲,保證數(shù)據(jù)庫操作的高效性 。
而且,分頁機制還能根據(jù)進程的實際需求,動態(tài)地分配和回收內(nèi)存頁。當電商網(wǎng)站在促銷活動期間,訪問量大幅增加,Web 服務進程需要更多的內(nèi)存來處理請求時,分頁機制可以及時為其分配額外的內(nèi)存頁;當活動結(jié)束后,訪問量減少,分頁機制又能回收多余的內(nèi)存頁,將其重新分配給其他有需要的進程,從而實現(xiàn)內(nèi)存資源的高效利用,保障服務器在高負載情況下的穩(wěn)定運行 。
7.2虛擬化場景
在虛擬化技術中,分頁機制扮演著不可或缺的角色,它是實現(xiàn)虛擬機內(nèi)存隔離與分配的關鍵所在。以 KVM(Kernel-based Virtual Machine)虛擬化技術為例,它基于 Linux 內(nèi)核,通過將 Linux 內(nèi)核轉(zhuǎn)變?yōu)橐粋€ Hypervisor,能夠在同一臺物理主機上運行多個虛擬機 。
每個虛擬機都仿佛是一臺獨立的物理計算機,擁有自己獨立的操作系統(tǒng)和應用程序,而分頁機制則為這些虛擬機提供了獨立的內(nèi)存空間,就像為每個虛擬機劃分了專屬的 “內(nèi)存區(qū)域”,確保它們之間的內(nèi)存相互隔離,不會出現(xiàn)數(shù)據(jù)泄露或相互干擾的情況。當一個虛擬機中的應用程序訪問內(nèi)存時,分頁機制會根據(jù)該虛擬機的頁表,將虛擬地址準確地映射到對應的物理內(nèi)存頁上,保證虛擬機的正常運行 。
同時,分頁機制還能實現(xiàn)內(nèi)存的高效分配。在物理主機內(nèi)存有限的情況下,分頁機制可以根據(jù)各個虛擬機的實際內(nèi)存需求,動態(tài)地分配內(nèi)存頁。比如,當一臺虛擬機運行一個輕量級的應用程序時,它可能只需要較少的內(nèi)存,分頁機制就會為其分配適量的內(nèi)存頁;而當另一臺虛擬機運行一個大型數(shù)據(jù)庫應用程序時,它需要大量的內(nèi)存,分頁機制會優(yōu)先滿足其需求,為其分配足夠的內(nèi)存頁 。
這種靈活的內(nèi)存分配方式,使得物理主機的內(nèi)存資源能夠得到充分利用,提高了虛擬化環(huán)境的整體性能 。此外,分頁機制還能支持內(nèi)存的共享。在一些情況下,多個虛擬機可能會運行相同的操作系統(tǒng)或應用程序,分頁機制可以讓這些虛擬機共享相同的物理內(nèi)存頁,減少內(nèi)存的占用,進一步提高內(nèi)存的利用率 。例如,在一個云計算數(shù)據(jù)中心中,通過 KVM 虛擬化技術運行著大量的虛擬機,分頁機制的存在使得這些虛擬機能夠高效地共享物理主機的內(nèi)存資源,實現(xiàn)了資源的最大化利用,為用戶提供了穩(wěn)定、高效的云計算服務 。