暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

linux内存管理(七)arm页表机制

二进制人生 2020-02-11
2818

微信公众号:二进制人生
专注于嵌入式linux开发。问题或建议,请发邮件至hjhvictory@163.com。
更新:2019/2/11,转载请注明出处。

内存管理系列文章:

内容整理自网络和自己的认知,旨在学习交流,请勿用于商业用途。
linux内存管理(一)开篇介绍
linux内存管理(二)两种内存架构和三种内存模型
linux内存管理(三)内存管理三级结构
linux内存管理(四)分页机制概述
linux内存管理(五)内存源头
linux内存管理(六)分页机制的演进
linux内存管理(七)arm页表机制
linux内存管理(八)页表最初的初始化--从内核启动的第一段代码谈起
linux内存管理(九)内存管理临时机制-memblock
linux内存管理(十)虚拟内存布局
linux内存管理(十一)arm低端内存映射
linux内存管理(十二)

内容目录

arm页表机制概述代码探索页目录地址普通进程页目录创建

arm页表机制概述

前面介绍了linux的页表机制,以及简单介绍了TLB。今天会介绍arm的页表机制,这里特指32位arm。本文部分参考

https://blog.csdn.net/geshifei/article/details/89574508。

目前内核提供了一个选项CONFIG_PGTABLE_LEVELS
来配置页表的级数。32位的嵌入式系统通常采用2级页表。MMU映射过程如下:

从上图可以得知,ARM MMU页表(称为硬件页表或hw pt)如下:

这个图从网上偷来的,有个错误就是页目录有4096项,所以左边数字最大应该是4095。

页表中的每一项称为一个entry,entry存放的是下一级页目录的物理地址值,PGD entry值指向2级页表(PTE页表),PTE entry值指向物理页。

我们上面叙述的是arm硬件MMU定义的页表机制,也就是说上面的映射过程由硬件实现。

由于以下两个原因,linux代码对图2的映射过程做了一些调整:

1)PTE entry中的低12位大部分被硬件使用了,没有多余的空位用于linux需要的“accessed”、“dirty”等标志位。
参考内核代码注释:Hardware-wise, we have a two level page table structure, where the first level has 4096 entries, and the second level has 256 entries.  Each entry is one 32-bit word.  Most of the bits in the second level entry are used by hardware, and there aren't any "accessed" and "dirty" bits。

下面是硬件MMU定义的页表项的内容:


对于表格下方的小页(small page),高20位用于表示物理页地址,低12位被征用于描述一些附加信息。

2)linux希望PTE页表本身也是一个页面大小。
参考内核代码注释:However, Linux also expects one "PTE" table per page, and at least a "dirty" bit.,但图1表明PTE页表是256*4 byte=1k大小。

针对上面提到的问题,linux做了一些处理,使内核中实现的页表能够满足硬件需求,最终的arm页表见图4。

对于图4,解释如下:

1)软件实现必须符合硬件要求。ARM要求4096个PGD entry、256个PTE entry。

解决:PGD每个entry为8 bytes,定义为pmdval_t pgd[2],故共2048*2=4096 PGD entry。ARM MMU用va的bit[31,20](见图1)在PGD 4096项中找到对应的entry,每个entry指向一个hw页表(见图4中pmdp)。每一个hw页表有256个entry,ARM MMU用va的bit[19,12]在hw页表中找到对应的entry。所以从硬件角度看,linux实现的arm页表,完全符合硬件要求。

2)Linux需要 "accessed" and "dirty"位。

解决:从图3中可以看出,PTE entry的低位已经被硬件占用,所以只能再复制出一份页表(称为linux页表或linux pt),图4的hw pt 0对应Linux pt 0,linux页表的低位被linux系统用来提供需要的 "accessed" and "dirty"位。hw pt由MMU使用,linux pt由操作系统使用。

3)Linux期望PTE页表占用1个page。

解决:ARM的hw pt为2564 bytes=1k,不满一个page大小。内核代码在实现上采用了一个小技巧,让一个PGD entry映射2个连续的hw pt,同时将对应的2个linux pt也组织在一起,共1k4=4k。

因为linux代码让PGD一次映射2个hw pt,所以软件需要做一些处理来实现这个目的。软件定义PGD表项为pmdval_t pgd[2],pgd[i]指向一个hw pt,所以PGD表项一共有4096/2=2048项,也就是说需要用bit[31,21]来寻址这2048项,所以pgtable-2level.h中定义了:#define PGDIR_SHIFT 21 (注意,图1中PGD偏移20bit,那是给硬件MMU用的,跟我们这里的软件偏移没有关系)。

看到这里,很多人还是糊里糊涂。我们重新梳捋一下。

