Linux分頁機制之分頁機制的實現詳解--Linux記憶體管理(八)

来源:https://www.cnblogs.com/linhaostudy/archive/2018/11/29/10038100.html
-Advertisement-
Play Games

1 linux的分頁機制 1.1 四級分頁機制 前面我們提到Linux內核僅使用了較少的分段機制,但是卻對分頁機制的依賴性很強,其使用一種適合32位和64位結構的通用分頁模型,該模型使用四級分頁機制,即 頁全局目錄(Page Global Directory) 頁上級目錄(Page Upper Di ...


1 linux的分頁機制

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位系統使用三級還是四級分頁取決於硬體對線性地址的位的劃分。

1.3 為什麼linux熱衷:分頁>分段

那麼,為什麼Linux是如此地熱衷使用分頁技術而對分段機製表現得那麼地冷淡呢,因為Linux的進程處理很大程度上依賴於分頁。事實上,線性地址到物理地址的自動轉換使下麵的設計目標變得可行:

  • 給每一個進程分配一塊不同的物理地址空間,這確保了可以有效地防止定址錯誤。
  • 區別頁(即一組數據)和頁框(即主存中的物理地址)之不同。這就允許存放在某個頁框中的一個頁,然後保存到磁碟上,以後重新裝入這同一頁時又被裝在不同的頁框中。這就是虛擬記憶體機制的基本要素。

每一個進程有它自己的頁全局目錄和自己的頁表集。當發生進程切換時,Linux把cr3控制寄存器的內容保存在前一個執行進程的描述符中,然後把下一個要執行進程的描述符的值裝入cr3寄存器中。因此,當新進程重新開始在CPU上執行時,分頁單元指向一組正確的頁表。

把線性地址映射到物理地址雖然有點複雜,但現在已經成了一種機械式的任務。

2 linux中頁表處理數據結構

2.1 頁表類型定義pgd_t、pmd_t、pud_t和pte_t

Linux分別採用pgd_tpmd_tpud_tpte_t四種數據結構來表示頁全局目錄項、頁上級目錄項、頁中間目錄項和頁表項。這四種 數據結構本質上都是無符號長整型unsigned long

Linux為了更嚴格數據類型檢查,將無符號長整型unsigned long分別封裝成四種不同的頁表項。如果不採用這種方法,那麼一個無符號長整型數據可以傳入任何一個與四種頁表相關的函數或巨集中,這將大大降低程式的健壯性。

pgprot_t是另一個64位(PAE激活時)或32位(PAE禁用時)的數據類型,它表示與一個單獨表項相關的保護標誌。

首先我們查看一下子這些類型是如何定義的

2.1.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.1.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;
}

2.1.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中使用下列巨集簡化了頁表處理,對於每一級頁表都使用有以下三個關鍵描述巨集:

巨集欄位 描述
XXX_SHIFT 指定Offset欄位的位數
XXX_SIZE 頁的大小
XXX_MASK 用以屏蔽Offset欄位的所有位。

我們的四級頁表,對應的巨集分別由PAGE,PMD,PUD,PGDIR

巨集欄位首碼 描述
PGDIR 頁全局目錄(Page Global Directory)
PUD 頁上級目錄(Page Upper Directory)
PMD 頁中間目錄(Page Middle Directory)
PAGE 頁表(Page Table)

2.2.1 PAGE巨集–頁表(Page Table)

欄位 描述
PAGE_SHIFT 指定Offset欄位的位數
PAGE_SIZE 頁的大小
PAGE_MASK 用以屏蔽Offset欄位的所有位。

定義如下,在/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.2.2 PMD-Page Middle Directory (頁目錄)

欄位 描述
PMD_SHIFT 指定線性地址的Offset和Table欄位的總位數;換句話說,是頁中間目錄項可以映射的區域大小的對數
PMD_SIZE 用於計算由頁中間目錄的一個單獨表項所映射的區域大小,也就是一個頁表的大小
PMD_MASK 用於屏蔽Offset欄位與Table欄位的所有位

當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。

