arm linux 进程页表,linux内存管理(五)arm页表

最近比较忙,好多天没更新了,后面可能还需要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、 如果异常向量表是放在高地址的,这应该算完事了。如果异常向量表是放在低地址的,还有一些处理。

你可能感兴趣的:(arm,linux,进程页表)