页表项是按需分配的,只用这个地址用到的时候才会进行分配,且一次性分配完1个页目录项的所有页表项(也就是刚好分配一个页,这是前面说的设计)。前面说到为了折衷满足linux和硬件mmu各自的需求,一个页目录项包含1024个页表项,其中前面512个给linux用,后面512个给硬件mmu用。但是实际上硬件mmu的一个页目录项只包含256个页表项,为了折衷满足双方的需求,mmu不得不映射多一个页目录项,这样子两个页目录项就映射了512个页表项。而硬件mmu的两个页目录项在linux眼里就是1个页目录项,因为linux定义的一个页目录项占8个字节。

对于1个32位的虚拟地址,按照硬件mmu页表定义,前12位表示页目录项索引,后8位表示页表项索引。下面是两个32位地址的高20位,-表示我们不关心。

- - - - - - - - - - - 0      - - - - - - - 0

- - - - - - - - - - - 1      - - - - - - - 0

对于linux页表,前11位表示页目录项索引,后9位表示页表项索引,所以上面的两个地址变成:

- - - - - - - - - - -      0 - - - - - - - 0

- - - - - - - - - - -      1 - - - - - - - 0

可以看到完全是兼容的。不同的划分有不同的表示意义。上图表示的两个地址分别对应两个相邻的页目录项,因为它们的索引值只差1,而页表项索引都是0,中间实际间隔了256个页目录项。而下图的两个地址对应同一个页目录项,而他们的页表项索引一个是0,一个是256,中间也同样是间隔了256个页表项,殊途同归。

以下是截取arch/arm/include/asm/pgtable-2level.h的注释,它阐述了linux arm页表的实现机制:

/*
 * Hardware-wise, we have a two level page table structurewhere the first
 * level has 4096 entries, and the second level has 256 entries.  Each entry
 * is one 32-bit word.  Most of the bits in the second level entry are used
 * by hardware, and there aren't any "accessed" and "dirty" bits.
 *
 * Linux on the other hand has a three level page table structure, which can
 * be wrapped to fit a two level page table structure easily - using the PGD
 * and PTE only.  However, Linux also expects one "PTE" table per page, and
 * at least a "dirty" bit.
 *
 * Therefore, we tweak the implementation slightly - we tell Linux that we
 * have 2048 entries in the first level, each of which is 8 bytes (iow, two
 * hardware pointers to the second level.)  The second level contains two
 * hardware PTE tables arranged contiguously, preceded by Linux versions
 * which contain the state information Linux needs.  We, therefore, end up
 * with 512 entries in the "PTE" level.
 *
 * This leads to the page tables having the following layout:
 *
 *    pgd             pte
 * |        |
 * +--------+
 * |        |       +------------+ +0
 * +- - - - +       | Linux pt 0 |
 * |        |       +------------+ +1024
 * +--------+ +0    | Linux pt 1 |
 * |        |-----> +------------+ +2048
 * +- - - - + +4    |  h/w pt 0  |
 * |        |-----> +------------+ +3072
 * +--------+ +8    |  h/w pt 1  |
 * |        |       +------------+ +4096
 *
 * See L_PTE_xxx below for definitions of bits in the "Linux pt"and
 * PTE_xxx for definitions of bits appearing in the "h/w pt".
 *
 * PMD_xxx definitions refer to bits in the first level page table.
 *
 * The "dirty" bit is emulated by only granting hardware write permission
 * iff the page is marked "writable" and "dirty" in the Linux PTE.  This
 * means that a write to a clean page will cause a permission fault, and
 * the Linux MM layer will mark the page dirty via handle_pte_fault().
 * For the hardware to notice the permission change, the TLB entry must
 * be flushed, and ptep_set_access_flags() does that for us.
 *
 * The "accessed" or "young" bit is emulated by a similar method; we only
 * allow accesses to the page if the "young" bit is set.  Accesses to the
 * page will cause a fault, and handle_pte_fault() will set the young bit
 * for us as long as the page is marked present in the corresponding Linux
 * PTE entry.  Again, ptep_set_access_flags() will ensure that the TLB is
 * up to date.
 *
 * However, when the "young" bit is cleared, we deny access to the page
 * by clearing the hardware PTE.  Currently Linux does not flush the TLB
 * for us in this case, which means the TLB will retain the transation
 * until either the TLB entry is evicted under pressure, or a context
 * switch which changes the user space mapping occurs.
 */

代码探索

我们以头文件为线索,探寻arm的页表机制。

在arm平台下的pgtable.h文件中包含了以下头文件:

如果是支持MMU的情况(我们也只讨论支持MMU的情况):

#include <asm-generic/pgtable-nopud.h>
#include <asm/memory.h>
#include <asm/pgtable-hwdef.h>
#include <asm/tlbflush.h>