2.2.3 PUD_SHIFT-頁上級目錄(Page Upper Directory)

欄位 描述
PUD_SHIFT 確定頁上級目錄項能映射的區域大小的位數
PUD_SIZE 用於計算頁全局目錄中的一個單獨表項所能映射的區域大小。
PUD_MASK 用於屏蔽Offset欄位,Table欄位,Middle Air欄位和Upper Air欄位的所有位

在80x86處理器上,PUD_SHIFT總是等價於PMD_SHIFT,而PUD_SIZE則等於4MB或2MB。

2.2.4 PGDIR_SHIFT-頁全局目錄(Page Global Directory)

欄位 描述
PGDIR_SHIFT 確定頁全局頁目錄項能映射的區域大小的位數
PGDIR_SIZE 用於計算頁全局目錄中一個單獨表項所能映射區域的大小
PGDIR_MASK 用於屏蔽Offset, Table,Middle Air及Upper Air的所有位

當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 GB
PGDIR_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 頁表處理函數

[註意]

以下內容主要參見 深入理解linux內核第二章記憶體定址中頁表處理

內核還提供了許多巨集和函數用於讀或修改頁表表項:

  • 如果相應的表項值為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。

2.3.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.3.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標誌的舊值

2.3.3 巨集函數-把一個頁地址和一組保護標誌組合成頁表項,或者執行相反的操作

現在,我們來討論下表中列出的巨集,它們把一個頁地址和一組保護標誌組合成頁表項,或者執行相反的操作,從一個頁表項中提取出頁地址。請註意這其中的一些巨集對頁的引用是通過 “頁描述符”的線性地址,而不是通過該頁本身的線性地址。

巨集名稱 說明
pgd_index(addr) 找到線性地址 addr 對應的的目錄項在頁全局目錄中的索引(相對位置)
pgd_offset(mm, addr) 接收記憶體描述符地址 mm 和線性地址 addr 作為參數。這個巨集產生地址addr 在頁全局目錄中相應表項的線性地址;通過記憶體描述符 mm 內的一個指針可以找到這個頁全局目錄
pgd_offset_k(addr) 產生主內核頁全局目錄中的某個項的線性地址,該項對應於地址 addr
pgd_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 ) 為非線性文件記憶體映射所在的頁創建對應頁表項的內容

2.3.4 簡化頁表項的創建和撤消

下麵我們羅列最後一組函數來簡化頁表項的創建和撤消。當使用兩級頁表時,創建或刪除一個頁中間目錄項是不重要的。如本節前部分所述,頁中間目錄僅含有一個指向下屬頁表的目錄項。所以,頁中間目錄項只是頁全局目錄中的一項而已。然而當處理頁表時,創建一個頁表項可能很複雜,因為包含頁表項的那個頁表可能就不存在。在這樣的情況下,有必要分配一個新頁框,把它填寫為 0 ,並把這個表項加入。

如果 PAE 被激活,內核使用三級頁表。當內核創建一個新的頁全局目錄時,同時也分配四個相應的頁中間目錄;只有當父頁全局目錄被釋放時,這四個頁中間目錄才得以釋放。當使用兩級或三級分頁時,頁上級目錄項總是被映射為頁全局目錄中的一個單獨項。與以往一樣,下表中列出的函數描述是針對 80x86 構架的。

