Linux内核ARM32页表映射,在32bit的Linux内核中一般采用3层的映射模型,第1层是页面目录(PGD),第2层是页面中间目录(PMD),第3层才是页面映射表(PTE)。但在\2RM32
系统中只用到两层映射,因此在实际代码中就要在3层的映射模型中合并1层。在ARM32架构中,可以按段(section)来映射,这时采用单层映射模式。使用页面映射需要两层映射结构,页面的选择可以是64KB的大页面或4KB的小页面,如图所示。Linux内核通常默认使用4KB大小的小页面。
ARM32处理器查询页表
如果采用单层的段映射,内存中有个段映射表,表中有4096个表项,每个表项的大小是4Byte,所以这个段映射表的大小是16KB,而且其位置必须与16KB边界对齐。每个段表项可以寻址1MB大小的地址空间。当CPU访问内存时,32位虚拟地址的高12位(bit[31:20]
)用作访问段映射表的索引,从表中找到相应的表项。每个表项提供了一个12位的物理段地址,以及相应的标志位,如可读、可写等标志位。将这个12位物理地址和虚拟地址的低20位拼凑在一起,就得到32位的物理地址。
如果采用页表映射的方式,段映射表就变成一级映射表(First Level table,在Linux内核中称为PGD),其表项提供的不再是物理段地址,而是二级页表的基地址。32位虚拟地址的高12位(bit[31:20]
)作为访问一级页表的索引值,找到相应的表项,每个表项指向一个二级页表。以虚拟地址的次8位(bit[19:12])作为访问二级页表的索引值,得到相应的页表项,从这个页表项中找到20位的物理页面地址。最后将这20位物理页面地址和虚拟地址的低12位拼凑在一起,得到最终的32位物理地址。这个过程在ARM32架构中由MMU硬件完成,软件不需要接入。
[arch/arm/include/asm/pgtable-2level.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的偏移量应该从20位开始,为何这里的头文件定义从21位开始呢?
我们从ARM Linux内核建立具体内存区间的页面映射过程来看页表映射是如何实现的。create_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_|1;
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) /* 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)
#definePROT_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__|1
成员用于一级页表项的控制位和标志位,具体定义如下:
#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_CACHED] = { /* ioremap_cached */
.prot_pte = PROT_PTE_DEVICE | L_PTE_MT_DEV_CACHED,
.prot__|1 = 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__|1 = PMD_TYPE_TABLE,
.prot_sect = PROT_SECT_DEVICE,
.domain = DOMAIN_IO,
},
[MT_MEMORY_RWX] = {
.prot_pte = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY,
.prot_|1 = 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__|1 = PMD_TYPE_TABLE,
.prot_sect = PMD_TYPE_SECT | PMD_SECT_AP_WRITE,
.domain = DOMAIN_KERNEL,
},
};
这样一个map_desc数据结构就完整地描述了一个内存区间,调用create_mapping()时以此数据结构指针为调用参数。
[start_kernel()->setup_arch()->paging_init()->map_lowmem()->create_mapping]
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;
type = &mem_types[md->type];
addr = md->virtual & PAGE_MASK;
phys = __pfn_to_phys(md->pfn);
length = PAGE_ALIGN(md->length + (md->virtual & ~PAGE_MASK));
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);
}
在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数据结构来获取。
[mm/init-mm.c]
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等同了。
static inline pud_t * pud_offset(pgd_t * pgd, unsigned long address)
{
return (pud_t *)pgd;
}
static inline pmd_t *pmd_offset(pud_t *pud, unsigned long addr)
{
return (pmd_t *)pud;
}
因此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_|1);
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 * sizeof(pte_t))
#define PTE HWTABLE SIZE (PTRS PER PTE * sizeof(u32))
先回答刚才的问题:ARM结构中一级页表PGD的偏移量应该从20位开始,为何这里的头文件定义从21位开始呢?
- 这里分配了两个
\2TRS_PER_PTE
(512)个页面表项,也就是分配了两份页面表项。因为Linux内核默认的PGD是从21位开始,也就是bit[31:21]
,一共2048个一级页表项。而ARM32硬件结构中,PGD是从20位开始,页表项数目是4096,比Linux内核的要多一倍,那么代码实现上取巧了,以PTE_HWTABLE_OFF为偏移来写PGD表项。也就是在ARM Linux中,一个PGD页表项,映射512个PTE表项。而在真实硬件中,一个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);
pmdp[1] = __pmd(pmdval + 256 * sizeof(pte_t));
flush_pmd_entry(pmdp);
}
注意这里是把刚分配的1024个PTE页面表中的第512个页表项的地址作为基地址,再加上一些标志位信息prot作为页表项内容,写入上一级页表项PMD中。
相邻的两个二级页表的基地址分别写入PMD的页表项中的pmdp[0]和pmdp[1]指针中。
typedef struct { pmdval_t pgd[2]; } pgd_t;
/* 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_|1);
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架构的处理器,例如Cortex-A9,它的实现是在汇编函数cpu_v7_set_pte_ext中:
[arch/arm/mm/proc-v7-2level.S]
0 ENTRY(cpu_v7_set_pte_ext)
1 #ifdef CONFIG_MMU
2 str r1, [r0] @ linux version
3
4 bic r3, r1, #0x000003f0
5 bic r3, r3, #PTE_TYPE_MASK
6 orr r3, r3, r2
7 orr r3, r3, #PTE_EXT_AP0 | 2
8
9 tst r1, #1 << 4
10 orrne r3, r3, #PTE_EXT_TEX(1) //设置TEX
11
12
13 eor r1, r1, #L_PTE_DIRTY
14 tst r1, #L_PTE_RDONLY | L_PTE_DIRTY
15 orrne r3, r3, #PTE_EXT_APX //设置AP[2]
16
17 tst r1, #L_PTE_USER
18 orrne r3, r3, #PTE_EXT_AP1 //设置AP[1:0]
19
20 tst r1, #L_PTE_XN
21 orrne r3, r3, #PTE_EXT_XN //设置PXN位
22
23 tst r1, #L_PTE_YOUNG
24 tstne r1, #L_PTE_VALID
25 eorne r1, r1, #L_PTE_NONE
26 tstne r1, #L_PTE_NONE
27 moveq r3, #0
28
29 ARM( str r3, [r0, #2048]! ) //写入硬件页表,硬件页表在软件页表+2048Byte
30 ALT_SMP(W(nop))
31 ALT_UP (mcr p15, 0, r0, c7, c10, 1) @ flush_pte
32 #endif
33 bx lr
34 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页面表定义的标志位如下:
[arch/arm/include/asm/pgtable-2level-hwdef.h]
/*
* - 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页面表相关的软件标志位如下:
[arch/arm/include/asm/pgtable-2level.h]
/*
* "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内核代码中(例如Linux 3.7),等同于如下代码片段:
tst r1, #L_PTE_YOUNG
tstne r1, #L_PTE_PRESENT
moveq r3, #0
如果没有设置L_PTE_YOUNG并且L_PTE_PRESENT置位,那就保持Linux版本的页表不变,把\2RM32硬件版本的页面表项内容清零。代码中的\2_PTE_VALID
和L_PTE_NONE这两个软件比特位是后来添加的,因此在Linux 3.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_fault()中会在该页的Linux版本PTE页面表项标记为“dirty”,并且发现PTE页表项内容改变了,ptep_set_access_flags()函数会把新的Linux版本的页表项内容写入硬件页表,从而完成模拟过程。
酷客网相关文章:
评论前必须登录!
注册