#ifdef CONFIG_ARM_LPAE
#include <asm/pgtable-3level.h>
#else
#include <asm/pgtable-2level.h>
#endif
……
#include <asm-generic/pgtable.h>

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 //pud页目项的个数,把整个pgd看成1个pud页目录项
#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当做是PUD来处理。

PGDIR_SHIFT
等于21可以知道一级目录占用11位,也就是说一级目录有2048个目录项。二级目录占用9位,也就是说二级目录有512项。

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)
......

可以看到都以"L_"开头这些标志位的具体含义后面会分析。

而硬件定义的页目录项和页表项标志位定义于asm/pgtable-hwdef.h

#ifdef CONFIG_ARM_LPAE
#include <asm/pgtable-3level-hwdef.h>
#else
#include <asm/pgtable-2level-hwdef.h>
#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),
    .arg_lock   =  __SPIN_LOCK_UNLOCKED(init_mm.arg_lock),
    .mmlist     = LIST_HEAD_INIT(init_mm.mmlist),
    .user_ns    = &init_user_ns,
    .cpu_bitmap = { [BITS_TO_LONGS(NR_CPUS)] = 0},
    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 0x8000, but we could probably
 * relax this restriction to KERNEL_RAM_VADDR >= PAGE_OFFSET + 0x4000.
 */

#define KERNEL_RAM_VADDR (PAGE_OFFSET + TEXT_OFFSET)
#if (KERNEL_RAM_VADDR & 0xffff) != 0x8000
#error KERNEL_RAM_VADDR must start at 0xXXXX8000
#endif

#ifdef CONFIG_ARM_LPAE
/* LPAE requires an additional page for the PGD */
#define PG_DIR_SIZE 0x5000
#define PMD_ORDER 3
#else
#define PG_DIR_SIZE 0x4000
#define PMD_ORDER 2
#endif

.globl swapper_pg_dir
.equ swapper_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=0x4000。

TEXT_OFFSET是由编译时传递进来的宏,等于0x8000,也就是32K,就是说swapper_pg_dir位于3G位置16K以上的地方,内核运行起始地址下面16K的地方。注意这里指的是虚拟地址。KERNEL_RAM_VADDR是指内核运行的起始虚拟地址。

所以就有了这张图(看swpper_pg_dir的位置即可,其他分段现在你可能还不了解,后面介绍):


这个图画得很好,将内核空间虚拟内存划分得清清楚楚,涵盖了内存管理的绝大部分知识点,我们后面还会继续拿来分析。

普通进程页目录创建

前面所讲的swpper_pg_dir变量存放的是内核进程也就是init进程页目录的虚拟地址。实际上每个进程都要维护自己的页表,因为它们都要映射内存。

那普通进程的页目录pgd又是在什么时候创建的呢?那当然是在创建进程的时候创建啦!创建进程时会调用mm_init初始化进程的mm_struct结构体,mm_init会创建进程页目录。

mm_init:

if (mm_alloc_pgd(mm))
    goto fail_nopgd;

mm_alloc_pgd从名字就可以知道是分配pgd:

static inline int mm_alloc_pgd(struct mm_struct *mm)
{
    mm->pgd = pgd_alloc(mm);
    if (unlikely(!mm->pgd))
        return -ENOMEMa;
    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页。与之配套使用的是页释放函数free_pages。关于页的管理分配机制我们会在后面重点介绍。

2、清空全部页目录项,将init_struct的用户空间之后的页目录项拷贝到该pgd。这一步操作实际就是将内核空间(范围从TASK_SIZE到4G)的页目录项拷贝过来,因为进程的内核空间是公用的,所以页目录项自然也是相同的。

#define TASK_SIZE (UL(CONFIG_PAGE_OFFSET) - UL(SZ_16M))

TASK_SIZE宏表示用户空间的大小,我们以往的认知是0-CONFIG_PAGE_OFFSET-4G即为内核空间,实际这里从CONFIG_PAGE_OFFSET向下多腾挪了16M的空间,为何如此,我们可以留个疑问。

3、 如果异常向量表是放在高地址的,那就算完事了。因为在拷贝内核空间页表的时候也顺带将异常向量的页表项拷贝过去了。如果异常向量表是放在低地址(异常向量放在0地址处),那么还需要为进程的虚拟地址0的页目录项分配页表项,将内核进程0地址开始的两个页表项的内容(即异常向量的物理地址)拷贝到该进程0地址对应的页表项,因为新创建的进程和内核进程异常向量是共用的。

回顾全文,我们知道了arm32使用二级页表机制,且区分成硬件页表和linux软件页表两套。对于linux软件页表,页目录占11位,页表占9位,页内偏移12位。也知道了内核进程页目录的地址,普通进程页目录的创建。要注意区分linux软件和arm mmu硬件定义的两套页表,下一节我们继续介绍页表项的创建。


每天进步一点点……

图 二进制人生公众号



文章转载自二进制人生,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论