函數名稱 說明
pgd_alloc( mm ) 分配一個新的頁全局目錄。如果 PAE 被激活,它還分配三個對應用戶態線性地址的子頁中間目錄。參數 mm( 記憶體描述符的地址 )在 80x86 構架上被忽略
pgd_free( pgd) 釋放頁全局目錄中地址為 pgd 的項。如果 PAE 被激活,它還將釋放用戶態線性地址對應的三個頁中間目錄
pud_alloc(mm, pgd, addr) 在兩級或三級分頁系統下,這個函數什麼也不做:它僅僅返回頁全局目錄項 pgd 的線性地址
pud_free(x) 在兩級或三級分頁系統下,這個巨集什麼也不做
pmd_alloc(mm, pud, addr) 定義這個函數以使普通三級分頁系統可以為線性地址 addr 分配一個新的頁中間目錄。如果 PAE 未被激活,這個函數只是返回輸入參數 pud 的值,也就是說,返回頁全局目錄中目錄項的地址。如果 PAE 被激活,該函數返回線性地址 addr 對應的頁中間目錄項的線性地址。參數 mm 被忽略
pmd_free(x) 該函數什麼也不做,因為頁中間目錄的分配和釋放是隨同它們的父全局目錄一同進行的
pte_alloc_map(mm, pmd, addr) 接收頁中間目錄項的地址 pmd 和線性地址 addr 作為參數,並返回與 addr 對應的頁表項的地址。如果頁中間目錄項為空,該函數通過調用函數 pte_alloc_one( ) 分配一個新頁表。如果分配了一個新頁表, addr 對應的項就被創建,同時 User/Supervisor 標誌被設置為 1 。如果頁表被保存在高端記憶體,則內核建立一個臨時內核映射,並用 pte_unmap 對它進行釋放
pte_alloc_kernel(mm, pmd, addr) 如果與地址 addr 相關的頁中間目錄項 pmd 為空,該函數分配一個新頁表。然後返回與 addr 相關的頁表項的線性地址。該函數僅被主內核頁表使用
pte_free(pte) 釋放與頁描述符指針 pte 相關的頁表
pte_free_kernel(pte) 等價於 pte_free( ) ,但由主內核頁表使用
clear_page_range(mmu, start,end) 從線性地址 start 到 end 通過反覆釋放頁表和清除頁中間目錄項來清除進程頁表的內容

3 線性地址轉換

3.1 分頁模式下的的線性地址轉換

線性地址、頁表和頁表項線性地址不管系統採用多少級分頁模型,線性地址本質上都是索引+偏移量的形式,甚至你可以將整個線性地址看作N+1個索引的組合,N是系統採用的分頁級數。在四級分頁模型下,線性地址被分為5部分,如下圖:

線上性地址中,每個頁表索引即代表線性地址在對應級別的頁表中中關聯的頁表項。正是這種索引與頁表項的對應關係形成了整個頁表映射機制。

3.1.1 頁表

多個頁表項的集合則為頁表,一個頁表內的所有頁表項是連續存放的。頁表本質上是一堆數據,因此也是以頁為單位存放在主存中的。因此,在虛擬地址轉化物理物理地址的過程中,每訪問一級頁表就會訪問一次記憶體。

3.1.2 頁表項

頁表項從四種頁表項的數據結構可以看出,每個頁表項其實就是一個無符號長整型數據。每個頁表項分兩大類信息:頁框基地址和頁的屬性信息。在x86-32體繫結構中,每個頁表項的結構圖如下:

這個圖是一個通用模型,其中頁表項的前20位是物理頁的基地址。由於32位的系統採用4kb大小的 頁,因此每個頁表項的後12位均為0。內核將後12位充分利用,每個位都表示對應虛擬頁的相關屬性。

不管是那一級的頁表,它的功能就是建立虛擬地址和物理地址之間的映射關係,一個頁和一個頁框之間的映射關係體現在頁表項中。上圖中的物理頁基地址是 個抽象的說明,如果當前的頁表項位於頁全局目錄中,這個物理頁基址是指頁上級目錄所在物理頁的基地址;如果當前頁表項位於頁表中,這個物理頁基地址是指最 終要訪問數據所在物理頁的基地址。

3.1.3 地址轉換過程

