最近比较忙,好多天没更新了,后面可能还需要20多篇文章才能把整个内存管理讲透。
前面讲述了一个通用的页表机制,以及简单介绍了TLB。今天会介绍arm的页表机制,这里特指32位arm。
目前内核提供了一个选项CONFIG_PGTABLE_LEVELS来配置页表的级数。32位的嵌入式系统通常采用2级页表,因为2级页表就足够映射所有内存了。我们以头文件为线索,探寻arm的页表机制。
在arm平台下的pgtable.h文件中包含了以下头文件:
如果是支持MMU的情况(我们也只讨论支持MMU的情况):
#include
#include
#include
#include
#ifdef CONFIG_ARM_LPAE
#include
#else
#include
#endif
……
#include
LPAE表示large physical address enlarge,表示是否启动大物理地址扩展,32位系统一般没定义,所以我们包含的是2级页表头文件。两级页表没有PUD和PMD页目录,只有PGD和PTE。但linux采用的是4级页表,为了对linux表现出一致,arm做了一些封装。封装很简单,把PUD和PMD看成是PGD即可。
继续看下asm-generic/pgtable-nopud.h的定义。
#define __PAGETABLE_PUD_FOLDED 1定义PUD不存在的宏
typedef struct { pgd_t pgd; } pud_t;
#define PUD_SHIFT PGDIR_SHIFT
#define PTRS_PER_PUD 1
#define PUD_SIZE (1UL << PUD_SHIFT)
#define PUD_MASK (~(PUD_SIZE-1))
……
static inline pud_t * pud_offset(pgd_t * pgd, unsigned long address)
{
return (pud_t *)pgd;
}
……
可以看到,代码把PUD当做是PGD来处理。
看下的定义。
#define __PAGETABLE_PMD_FOLDED 定义了没有PMD的宏
……
#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))
……
static inline pmd_t *pmd_offset(pud_t *pud, unsigned long addr)
{
return (pmd_t *)pud;
}
……
可以看到,代码把PMD当做是PGD来处理。
从PGDIR_SHIFT等于21可以知道一级目录占用11位,也就是说一级目录有2048个目录项,但是一项占用8个字节,其实一项包含了两项,一个给linux软件用,一个给arm硬件用。二级目录占用8位,也就是说二级目录有512项。
这里可能很多人不理解什么叫一个给linux用,一个给硬件用。页表项或者页目录项不单单存放页地址或者页表地址,还有一些字段用于描述一些信息。但由于linux软件定义的和arm硬件定义的这些信息略有不同,所以需要各自维护一套页表。
Linux软件定义的页表项的标志位如下(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)
这些标志位的具体含义后面会分析。
而硬件定义的页目录项和页表项标志位定义
#ifdef CONFIG_ARM_LPAE
#include
#else
#include
#endif
继续看asm/pgtable-2level-hwdef.h。
asm/pgtable-2level-hwdef.h定义了硬件规定的一级描述符和二级描述的标志位,也就是页目录项和页表项的标志位,这些标志位需要结合硬件手册才能知道它的意思。
页目录地址
了解了arm的页表,我们很好奇,内核把这些页表和页目录存放在哪里?
每个进程都有自己的页表,进程的页目录地址存放于进程的mm_struct结构体中的pgd中。关于mm_strcut结构体后面会重点介绍。
内核的init进程的mm_struct结构体采用静态定义的方式,其页目录也是事先分配好的。
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),
.user_ns= &init_user_ns,
INIT_MM_CONTEXT(init_mm)
};
我们关注的重点是pgd成员的赋值:
.pgd= swapper_pg_dir,
我们看下swapper_pg_dir的定义。
下面的内容截取自Head.S :
/*
* swapper_pg_dir is the virtual address of the initial page table.
* We place the page tables 16K below KERNEL_RAM_VADDR. Therefore, we must
* make sure that KERNEL_RAM_VADDR is correctly set. Currently, we expect
* the least significant 16 bits to be 0×8000, but we could probably
* relax this restriction to KERNEL_RAM_VADDR >= PAGE_OFFSET + 0×4000.
*/
#define KERNEL_RAM_VADDR(PAGE_OFFSET + TEXT_OFFSET)
#if (KERNEL_RAM_VADDR & 0xffff) != 0×8000
#error KERNEL_RAM_VADDR must start at 0xXXXX8000
#endif
#ifdef CONFIG_ARM_LPAE
/* LPAE requires an additional page for the PGD */
#define PG_DIR_SIZE0x5000
#define PMD_ORDER 3
#else
#define PG_DIR_SIZE 0×4000
#define PMD_ORDER 2
#endif
.globlswapper_pg_dir
.equswapper_pg_dir, KERNEL_RAM_VADDR – PG_DIR_SIZE
.equ指令相当于赋值,.globl是把一个符号声明为全局符号,也就是哪里都可以访问。
head.S定义了一个全局数组swapper_pg_dir,它存放了内核页目录项PGD,
extern pgd_t swapper_pg_dir[PTRS_PER_PGD];
2级页表中PTRS_PER_PGD等于2048,也就是有2048个页目录项,记住每项是8字节,所以就是16K=0×4000。
TEXT_OFFSET是由编译时传递进来的宏,等于0×8000,也就是32K,就是说swapper_pg_dir位于3G位置16K以上的地方,内核运行起始地址下面16K的地方。注意这里指的是虚拟地址。KERNEL_RAM_VADDR是指内核运行的起始虚拟地址。
所以就有了这张图(看swpper_pg_dir的位置即可,其他分段现在你可能还不了解,后面介绍):
那普通进程的页目录pgd又是在什么时候创建的呢?那当然是在创建进程的时候创建啦!创建进程时会调用mm_init初始化进程的mm_struct结构体,mm_init会创建进程页目录。
mm_init:
if (mm_alloc_pgd(mm))
goto fail_nopgd;
static inline int mm_alloc_pgd(struct mm_struct *mm)
{
mm->pgd = pgd_alloc(mm);
if (unlikely(!mm->pgd))
return -ENOMEM;
return 0;
}
pgd_alloc函数比较简单,代码就不贴出来了,其主要流程是:
1、为页目录分配连续4页即16K的空间,因为页目录的大小是16K。这里的连续是指物理地址上的连续,因此进程的页目录一定是在低端内存创建的。前面还没有介绍过低端内存的概念,所谓的低端内存是指线性映射区。
为页目录分配空间的函数
#define __pgd_alloc()(pgd_t *) __get_free_pages(GFP_KERNEL, 2)
__get_free_pages函数用于在低端内存分配连续的页,第二个参数是页数,以2的阶为单位。这里2也就是4页。
2、清空全部页目录项,将init_struct的用户空间之后的页目录项拷贝到该pgd。这一步操作实际就是将内核空间(范围从TASK_SIZE到4G)的页目录项拷贝过来,因为进程的内核空间是公用的,所以页目录项自然也是相同的。
#define TASK_SIZE (UL(CONFIG_PAGE_OFFSET) – UL(SZ_16M))
TASK_SIZE宏表示用户空间的大小
3、 如果异常向量表是放在高地址的,这应该算完事了。如果异常向量表是放在低地址的,还有一些处理。