在32bit中的Linux內核中一般採用3層映射模型,第1層是頁面目錄(PGD),第2層是頁面中間目錄(PMD),第3層才是頁面映射表(PTE)。 但在ARM32系統中只用到兩層映射,因此在實際代碼中就要3層映射模型中合併一層 。在ARM32架構中,可以按段(section)來映射,這時採用單層映射 ...
在32bit中的Linux內核中一般採用3層映射模型,第1層是頁面目錄(PGD),第2層是頁面中間目錄(PMD),第3層才是頁面映射表(PTE)。但在ARM32系統中只用到兩層映射,因此在實際代碼中就要3層映射模型中合併一層。在ARM32架構中,可以按段(section)來映射,這時採用單層映射模式。使用頁面映射需要兩層映射結構,頁面的選擇可以是64KB的大頁面或4KB的小頁面,如圖2.4所示。Linux內核通常使用4KB大小的小頁面。
如果採用單層的段映射,記憶體中有一個段映射表,表中有4096個表項,每個表項的大小是4Byte,所以這個段映射表的大小是16KB,而且其位置必須與16KB邊界對齊。每個段表項可以定址1MB大小的地址空間。當CPU訪問記憶體時,32位虛擬地址的高12位(bit[31:20])用作訪問段映射表的索引,從表中找到相應的表項。每個表項提供了一個12位的物理段地址,以及相應的標誌位,如可讀、可寫等標誌位。將這個12bit的物理地址和虛擬地址的低20bit拼湊在一起,就得到32bit的物理地址;
如果使用頁面映射的方式,段映射表就變成了一級映射表(First Level table,在linux內核中成為PGD),其表項提供的不再是物理段地址,而是二級頁表的基地址。32bit虛擬地址的高12bit(bit[31:20])作為訪問一級頁表的索引值,找到相應的表項,每個表項指向一個二級頁表。以虛擬地址的次8bit(bit[19:12])作為訪問二級目錄的索引值,得到相應的頁表項,從這個頁表項找到20位的物理頁面地址。最後將這20bit的物理頁面地址和虛擬地址的低12bit拼湊起來,得到最終的32bit物理地址。這個過程在ARM32架構中由MMU硬體完成,軟體不需要介入;
[arch/arm/include/asm/pgtable-21level.h]
#define PMD_SHIFT 21
#define PGDIR_SHIFT 21
#define PMD_SIZE (1UL << PMD_SHIFT)
#define PMD_MASK (~(PMD_SIZE-1))
#define PGDIR_SIZE (1UL << PGDIR_SHIFT)
#define PGDIR_MASK (~(PGDIR_SIZE-1))
ARM32架構中一級頁表PGD的偏移量應該從20bit開始,為何這裡的頭文件定義從21bit開始呢?
我們從ARM linux內核建立具體記憶體區間的頁表映射過程中來看頁表映射是如何實現的。crate_mapping()函數就是為一個給定記憶體區間建立頁表映射,這個函數使用map_desc數據結構來描述一個記憶體區間。
struct map_desc {
unsigned long virtual; //虛擬地址的起始地址
unsigned long pfn; //物理地址的開始地址的頁幀號
unsigned long length; //記憶體區間大小
unsigned int type;
}
其中,virtual表示這個區間的虛擬地址起始點,pfn表示起始物理地址的頁幀號,length表示記憶體區間的長度,type表示記憶體區間的屬性,通常有個struct mem_type[]數組來描述記憶體屬性。struct mem_type數據結構描述記憶體區間類型以及相應的許可權和屬性等信息,其數據結構定義如下:
struct mem_type {
pteval_t prot_pte;
pteval_t prot_pte_s2;
pmdval_t prot_l1;
pmdval_t prot_sect;
unsigned int domain;
};
其中,domain成員用於ARM中定義的不同域,ARM中允許使用16個不同域,但在ARM Linux只定義和使用3個;
#define DOMAIN_KERNEL 2
#define DOMAIN_TABLE 2
#define DOMAIN_USER 1
#define DOMAIN_IO 0
DOMAIN_KERNEL和DOMAIN_TABLE其實用於系統空間,DOMAIN_IO用於I/O地址域,實際上也屬於系統空間,DOMAIN_USER則是用戶空間;
prot_pte成員用於頁面表項的控制位和標誌位,具體定義在:
#define L_PTE_VALID (_AT(pteval_t, 1) << 0)
#define L_PTE_PRESENT (_AT(pteval_t, 1) << 0)
#define L_PTE_YOUNG (_AT(pteval_t, 1) << 1)
#define L_PTE_DIRTY (_AT(pteval_t, 1) << 6)
#define L_PTE_RDONLY (_AT(pteval_t, 1) << 7)
#define L_PTE_USER (_AT(pteval_t, 1) << 8)
#define L_PTE_XN (_AT(pteval_t, 1) << 9)
#define L_PTE_SHARED (_AT(pteval_t, 1) << 10)
#define L_PTE_NONE (_AT(pteval_t, 1) << 11)
#define PROT_PTE_DEVICE L_PTE_PRESENT|L_PTE_YOUNG|L_PTE_DIRTY|L_PTE_XN
#define PROT_PTE_S2_DEVICE PROT_PTE_DEVICE
#define PROT_SECT_DEVICE PMD_TYPE_SECT|PMD_SECT_AP_WRITE
prot__l1用於一級頁表項的控制位和標誌位,具體定義如下:
/*
* Hardware page table definitions.
*
* + Level 1 descriptor (PMD)
* - common
*/
#define PMD_TYPE_MASK (_AT(pmdval_t, 3) << 0)
#define PMD_TYPE_FAULT (_AT(pmdval_t, 0) << 0)
#define PMD_TYPE_TABLE (_AT(pmdval_t, 1) << 0)
#define PMD_TYPE_SECT (_AT(pmdval_t, 2) << 0)
#define PMD_PXNTABLE (_AT(pmdval_t, 1) << 2) /* v7 */
#define PMD_BIT4 (_AT(pmdval_t, 1) << 4)
#define PMD_DOMAIN(x) (_AT(pmdval_t, (x)) << 5)
#define PMD_PROTECTION (_AT(pmdval_t, 1) << 9) /* v5 */
系統中定義了一個全局的mem_type[]數組來描述所有的記憶體區間類型。例如MT_DEVICE_CACHED、MT_DEVICE_WC、MT_MEMORY_RWX和MT_MEMORY_RW類型的記憶體區間的定義如下:
static struct mem_type mem_types[] = {
[MT_DEVICE] = { /* Strongly ordered / ARMv6 shared device */
.prot_pte = PROT_PTE_DEVICE | L_PTE_MT_DEV_SHARED |
L_PTE_SHARED,
.prot_pte_s2 = s2_policy(PROT_PTE_S2_DEVICE) |
s2_policy(L_PTE_S2_MT_DEV_SHARED) |
L_PTE_SHARED,
.prot_l1 = PMD_TYPE_TABLE,
.prot_sect = PROT_SECT_DEVICE | PMD_SECT_S,
.domain = DOMAIN_IO,
},
[MT_DEVICE_NONSHARED] = { /* ARMv6 non-shared device */
.prot_pte = PROT_PTE_DEVICE | L_PTE_MT_DEV_NONSHARED,
.prot_l1 = PMD_TYPE_TABLE,
.prot_sect = PROT_SECT_DEVICE,
.domain = DOMAIN_IO,
},
[MT_DEVICE_CACHED] = { /* ioremap_cached */
.prot_pte = PROT_PTE_DEVICE | L_PTE_MT_DEV_CACHED,
.prot_l1 = PMD_TYPE_TABLE,
.prot_sect = PROT_SECT_DEVICE | PMD_SECT_WB,
.domain = DOMAIN_IO,
},
[MT_DEVICE_WC] = { /* ioremap_wc */
.prot_pte = PROT_PTE_DEVICE | L_PTE_MT_DEV_WC,
.prot_l1 = PMD_TYPE_TABLE,
.prot_sect = PROT_SECT_DEVICE,
.domain = DOMAIN_IO,
},
[MT_UNCACHED] = {
.prot_pte = PROT_PTE_DEVICE,
.prot_l1 = PMD_TYPE_TABLE,
.prot_sect = PMD_TYPE_SECT | PMD_SECT_XN,
.domain = DOMAIN_IO,
},
[MT_CACHECLEAN] = {
.prot_sect = PMD_TYPE_SECT | PMD_SECT_XN,
.domain = DOMAIN_KERNEL,
},
#ifndef CONFIG_ARM_LPAE
[MT_MINICLEAN] = {
.prot_sect = PMD_TYPE_SECT | PMD_SECT_XN | PMD_SECT_MINICACHE,
.domain = DOMAIN_KERNEL,
},
#endif
[MT_LOW_VECTORS] = {
.prot_pte = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY |
L_PTE_RDONLY,
.prot_l1 = PMD_TYPE_TABLE,
.domain = DOMAIN_USER,
},
[MT_HIGH_VECTORS] = {
.prot_pte = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY |
L_PTE_USER | L_PTE_RDONLY,
.prot_l1 = PMD_TYPE_TABLE,
.domain = DOMAIN_USER,
},
[MT_MEMORY_RWX] = {
.prot_pte = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY,
.prot_l1 = PMD_TYPE_TABLE,
.prot_sect = PMD_TYPE_SECT | PMD_SECT_AP_WRITE,
.domain = DOMAIN_KERNEL,
},
[MT_MEMORY_RW] = {
.prot_pte = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY |
L_PTE_XN,
.prot_l1 = PMD_TYPE_TABLE,
.prot_sect = PMD_TYPE_SECT | PMD_SECT_AP_WRITE,
.domain = DOMAIN_KERNEL,
},
[MT_ROM] = {
.prot_sect = PMD_TYPE_SECT,
.domain = DOMAIN_KERNEL,
},
[MT_MEMORY_RWX_NONCACHED] = {
.prot_pte = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY |
L_PTE_MT_BUFFERABLE,
.prot_l1 = PMD_TYPE_TABLE,
.prot_sect = PMD_TYPE_SECT | PMD_SECT_AP_WRITE,
.domain = DOMAIN_KERNEL,
},
[MT_MEMORY_RW_DTCM] = {
.prot_pte = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY |
L_PTE_XN,
.prot_l1 = PMD_TYPE_TABLE,
.prot_sect = PMD_TYPE_SECT | PMD_SECT_XN,
.domain = DOMAIN_KERNEL,
},
[MT_MEMORY_RWX_ITCM] = {
.prot_pte = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY,
.prot_l1 = PMD_TYPE_TABLE,
.domain = DOMAIN_KERNEL,
},
[MT_MEMORY_RW_SO] = {
.prot_pte = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY |
L_PTE_MT_UNCACHED | L_PTE_XN,
.prot_l1 = PMD_TYPE_TABLE,
.prot_sect = PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_SECT_S |
PMD_SECT_UNCACHED | PMD_SECT_XN,
.domain = DOMAIN_KERNEL,
},
[MT_MEMORY_DMA_READY] = {
.prot_pte = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY |
L_PTE_XN,
.prot_l1 = PMD_TYPE_TABLE,
.domain = DOMAIN_KERNEL,
},
};
這樣一個map_desc數據結構就完整描述了一個記憶體區間,調用create_mapping()時以此數據結構指針為調用參數;
start_kernel() -->
setup_arch() -->
paging_init() -->
map_lowmem() -->
create_mapping()
/*
* Create the page directory entries and any necessary
* page tables for the mapping specified by `md'. We
* are able to cope here with varying sizes and address
* offsets, and we take full advantage of sections and
* supersections.
*/
static void __init create_mapping(struct map_desc *md)
{
unsigned long addr, length, end;
phys_addr_t phys;
const struct mem_type *type;
pgd_t *pgd;
if (md->virtual != vectors_base() && md->virtual < TASK_SIZE) {
pr_warn("BUG: not creating mapping for 0x%08llx at 0x%08lx in user region\n",
(long long)__pfn_to_phys((u64)md->pfn), md->virtual);
return;
}
if ((md->type == MT_DEVICE || md->type == MT_ROM) &&
md->virtual >= PAGE_OFFSET &&
(md->virtual < VMALLOC_START || md->virtual >= VMALLOC_END)) {
pr_warn("BUG: mapping for 0x%08llx at 0x%08lx out of vmalloc space\n",
(long long)__pfn_to_phys((u64)md->pfn), md->virtual);
}
type = &mem_types[md->type];
#ifndef CONFIG_ARM_LPAE
/*
* Catch 36-bit addresses
*/
if (md->pfn >= 0x100000) {
create_36bit_mapping(md, type);
return;
}
#endif
addr = md->virtual & PAGE_MASK;
phys = __pfn_to_phys(md->pfn);
length = PAGE_ALIGN(md->length + (md->virtual & ~PAGE_MASK));
if (type->prot_l1 == 0 && ((addr | phys | length) & ~SECTION_MASK)) {
pr_warn("BUG: map for 0x%08llx at 0x%08lx can not be mapped using pages, ignoring.\n",
(long long)__pfn_to_phys(md->pfn), addr);
return;
}
pgd = pgd_offset_k(addr);
end = addr + length;
do {
unsigned long next = pgd_addr_end(addr, end);
alloc_init_pud(pgd, addr, next, phys, type);
phys += next - addr;
addr = next;
} while (pgd++, addr != end);
}
這個函數的入參是一個結構體為map_desc叫 md 的東東,這個玩意用來表徵一個映射關係的結構;
在create_mapping()函數中,以PGDIR_SIZE為單位,在記憶體區域[virtual, virtual+length]中通過調用alloc_init_pud()來初始化PGD頁表項內容和下一級頁表PUD。pgd_addr_end()以PGDIR_SIZE為步長;
在第7行代碼中,通過md->type來獲取描述記憶體區域屬性的mem_type數據結構,這裡只需要通過查表的方式來獲取mem_type數據結構里的具體內容;
在第13行代碼中,通過pgd_offset_k()函數獲取所屬的頁面目錄項PGD。內核頁表存放在
swapper_pg_dir地址中,可以通過init_mm數據結構來獲取;
struct mm_struct init_mm = {
.mm_rb = RB_ROOT,
.pgd = swapper_pg_dir,
.mm_users = ATOMIC_INIT(2),
.mm_count = ATOMIC_INIT(1),
.mmap_sem = __RWSEM_INITIALIZER(init_mm.mmap_sem),
.page_table_lock = __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock),
.mmlist = LIST_HEAD_INIT(init_mm.mmlist),
INIT_MM_CONTEXT(init_mm)
};
內核頁表的基地址定義在arch/arm/kernel/head.S彙編代碼中。
[arch/arm/kernel/head.S]
#define KERNEL_RAM_VADDR (PAGE_OFFSET+TEXT_OFFSET)
#define PG_DIR_SIZE 0x4000
.globl swapper_pg_dir
.equ swapper_pg_dir, KERNEL_RAM_VADDR - PG_DIR_SIZE
[arch/arm/Makefile]
textofs-y := 0x00008000
TEXT_OFFSET := $(textofs-y)
從上面的代碼中,可以推算出頁表的基地址是0xc0004000;
pgd_offset_k()巨集可以從init_mm數據結構所指定的頁面目錄中找到地址addr所屬的頁面目錄項指針pgd。首先通過init_mm結構體得到頁表的基地址,然後通過addr右移PGDIR_SHIFT得到pgd的索引值,最後在一級頁表中找到頁表項pgd指針。pgd_offset_k()巨集定義如下:
#define PGDIR_SHIFT 21
#define pgd_index(addr) ((addr) >> PGDIR_SHIFT)
#define pgd_offset(mm, addr) ((mm)->pgd + pgd_index(addr))
#define pgd_offset_k(addr) pgd_offset(&init_mm, addr)
create_mapping()函數中的第15-22行代碼,由於ARM Vexpress平臺支持兩級頁表映射,所以PUD和PMD設置成與PGD等同了。
#define pud_offset(dir,addr) \
((pud_t *) pgd_page_vaddr(*(dir)) + (((addr) >> PUD_SHIFT) & (PTRS_PER_PUD - 1)))
#endif
/* Find an entry in the third-level page table.. */
#define pmd_offset(dir,addr) \
((pmd_t *) pud_page_vaddr(*(dir)) + (((addr) >> PMD_SHIFT) & (PTRS_PER_PMD - 1)))
因此,alloc_init_pud()函數一路調用到alloc_init_pte()函數。
static void __init alloc_init_pte(pmd_t *pmd, unsigned long addr,
unsigned long end, unsigned long pfn,
const struct mem_type *type)
{
pte_t *pte = early_pte_alloc(pmd, addr, type->prot_l1);
do {
set_pte_ext(pte, pfn_pte(pfn, __pgprot(type->prot_pte)), 0);
pfn++;
} while (pte++, addr += PAGE_SIZE, addr != end);
}
alloc_init_pte()首先判斷相應的PTE頁表項是否已經存在,如果不存在,那就要新建PTE頁表項。接下來的while迴圈是根據物理地址的pfn頁幀號來生成新的PTE表項(PTE entry),最後設置到ARM硬體頁表中。
create_mapping ->
alloc_init_pud ->
alloc_init_pmd ->
alloc_init_pte ->
early_pte_alloc
static pte_t * __init early_pte_alloc(pmd_t *pmd, unsigned long addr, unsigned long prot)
{
if (pmd_none(*pmd)) {
pte_t *pte = early_alloc(PTE_HWTABLE_OFF + PTE_HWTABLE_SIZE);
__pmd_populate(pmd, __pa(pte), prot);
}
BUG_ON(pmd_bad(*pmd));
return pte_offset_kernel(pmd, addr);
}
pmd_none()檢查這個參數的對應的PMD表項的內容,如果為0,說明這個頁表PTE還沒建立,所以要先去建立頁面表。這裡會去分配(PTE_HWTABLE_OFF+PTE_HWTABLE_SIZE)個PTE頁面表項,即會分配512+512個PTE頁表項。但是在ARM32架構中,二級頁表也只有256個頁面表項,為何要分配那麼多呢?
#define PTRS_PER_PTE 512
#define PTRS_PER_PMD 1
#define PTRS_PER_PGD 2048
#define PTE_HWTABLE_PTRS (PTRS_PER_PTE)
#define PTE_HWTABLE_OFF (PTE_HWTABLE_PTRS * sizoef(pte_t))
#define PTE_HWTABLE_SIZE (PTRS_PER_PTE * sizoof(u32))
先回答剛纔的問題:ARM架構中一級頁表的PGD的偏移量應該從20位開始,為何這裡的頭文件定義從21位開始呢?
-
這裡分配了兩個PTRS_PER_PTE(512)個頁面表項,也就是分配了兩份頁面表項。因為Linux內核預設的PGD從21位開始,也就是bit[31:21],一共2048個一級頁表項;而ARM32硬體結構上,PGD是從20bit開始的,頁表項為4096個,比Linux內核多一倍,那麼代碼實現上取巧了,以PTE_HWTABLE_OFF為偏移來寫PGD表項。而在真實硬體中,一個PGD頁表項,只有256個PTE,也就是說,前512個PTE頁表項是給OS用的(也就是Linux內核用的頁表,可以用於模擬L_PTE_DIRTY、L_PTE_YOUNG等標誌位),後512個頁面表是給ARM硬體MMU使用的;
-
一次映射兩個相鄰的一級頁表項,也就是對應的兩個相鄰的二級頁表都存放在一個page中;
然後把這個PTE頁面表的基地址通過__pmd_populate()函數設置到PMD頁表項中;
static inline void __pmd_populate(pmd_t *pmdp, phys_addr_t pte,
pmdval_t prot)
{
pmdval_t pmdval = (pte + PTE_HWTABLE_OFF) | prot;
pmdp[0] = __pmd(pmdval);
#ifndef CONFIG_ARM_LPAE
pmdp[1] = __pmd(pmdval + 256 * sizeof(pte_t));
#endif
flush_pmd_entry(pmdp);
}
註意這裡是把剛分配的1024個PTE頁面表中的第512個頁表項的地址作為基地址,再加上一些標誌位信息prot作為頁表項內容,寫入上一級頁表項PMD中。
相鄰的兩個二級頁表的基地址分別寫入PMD的頁表項中的pmdp[0]和pmdp[1]指針中。
typedef struct (pmdval_t pgd(2))
/* to find an entry in a page-table-directory */
#define pgd_index(addr) ((addr) >> PGDIR_SHIFT)
#define pgd_offset(mm, addr) ((mm)->pgd + pgd_index(addr))
PGD的定義其實是pmdval_t pgd[2],長度是兩倍,也就是pgd包括兩份相鄰的PTE頁表。所以pgd_offset()在查找pgd表項時,是按照pgd[2]長度來進行計算的,因此查找相應的pgd表項時,其中pgd[0]指向第一份PTE頁表,pgd[1]指向第二份PTE頁表;
pte_offset_kernel()函數返回相應的PTE頁表表項,然後通過__pgprot()和pfn組成PTE entry,最後由set_pte_ext()完成對硬體頁表項的設置;
static void __init alloc_init_pte(pmd_t *pmd, unsigned long addr,
unsigned long end, unsigned long pfn,
const struct mem_type *type)
{
pte_t *pte = early_pte_alloc(pmd, addr, type->prot_l1);
do {
set_pte_ext(pte, pfn_pte(pfn, __pgprot(type->prot_pte)), 0);
pfn++;
} while (pte++, addr += PAGE_SIZE, addr != end);
}
set_pte_ext()對於不同的CPU有不同的實現。對於基於ARMv7-A架構的處理器,例如Contex-A9,它的實現是在彙編函數cpu_v7_set_pte_ext中:
ENTRY(cpu_v7_set_pte_ext)
#ifdef CONFIG_MMU
str r1, [r0] @ linux version
bic r3, r1, #0x000003f0
bic r3, r3, #PTE_TYPE_MASK
orr r3, r3, r2
orr r3, r3, #PTE_EXT_AP0 | 2
tst r1, #1 << 4
orrne r3, r3, #PTE_EXT_TEX(1)
eor r1, r1, #L_PTE_DIRTY
tst r1, #L_PTE_RDONLY | L_PTE_DIRTY
orrne r3, r3, #PTE_EXT_APX
tst r1, #L_PTE_USER
orrne r3, r3, #PTE_EXT_AP1
tst r1, #L_PTE_XN
orrne r3, r3, #PTE_EXT_XN
tst r1, #L_PTE_YOUNG
tstne r1, #L_PTE_VALID
eorne r1, r1, #L_PTE_NONE
tstne r1, #L_PTE_NONE
moveq r3, #0
ARM( str r3, [r0, #2048]! )
THUMB( add r0, r0, #2048 )
THUMB( str r3, [r0] )
ALT_SMP(W(nop))
ALT_UP (mcr p15, 0, r0, c7, c10, 1) @ flush_pte
#endif
bx lr
ENDPROC(cpu_v7_set_pte_ext)
cpu_v7_set_pte_ext()函數參數r0表示PTE entry頁表項的指針,註意ARM Linux中實現了兩份頁表,硬體頁表的地址r0+2048。因此r0指Linux版本的頁面表地址,r1表示要寫入的Linux版本的PTE頁面表項內容,這裡指Linux版本的頁面表項內容,而非硬體版本的頁面表項內容。該函數的主要目的是根據Linux版本的頁面表項內容來填充ARM硬體版本的頁表項;
首先把linux內核版本的頁表項內容寫入linux版本的頁表中,然後根據mem_type數據結構prot_pte的標誌位來設置ARMV7-A硬體相關標誌位。prot_pte的標誌位是linux內核中採用的,定義在arch/arm/include/asm/pgtable-2level.h
頭文件。而硬體相關的標誌位定義在arch/arm/include/asm/pgtable-2level-hwdef.h
頭文件。這兩個標誌位對應的偏移是不一樣的,所以不同架構下的處理器需要單獨處理。ARM32架構硬體PTE頁面表定義的標誌位如下:
/*
* - extended small page/tiny page
*/
#define PTE_EXT_XN (_AT(pteval_t, 1) << 0) /* v6 */
#define PTE_EXT_AP_MASK (_AT(pteval_t, 3) << 4)
#define PTE_EXT_AP0 (_AT(pteval_t, 1) << 4)
#define PTE_EXT_AP1 (_AT(pteval_t, 2) << 4)
#define PTE_EXT_AP_UNO_SRO (_AT(pteval_t, 0) << 4)
#define PTE_EXT_AP_UNO_SRW (PTE_EXT_AP0)
#define PTE_EXT_AP_URO_SRW (PTE_EXT_AP1)
#define PTE_EXT_AP_URW_SRW (PTE_EXT_AP1|PTE_EXT_AP0)
#define PTE_EXT_TEX(x) (_AT(pteval_t, (x)) << 6) /* v5 */
#define PTE_EXT_APX (_AT(pteval_t, 1) << 9) /* v6 */
#define PTE_EXT_COHERENT (_AT(pteval_t, 1) << 9) /* XScale3 */
#define PTE_EXT_SHARED (_AT(pteval_t, 1) << 10) /* v6 */
#define PTE_EXT_NG (_AT(pteval_t, 1) << 11) /* v6 */
linux內核定義的PTE頁面表相關的軟體標誌位如下:
/*
* "Linux" PTE definitions.
*
* We keep two sets of PTEs - the hardware and the linux version.
* This allows greater flexibility in the way we map the Linux bits
* onto the hardware tables, and allows us to have YOUNG and DIRTY
* bits.
*
* The PTE table pointer refers to the hardware entries; the "Linux"
* entries are stored 1024 bytes below.
*/
#define L_PTE_VALID (_AT(pteval_t, 1) << 0) /* Valid */
#define L_PTE_PRESENT (_AT(pteval_t, 1) << 0)
#define L_PTE_YOUNG (_AT(pteval_t, 1) << 1)
#define L_PTE_DIRTY (_AT(pteval_t, 1) << 6)
#define L_PTE_RDONLY (_AT(pteval_t, 1) << 7)
#define L_PTE_USER (_AT(pteval_t, 1) << 8)
#define L_PTE_XN (_AT(pteval_t, 1) << 9)
#define L_PTE_SHARED (_AT(pteval_t, 1) << 10) /* shared(v6), coherent(xsc3) */
#define L_PTE_NONE (_AT(pteval_t, 1) << 11)
第9~10行代碼設置ARM硬體頁表的PTE_EXT_TEX比特位;
第13~15行代碼設置ARM硬體頁表的PTE_EXT_APX比特位;
第17~18行代碼設置ARM硬體頁表的PTE_EXT_AP1比特位;
第20~21行代碼設置ARM硬體頁表的PTE_EXT_XN比特位;
第23~27行代碼,在舊版本中的linux內核代碼中(例如linux3.7),等同如下代碼片段:
tst r1, #L_PTE_YOUNG
tstne r1, #L_PTE_PRESENT
moveq r3, #0
如果沒有設置L_PTE_YOUNG並且L_PTE_PRESENT置位,那就保持Linux版本的頁表不變,把ARM32硬體版本的頁表表項內容清零。代碼中的L_PTE_VAILD和L_PTE_NONE這兩個軟體比特位是後來添加的,因此在linux3.7及以前內核版本中更加容易理解;
為什麼這裡要把ARM硬體版本的頁面表項內容清零呢?我們觀察ARM32硬體版本的頁面表的相關標誌位會發現,沒有表示被訪問和頁面在記憶體中的硬體標誌位。linux內核最早基於x86體繫結構設計的,所以linux內核關於頁表的許多術語和設計都是針對x86體系的,而ARM Linux只能從軟體架構上去跟隨了,因此設計了兩套頁表。在x86的頁表中有3個標誌位是ARM32硬體頁面表沒有提供的。
- PTE_DIRTY: CPU是寫操作時會設置該標誌位,表示該頁表被寫過,為臟頁;
- PTE_YOUNG: CPU訪問該頁時會設置該標誌位,在頁面換出的時候,如果該標誌位置位了,說明該頁剛被訪問過,頁面是young的,不適合把該頁換出,同事清除該標誌位;
- PTE_PRESENT:表示該頁在記憶體中;
因此在ARM Linux中實現中需要模擬上述3個比特位;
如何模擬PTE_DIRTY呢?在ARM MMU硬體中為一個乾凈頁面建立映射時,設置硬體頁表項是只讀許可權的。當往一個乾凈頁表寫入時,會觸發寫許可權缺頁中斷(雖然linux版本的頁面表項標記了可寫許可權,但是ARM硬體頁面表項還不具有寫入許可權),那麼在缺頁中斷處理handle_pte_falut()會在該頁的linux版本中PTE頁面表項標記為"dirty",並且發現PTE頁表項內容改變了,ptep_set_access_flags()函數會把新的linux版本的頁表項內容寫入硬體頁表,從而實現模擬過程;