地址轉換過程有了上述的基本知識,就很好理解四級頁表模式下如何將虛擬地址轉化為邏輯地址了。基本過程如下:

  1. 從CR3寄存器中讀取頁目錄所在物理頁面的基址(即所謂的頁目錄基址),從線性地址的第一部分獲取頁目錄項的索引,兩者相加得到頁目錄項的物理地址。
  2. 第一次讀取記憶體得到pgd_t結構的目錄項,從中取出物理頁基址取出(具體位數與平臺相關,如果是32系統,則為20位),即頁上級頁目錄的物理基地址。
  3. 從線性地址的第二部分中取出頁上級目錄項的索引,與頁上級目錄基地址相加得到頁上級目錄項的物理地址。
  4. 第二次讀取記憶體得到pud_t結構的目錄項,從中取出頁中間目錄的物理基地址。
  5. 從線性地址的第三部分中取出頁中間目錄項的索引,與頁中間目錄基址相加得到頁中間目錄項的物理地址。
  6. 第三次讀取記憶體得到pmd_t結構的目錄項,從中取出頁表的物理基地址。
  7. 從線性地址的第四部分中取出頁表項的索引,與頁表基址相加得到頁表項的物理地址。
  8. 第四次讀取記憶體得到pte_t結構的目錄項,從中取出物理頁的基地址。
  9. 從線性地址的第五部分中取出物理頁內偏移量,與物理頁基址相加得到最終的物理地址。
  10. 第五次讀取記憶體得到最終要訪問的數據。

整個過程是比較機械的,每次轉換先獲取物理頁基地址,再從線性地址中獲取索引,合成物理地址後再訪問記憶體。不管是頁表還是要訪問的數據都是以頁為單 位存放在主存中的,因此每次訪問記憶體時都要先獲得基址,再通過索引(或偏移)在頁內訪問數據,因此可以將線性地址看作是若幹個索引的集合。

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&lt;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)
}

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • hostname命令 作用:顯示以及設置主機名 一. 顯示系統主機名 第一種方式:hostname 第二種方式:cat /etc/sysconfig/ntework 使用舉例: 從上面可以看到我的系統主機名為pclu 二. 設置系統主機名,必須是root用戶才可以執行 第一種方式:hostname ...
  • [TOC] 背景 一直沒搞清楚 與 的區別, 看著公司里遺留的shell, 也就稀里糊塗地用著... 這是很糟糕的態度 結論放前面 使進程在後臺運行, 預設輸出到標準輸出(即當前屏幕), 除非重定向輸出. 此時忽略 SIGINT 信號. ==若關閉會話, 則進程會結束== 進程仍舊在前臺跑, 預設輸 ...
  • 前言:因windows10的更新,最近很多朋友會遇到mstsc遠程連接桌面報錯: windows10企業版解決方式: 按“win+R”,運行 gpedit.msc, 找:“電腦配置”->“管理模板”->“系統”->“憑據分配”,這裡面有個“加密 Oracle 修正”,按圖修改即可; windows ...
  • 【轉載】 Windows Subsystem for Linux -- Pico Process Overview Overview This post discusses pico processes, the foundation of WSL. It explains how pico pro ...
  • Zabbix簡介 Zabbix是一個高度集成的企業級開源網路監控解決方案,與Cacti、nagios類似,提供分散式監控以及集中的web管理界面。zabbix具備常見商業監控軟體所具備的功能,例如主機性能監控,網路設備性能監控,資料庫性能監控,ftp等通用協議的監控,能夠靈活利用可定製警告機制,允許 ...
  • 基於linux 4.20 rc3源碼分析 1 .掃描所有PCI設備並檢測,填充設備結構體 其中pci_setup_device(dev)函數對掛載在該匯流排上所有的設備進行檢測並獲取相關數據,並設備信息進行填充。對於有些需特殊處理的設備也進行了特殊處理,達到儘量相容新老設備的目的。 1.1查詢設備廠商 ...
  • 目錄相關: 文件屬性: stat命令: vim 查看文件內容 more命令 echo命令 特殊符號 移動命令 刪除命令 查找命令 管道命令 head,tail命令 sed sed sed是一種流編輯器,它是文本處理中非常中的工具,能夠完美的配合正則表達式使用,功能不同凡響。處理時,把當前處理的行存儲 ...
  • 微服務架構中我們使用了必須的四個組件, `config gateway auth` 其中 依賴 ,`auth gateway auth` 這樣就確定了四個組件的啟動順序為 , 既然有依賴,那就肯定會使用 參數,但是這個參數只會判斷容器啟動沒有,並不會去判斷容器是否能用,就像你創建了一個nginx鏡像 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...