经过上个星期的试验,我们觉得有必要关注Leon3的MMU。
硬件背景:
由于Leon3是Sparc架构,所以我查看了Sparc V8手册。Sparc的MMU大体结构是将32位虚拟地址分为4个段。(这里贴图不能直接贴,好麻烦!)
第一,二层为PTD:PTP为下一级页表的物理地址
PTD:(Page Table Descriptor)
|_________PTP_______________|__ET_|
31 2 1 0
第三层为PTE:PPN为36位物理地址的高24位(Leon3的PPN为20位)
PTE:(Page Table Entry )
|_________PPN___________|_________|
31 8 7 0
最后的当然就是偏移地址了。
具体的寻址过程和x86几乎一样,不过x86的cr3就变为一个特定的上下文寄存器,这个寄存器标识当前进程。
具体过程:
上下文寄存器从上下文表中获得当前进程的标识Root Pointer,Root Pointer指向当前进程的全局页表的物理地址PA,然后从PTD获得偏移量PTP。PA+PTP就是第二层页表的物理地址,以此类推,直到第三层。从指向的PTE中获得PPN也就是物理地址的前20位,然后再加上OFFSET的12位,最后得到相应的物理地址。
看看头文件的定义是否和手册说的一样。
../include/asm-sparc/pgtsrmmu.h
/* Number of contexts is implementation-dependent; 64k is the most we support */
#define SRMMU_MAX_CONTEXTS 65536
/* PMD_SHIFT determines the size of the area a second-level page table entry can map */
#define SRMMU_REAL_PMD_SHIFT 18
#define SRMMU_REAL_PMD_SIZE (1UL << SRMMU_REAL_PMD_SHIFT)
#define SRMMU_REAL_PMD_MASK (~(SRMMU_REAL_PMD_SIZE-1))
#define SRMMU_REAL_PMD_ALIGN(__addr) (((__addr)+SRMMU_REAL_PMD_SIZE-1)&SRMMU_REAL_PMD_MASK)
/* PGDIR_SHIFT determines what a third-level page table entry can map */
#define SRMMU_PGDIR_SHIFT 24
#define SRMMU_PGDIR_SIZE (1UL << SRMMU_PGDIR_SHIFT)
#define SRMMU_PGDIR_MASK (~(SRMMU_PGDIR_SIZE-1))
#define SRMMU_PGDIR_ALIGN(addr) (((addr)+SRMMU_PGDIR_SIZE-1)&SRMMU_PGDIR_MASK)
#define SRMMU_REAL_PTRS_PER_PTE 64
#define SRMMU_REAL_PTRS_PER_PMD 64
#define SRMMU_PTRS_PER_PGD 256
单从上面的定义,可以看出定义和手册说明是一样的。但是现实往往要想象的残酷,在头文件中作者(David Miller)提到了一个问题。原文是:
/*
* To support pagetables in highmem, Linux introduces APIs which
* return struct page* and generally manipulate page tables when
* they are not mapped into kernel space. Our hardware page tables
* are smaller than pages. We lump hardware tabes into big,
* page sized software tables.
*
* PMD_SHIFT determines the size of the area a second-level page
* table entry can map, and our pmd_t is 16 times larger than
* normal. The values which were once defined here are now
* generic for 4c and srmmu, so they're found in pgtable.h.
*/
大概的意思就是为了映射到高位内存,内存的页表大小必须作出调整。但是很遗憾他没有继续说明如何调整,我也找不到其他的文档说明。这也直接导致了没办法看懂源代码的一些页表操作。
后来也发现了另外一个问题,Linux从2.6.11开始,页表映射统一形式为四层:
pgd->pud->pmd->ptd。这真的让我很诧异,因为从头文件的定义怎样都看不出是分四层,而我们的内核是2.6.21.1。这是否说明这个内核Sparc部分处理MMU是有一定问题的?那么多问题,看来也只能是阅读源代码来解决了。
以我目前的水平,读内核源代码带来的痛苦远远比快乐要多。错综复杂的宏定义、言简意赅的内嵌汇编、微妙而陌生的Sparc架构都让我沮丧不已,放弃的念头曾经象春天的野草一样在我大脑疯长。不过老子说得好:合抱之木,生于毫末;九层之台,起于累土;千里之行,始于足下。唉,以此自勉吧。
我们主要关注的文件是../arch/sparc/srmmu.c。首先要认识一些类型转换宏(特别是:__pte、__pgd 和__pgprot)把一个无符号整数转换成所需的类型。这里值得注意的是__pmd被注释了。
../include/asm/page.h
typedef struct { unsigned long pte; } pte_t;
typedef struct { unsigned long iopte; } iopte_t;
typedef struct { unsigned long pmdv[16]; } pmd_t;
typedef struct { unsigned long pgd; } pgd_t;
typedef struct { unsigned long ctxd; } ctxd_t;
typedef struct { unsigned long pgprot; } pgprot_t;
typedef struct { unsigned long iopgprot; } iopgprot_t;
#define __pte(x) ((pte_t) { (x) } )
#define __iopte(x) ((iopte_t) { (x) } )
/* #define __pmd(x) ((pmd_t) { (x) } ) */ /* XXX procedure with loop */
#define __pgd(x) ((pgd_t) { (x) } )
#define __ctxd(x) ((ctxd_t) { (x) } )
#define __pgprot(x) ((pgprot_t) { (x) } )
#define __iopgprot(x) ((iopgprot_t) { (x) } )
另外的类型转换宏执行和以上宏相反的转换,即把上面提到的特殊的类型转换成一个无符号整数。
#define pte_val(x) ((x).pte)
#define iopte_val(x) ((x).iopte)
#define pmd_val(x) ((x).pmdv[0])
#define pgd_val(x) ((x).pgd)
#define ctxd_val(x) ((x).ctxd)
#define pgprot_val(x) ((x).pgprot)
#define iopgprot_val(x) ((x).iopgprot)
下面还有很多操作页表的宏和函数。不过这些函数用得并不多,但是理解它们对我们理解Sparc架构很有好处。
../arch/sparc/mm/srmmu.c
向一个pte页表写入指定的值。
static inline void srmmu_set_pte(pte_t *ptep, pte_t pteval)
{
srmmu_swap((unsigned long *)ptep, pte_val(pteval));
}
static inline unsigned long srmmu_swap(unsigned long *addr, unsigned long value)
{
__asm__ __volatile__(
"swap [%2], %0" :
"=&r" (value) :
"0" (value), "r" (addr));
return value;
}
汇编代码的意思也很简单,执行的语句就是Sparc的swap汇编指令,%2就是指addr,%0就是value。语句中的“0”也可以用r互换。
如果pte页表为0,返回1,否则为0。
static inline int srmmu_pte_none(pte_t pte)
{
return !(pte_val(pte) & 0xFFFFFFF);
}
利用标志位,检测页面是否pte页面,是返回1,否则为0。
static inline int srmmu_pte_present(pte_t pte)
{
return ((pte_val(pte) & SRMMU_ET_MASK) == SRMMU_ET_PTE);
}
检测pte页指向的物理页是否有执行和读的权限。
static inline int srmmu_pte_read(pte_t pte)
{
return !(pte_val(pte) & SRMMU_NOREAD);
}
清除相应pte页表的一个表项,由此禁止进程使用由该页表 项映射的线性地址。
static inline void srmmu_pte_clear(pte_t *ptep)
{
srmmu_set_pte(ptep, __pte(0));
}
如果pmd表为0,返回1,否则为0。
static inline int srmmu_pmd_none(pmd_t pmd)
{
return !(pmd_val(pmd) & 0xFFFFFFF);
}
利用标志位,检测页面是否pmd页面,是返回0,否则为1(yes is bad)。
static inline int srmmu_pmd_bad(pmd_t pmd)
{
return (pmd_val(pmd) & SRMMU_ET_MASK) != SRMMU_ET_PTD; }
利用标志位,检测页面是否pmd页面,是返回1,否则为0。
static inline int srmmu_pmd_present(pmd_t pmd)
{
return ((pmd_val(pmd) & SRMMU_ET_MASK) == SRMMU_ET_PTD);
}
清除相应pmd页表的一个表项,由此禁止进程使用由该页表 项映射的线性地址。只是代码的for循环实现的有点莫名其妙。强制转换为pte_t类型是为了重用srmmu_set_pte 函数。
是static inline void srmmu_pmd_clear(pmd_t *pmdp)
{
int i;
for (i = 0; i < PTRS_PER_PTE/SRMMU_REAL_PTRS_PER_PTE; i++)
srmmu_set_pte((pte_t *)&pmdp->pmdv[i], __pte(0));
}
pgtable.h:
#define PTRS_PER_PTE 1024
#define PTE_SIZE (PTRS_PER_PTE*4)
pgtsrmmu.h:
typedef struct { unsigned long pmdv[16]; } pmd_t;(page.h)
#define SRMMU_REAL_PTRS_PER_PTE 64
#define SRMMU_REAL_PTE_TABLE_SIZE (SRMMU_REAL_PTRS_PER_PTE*4)
如果pgd表为0,返回1,否则为0。
static inline int srmmu_pgd_none(pgd_t pgd)
{
return !(pgd_val(pgd) & 0xFFFFFFF);
}
利用标志位,检测页面是否pgd页面,是返回0,否则为1(yes is bad)。
static inline int srmmu_pgd_bad(pgd_t pgd)
{
return (pgd_val(pgd) & SRMMU_ET_MASK) != SRMMU_ET_PTD; }
利用标志位,检测页面是否pmd页面,是返回1,否则为0。
static inline int srmmu_pgd_present(pgd_t pgd)
{
return ((pgd_val(pgd) & SRMMU_ET_MASK) == SRMMU_ET_PTD);
}
清除相应pgd页表的一个表项,由此禁止进程使用由该页表 项映射的线性地址。
static inline void srmmu_pgd_clear(pgd_t * pgdp)
{
srmmu_set_pte((pte_t *)pgdp, __pte(0));
}
page.h:
typedef struct { unsigned long pgd; } pgd_t;
清除pte的读写标志。
static inline pte_t srmmu_pte_wrprotect(pte_t pte)
{
return __pte(pte_val(pte) & ~SRMMU_WRITE);
}
清除pte的dirty位。
static inline pte_t srmmu_pte_mkclean(pte_t pte)
{
return __pte(pte_val(pte) & ~SRMMU_DIRTY);
}
清除pte的Referenced位,意味着把此页标志为未访问。
static inline pte_t srmmu_pte_mkold(pte_t pte)
{
return __pte(pte_val(pte) & ~SRMMU_REF);
}
设置pte的读写位。
static inline pte_t srmmu_pte_mkwrite(pte_t pte)
{
return __pte(pte_val(pte) | SRMMU_WRITE);
}
设置pte的dirty位。
static inline pte_t srmmu_pte_mkdirty(pte_t pte)
{
return __pte(pte_val(pte) | SRMMU_DIRTY);
}
设置pte的Referenced位,意味着把此页标志为访问过。
static inline pte_t srmmu_pte_mkyoung(pte_t pte)
{
return __pte(pte_val(pte) | SRMMU_REF);
}