Linux内存管理 paging_init解析

内存初始化代码分析(三):创建系统内存地址映射

Linux-内存管理子系统_yedushu的博客-CSDN博客_内存管理子系统

Memory Management - 随笔分类(第5页) - yooooooo - 博客园

Linux内存描述之概述--Linux内存管理(一) - yooooooo - 博客园

Linux内核子系统---内存管理子系统、进程管理子系统_ZhY_Rening的博客-CSDN博客_内核子系统

Linux内核分析(三)----初识linux内存管理子系统 - wrjvszq - 博客园

背景

  • Read the fucking source code! --By 鲁迅
  • A picture is worth a thousand words. --By 高尔基

说明:

  1. Kernel版本:4.14
  2. ARM64处理器,Contex-A53,双核
  3. 使用工具:Source Insight 3.5, Visio

1. 介绍

(二)Linux物理内存初始化中,可知在paging_init调用之前,存放Kernel ImageDTB的两段物理内存区域可以访问了(相应的页表已经建立好)。尽管物理内存已经通过memblock_add添加进系统,但是这部分的物理内存到虚拟内存的映射还没有建立,可以通过memblock_alloc分配一段物理内存,但是还不能访问,一切还需要等待paging_init的执行。最终页表建立好后,可以通过虚拟地址去访问最终的物理地址了。

按照惯例,先上图,来一张ARM64内核的内存布局图片吧,最终的布局如下所示:

开启探索之旅吧!

2. paging_init

paging_init源代码短小精悍,直接贴上来,分模块来介绍吧。

/*
 * paging_init() sets up the page tables, initialises the zone memory
 * maps and sets up the zero page.
 */
void __init paging_init(void)
{
	phys_addr_t pgd_phys = early_pgtable_alloc();   /********(mark 1)*******/
	pgd_t *pgd = pgd_set_fixmap(pgd_phys);

	map_kernel(pgd);                                        /********(mark 2)*******/
	map_mem(pgd);                                         /********(mark 3)*******/

	/*
	 * We want to reuse the original swapper_pg_dir so we don't have to
	 * communicate the new address to non-coherent secondaries in
	 * secondary_entry, and so cpu_switch_mm can generate the address with
	 * adrp+add rather than a load from some global variable.
	 *
	 * To do this we need to go via a temporary pgd.
	 */
	cpu_replace_ttbr1(__va(pgd_phys));                 /********(mark 4)*******/
	memcpy(swapper_pg_dir, pgd, PGD_SIZE);
	cpu_replace_ttbr1(lm_alias(swapper_pg_dir));

	pgd_clear_fixmap();
	memblock_free(pgd_phys, PAGE_SIZE);

	/*
	 * We only reuse the PGD from the swapper_pg_dir, not the pud + pmd
	 * allocated with it.
	 */
	memblock_free(__pa_symbol(swapper_pg_dir) + PAGE_SIZE,
		      SWAPPER_DIR_SIZE - PAGE_SIZE);
}
  • mark 1:分配一页大小的物理内存存放pgd
  • mark 2:将内核的各个段进行映射;
  • mark 3:将memblock子系统添加的物理内存进行映射;
  • mark 4:切换页表,并将新建立的页表内容替换swappper_pg_dir页表内容;

代码看起来费劲?图来了:

下边将对各个子模块进一步的分析。

3. early_pgtable_alloc

这个模块与FIX MAP映射区域相关,建议先阅读前文(二)Linux物理内存初始化
先上图:

FIX MAP的区域划分从图中可以看出来
本函数会先分配物理内存,然后借用之前的全局页表bm_pte,建立物理地址到虚拟地址的映射,这次映射的作用是为了去访问物理内存,把内存清零,所以它只是一个临时操作,操作完毕后,会调用pte_clear_fixmap()来清除映射。

early_pgtable_alloc之后,我们看到paging_init调用了pgd_set_fixmap函数,这个函数调用完后,通过memblock_alloc分配的物理内存,最终就会用来存放pgd table了,这片区域的内容最后也会拷贝到swapper_pg_dir中去。

4. map_kernel

map_kernel的主要工作是完成内核中各个段的映射,此外还包括了FIXADDR_START虚拟地址的映射,如下图:

映射完成之后,可以看一下具体各个段的区域,以我自己使用的平台为例:

这些地址信息也能从System.map文件中找到。

aarch64-linux-gnu-objdump -x vmlinux能查看更详细的地址信息。

5. map_mem

从函数名字中可以看出,map_mem主要完成的是物理内存的映射,这部分的物理内存是通过memblock_add添加到系统中的,当对应的memblock设置了MEMBLOCK_NOMAP的标志时,则不对其进行地址映射。
map_mem函数中,会遍历memblock中的各个块,然后调用__map_memblock来完成实际的映射操作。先来一张效果图:

map_mem都是将物理地址映射到线性区域中,我们也发现了Kernel Image中的text, rodata段映射了两次,原因是其他的子系统,比如hibernate,会映射到线性区域中,可能需要线性区域的地址来引用内核的text, rodata,映射的时候也会限制成了只读/不可执行,防止意外修改或执行。

map_kernelmap_mem函数中的页表映射,最终都是调用__create_pgd_mapping函数实现的:

总体来说,就是逐级页表建立映射关系,同时中间会进行权限的控制等。
细节不再赘述,代码结合图片阅读,效果会更佳噢。

6. 页表替换及内存释放

这部分代码不多,不上图了,看代码吧:

	/*
	 * We want to reuse the original swapper_pg_dir so we don't have to
	 * communicate the new address to non-coherent secondaries in
	 * secondary_entry, and so cpu_switch_mm can generate the address with
	 * adrp+add rather than a load from some global variable.
	 *
	 * To do this we need to go via a temporary pgd.
	 */
	cpu_replace_ttbr1(__va(pgd_phys));
	memcpy(swapper_pg_dir, pgd, PGD_SIZE);
	cpu_replace_ttbr1(lm_alias(swapper_pg_dir));

	pgd_clear_fixmap();
	memblock_free(pgd_phys, PAGE_SIZE);

	/*
	 * We only reuse the PGD from the swapper_pg_dir, not the pud + pmd
	 * allocated with it.
	 */
	memblock_free(__pa_symbol(swapper_pg_dir) + PAGE_SIZE,
		      SWAPPER_DIR_SIZE - PAGE_SIZE);

简单来说,将新建立好的pgd页表内容,拷贝到swapper_pg_dir中,也就是覆盖掉之前的临时页表了。当拷贝完成后,显而易见的是,我们可以把paging_init一开始分配的物理内存给释放掉。
此外,在之前的文章也分析过swapper_pg_dir页表存放的时候,是连续存放的pgd, pud, pmd等,现在只需要复用swapper_pg_dir,其余的当然也是可以释放的了。

好了,点到为止,前路漫漫,离Buddy System,Slab,Malloc以及各种内存的骚操作好像还有很远的样子,待续吧。

准备页表page_init

 Linux是为通用的操作系统而设计,为了便于移植需要抽象出一些硬件细节,在驱动代码中看到大量的抽象层的思想。内核中只有和硬件相关的代码才会单独实现,这样做便于移植和添加新硬件。

内核里所有进程和内核线程都共享1GB的地址空间,而每个应用程序对应的进程都有独立的3GB的地址空间,相互不干扰

用户空间:在Linux中,每个用户进程都可以访问4GB的线性地址空间,从0到3GB的虚拟地址空间是用户空间,每个用户进程通过自己的页目录,页表来直接访问
内核空间:从3GB到4GB的虚拟地址为内核空间,存放内核访问的代码和数据,用户进程不能访问,只有内核态进程才能访问。所有进程(包括用户进程,用户线程,内核线程)从3GB到4GB的虚拟地址空间内容都是一样的,Linux用该方式让内核进程共享代码段和数据段。
由于虚拟机制的引入,进程可以使用32位地址系统支持全部4G线性空间,进程的线性地址空间分为两部分:

从0x00000000到0xbfffffff的线性地址,无论用户态还是内核态的进程都可以寻址
从0xc0000000到0xffffffff的线性地址,只有内核态的进程能寻址
从前面章节的学习,通过内核临时页表的创建,相应的页表项已经建立号,只映射Kernel Image和DTB的物理内存,在某个还是的时候,内核需要将尽可能多的物理内存映射到页表中。尽管物理内存已经通过memblock_add添加进系统,但是这部分的物理内存到虚拟内存的映射还没有建立,可以通过memblock_alloc分配一段物理内存,但是还不能访问,一切还需要等待paging_init的执行。最终页表建立好后,可以通过虚拟地址去访问最终的物理地址了。

paging_init()负责建立仅用于kernel而用户空间不可访问的页表,我们主要来看看其做了些什么?

void __init paging_init(const struct machine_desc *mdesc)
{
    void *zero_page;

    build_mem_type_table();                              -----------------(1)
    prepare_page_table();                                -----------------(2)
    map_lowmem();                                        -----------------(3)
    memblock_set_current_limit(arm_lowmem_limit);        -----------------(4)
    dma_contiguous_remap();                              -----------------(5)
    early_fixmap_shutdown();                             
    devicemaps_init(mdesc);                              -----------------(6)
    kmap_init();                                         -----------------(7)
    tcm_init();                                          -----------------(8)

    top_pmd = pmd_off_k(0xffff0000);                     

    /* allocate the zero page. */
    zero_page = early_alloc(PAGE_SIZE);                 

    bootmem_init();                                      -----------------(9)

    empty_zero_page = virt_to_page(zero_page);           -----------------(10)
    __flush_dcache_page(NULL, empty_zero_page);
}



1.给静态全局变量mem_types赋值,这个变量就在本文件(arch/arm/mm/mmu.c)定义,它的用处就是在create_mapping函数创建映射时配置MMU硬件时需要。mem_types数组是kernel记录当前系统映射不同地址空间类型(普通内存 设备内存 IO空间等)的页表属性,其中页表属性还包括section-mapping的属性prot_sect,以及page-mapping的一级页目录属性prot_l1,二级页表属性prot_pte。这个都是与处理器相关的内容,后面章节不做介绍。
2.准备页表,主要是清除段映射(16K一级页表),将对应于内核映像下方以及内核空间的页目录项pmd段均清空为0
3.真正创建页表,重新建立从物理地址起始点到high_mem的起始点的一一映射
4.根据arm_lowmem_limit来作为ZONE_NORMAL的终点
5.建立DMA映射表
6.为设备IO空间和中断向量表创建页表,并刷新TLB和缓存
7.进行永久内存映射的初始化,存储在pkmap_page_table中,高64K是用来存放中断向量表
8.TCM初始化,TCM是一个固定大小的RAM,紧密地耦合至处理器内核,提供与cache相当的性能,相比于cache的优点是,程序代码可以精确地控制什么函数和代码放在哪里。
9.bootmem_init初始化内存管理
10.分配一个0页,该页用于写时复制机制。zero_page是全局变量,刷新D-CAHCE内容进RAM中。empty_zero_page是一个全局的页面数组,主要作用就是只要用户引用一个只读的匿名页面并没有进行写操作,缺页中断处理中内核就不会给用户进程分配新的页面。
1. 设置内存类型表
在build_mem_type_table函数中,根据ARM版本及内存类型,对struct mem_type结构体类型的全局数组mem_type进行初始化。mem_types结构体数组是具有L1、L2列表和缓存策略、域属性信息的内核数据结构。
为了按内存类型使用虚拟地址空间,Linux对内核使用的内存进行了分类,内核定义在arch/arm/mm/mmu.c中,其类型如下

内存类型    使用目的
MT_DEVICE    ARMV6的共享设备
MT_DEVICE_NONSHARED    ARMV6的f非共享设备
MT_DEVICE_CACHED    使用写缓冲及缓存设备
MT_DEVICE_WC    未使用写缓冲及缓存设备
根据内存使用的不同目的,内存类型对是否使用缓存,是否使用写缓冲,是否共享,域等信息定义了不同的设置,这里面只是列了部分。

2. 准备页表
在内核使用内存之前,需要初始化内核的页表,初始化页表主要在map_lowmem()函数中。在映射页表之前,需要把页表的页表项清零,主要在prepare_page_table()函数中实现。

static inline void prepare_page_table(void)
{
    unsigned long addr;
    phys_addr_t end;

    /*
     * Clear out all the mappings below the kernel image.
     */
    for (addr = 0; addr < MODULES_VADDR; addr += PMD_SIZE)                     ----------------(1)
        pmd_clear(pmd_off_k(addr));

#ifdef CONFIG_XIP_KERNEL
    /* The XIP kernel is mapped in the module area -- skip over it */
    addr = ((unsigned long)_exiprom + PMD_SIZE - 1) & PMD_MASK;               
#endif
    for ( ; addr < PAGE_OFFSET; addr += PMD_SIZE)                             ----------------(2)
        pmd_clear(pmd_off_k(addr));

    /*
     * Find the end of the first block of lowmem.
     */
    end = memblock.memory.regions[0].base + memblock.memory.regions[0].size;
    if (end >= arm_lowmem_limit)
        end = arm_lowmem_limit;

    /*
     * Clear out all the kernel space mappings, except for the first
     * memory bank, up to the vmalloc region.
     */
    for (addr = __phys_to_virt(end);                                         ----------------(3)
         addr < VMALLOC_START; addr += PMD_SIZE)
        pmd_clear(pmd_off_k(addr));
}
1


1.模块加载的范围应该是在MODULES_VADDR到MODULES_END之间,而MODULES_VADDR在文件arch/arm/include/asm/memory.h定义#define MODULES_VADDR (CONFIG_PAGE_OFFSET - SZ_8M),则该函数pmd_clear清理0~MODULES_VADDR所对应的一级页表项内容,所对应的地址为0x0 ~~~ bf000000

2.对PAGE_OFFSET之前的页目录项执行初始化,PAGE_OFFSET表示内核空间的起始地址,在32位系统地址空间最多为4G,在编译的时候通过Kconfig分为内核空间和用户空间,一般比为1:3,所以内核空间的起始地址为0xc000 0000。pmd_clear清理MODULES_VADDR~ PAGE_OFFSET 所对应的一级页表项内容,所对应的地址为 bf000000 ~~~ c0000000
通过下面的方式配置
config PAGE_OFFSET
hex
default PHYS_OFFSET if !MMU
default 0x40000000 if VMSPLIT_1G
default 0x80000000 if VMSPLIT_2G
default 0xB0000000 if VMSPLIT_3G_OPT
default 0xC0000000

3.pmd_clear清理第一个0xe0000000~0xe0800000所对应对应的8M空间的一级页表项内容

该函数是在建立完整页表前,需要对一级页目录进行清空操作,便于建立页表时,对空页表目录项进行分配。我们以imx6上模拟器为例,第一块也是唯一一块Membank是0x80000000起始地址,大小为512MB,arm_lowmem_limit也是0xa0000000。

为了初始化页目录项,需要获得要初始化的项地址,从上述的代码可以看出,pmd_clear函数将pmd_off_k函数作为输入值。正是通过pmd_off_k函数获得项地址

static inline pmd_t *pmd_off_k(unsigned long virt)
{
    return pmd_offset(pud_offset(pgd_offset_k(virt), virt), virt);
}

#define pgd_offset_k(address) pgd_offset(&init_mm, (address))
#define pgd_offset(mm, addr)    ((mm)->pgd + pgd_index(addr))
/* to find an entry in a page-table-directory */
#define pgd_index(addr)        ((addr) >> PGDIR_SHIFT)
1

9
pdg_offset_k调用pgd_offset函数,传递的参数init_mm地址,pgd_index将输入地址addr以PGDIR_SHIFT的大小向右移动,因此会求出对应于输入地址的页目录项号,并通过pdg_offset获得管理ADDR所属内存块的页目录的相应项地址。

Init_mm根据INIT_MM进行初始化,其定义如下

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)
};
1
2

成员    说明
mm_rb    虚拟内存各个区域用vm_area_struct进行说明,而进程区域用2中方式排列,单链表和红黑树方式,mm_rb指向红黑树的root节点全局页目录,指向页目录
pgd    swapper_pg_dir全局页目录,指向页目录
mm_users    表示使用该内存管理结构体的处理器数量
mm_count    用于mm_struct的计数,mm_user=mm_count+1
mmap_sem    读写信号量变量
page_table_lock    用于保护页表或计数器值的自旋锁变量
mmlist    系统内所有mm_struct连接到双向链表,其第一个节点通过INIT_MM()进行初始化
cpu_vm_mask    以相同内CPU数量位cpumask_t类型的cpu_vm_mask
下面我们接着看以下pmd_clear函数

#define pmd_clear(pmdp)            \
    do {                \
        pmdp[0] = __pmd(0);    \
        pmdp[1] = __pmd(0);    \
        clean_pmd_entry(pmdp);    \
    } while (0)
1
2

传递给pmd_clear的参数pmdp为2M单位,并且将pmdp分为2个,并初始化为0,之后变更了页目录值,因此调用clean_pmd_entry函数

static inline void clean_pmd_entry(void *pmd)
{
    const unsigned int __tlb_flag = __cpu_tlb_flags;

    tlb_op(TLB_DCLEAN, "c7, c10, 1    @ flush_pmd", pmd);
    tlb_l2_op(TLB_L2CLEAN_FR, "c15, c9, 1  @ L2 flush_pmd", pmd);
}
1
2
在代码中清空对应于虚拟地址pmd的MMU数据缓存,总而言之,prepare_page_table函数的作用是将页目录项的pmd段初始化为0,其对应的关系如下图所示

在这里插入图片描述

那么从图中可以看出prepare_page_table完成了以下的工作

1 对虚拟地址0到MODULES_VADDR(0xc0000000以下8MB或16MB的地址)的一级页目录项进行清空
2 对MODULES_VADDR到PAGE_OFFSET(0xc0000000)的一级页目录项进行清空
3 对lowmem顶端到VMALLOC_START的一级页目录项进行清空

准备页表

Linux是为通用的操作系统而设计,为了便于移植需要抽象出一些硬件细节,在驱动代码中看到大量的抽象层的思想。内核中只有和硬件相关的代码才会单独实现,这样做便于移植和添加新硬件。

内核里所有进程和内核线程都共享1GB的地址空间,而每个应用程序对应的进程都有独立的3GB的地址空间,相互不干扰

用户空间:在Linux中,每个用户进程都可以访问4GB的线性地址空间,从0到3GB的虚拟地址空间是用户空间,每个用户进程通过自己的页目录,页表来直接访问
内核空间:从3GB到4GB的虚拟地址为内核空间,存放内核访问的代码和数据,用户进程不能访问,只有内核态进程才能访问。所有进程(包括用户进程,用户线程,内核线程)从3GB到4GB的虚拟地址空间内容都是一样的,Linux用该方式让内核进程共享代码段和数据段。
由于虚拟机制的引入,进程可以使用32位地址系统支持全部4G线性空间,进程的线性地址空间分为两部分:

从0x00000000到0xbfffffff的线性地址,无论用户态还是内核态的进程都可以寻址
从0xc0000000到0xffffffff的线性地址,只有内核态的进程能寻址
从前面章节的学习,通过内核临时页表的创建,相应的页表项已经建立号,只映射Kernel Image和DTB的物理内存,在某个还是的时候,内核需要将尽可能多的物理内存映射到页表中。尽管物理内存已经通过memblock_add添加进系统,但是这部分的物理内存到虚拟内存的映射还没有建立,可以通过memblock_alloc分配一段物理内存,但是还不能访问,一切还需要等待paging_init的执行。最终页表建立好后,可以通过虚拟地址去访问最终的物理地址了。

paging_init()负责建立仅用于kernel而用户空间不可访问的页表,我们主要来看看其做了些什么?

void __init paging_init(const struct machine_desc *mdesc)
{
    void *zero_page;

    build_mem_type_table();                              -----------------(1)
    prepare_page_table();                                -----------------(2)
    map_lowmem();                                        -----------------(3)
    memblock_set_current_limit(arm_lowmem_limit);        -----------------(4)
    dma_contiguous_remap();                              -----------------(5)
    early_fixmap_shutdown();                             
    devicemaps_init(mdesc);                              -----------------(6)
    kmap_init();                                         -----------------(7)
    tcm_init();                                          -----------------(8)

    top_pmd = pmd_off_k(0xffff0000);                     

    /* allocate the zero page. */
    zero_page = early_alloc(PAGE_SIZE);                 

    bootmem_init();                                      -----------------(9)

    empty_zero_page = virt_to_page(zero_page);           -----------------(10)
    __flush_dcache_page(NULL, empty_zero_page);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
1.给静态全局变量mem_types赋值,这个变量就在本文件(arch/arm/mm/mmu.c)定义,它的用处就是在create_mapping函数创建映射时配置MMU硬件时需要。mem_types数组是kernel记录当前系统映射不同地址空间类型(普通内存 设备内存 IO空间等)的页表属性,其中页表属性还包括section-mapping的属性prot_sect,以及page-mapping的一级页目录属性prot_l1,二级页表属性prot_pte。这个都是与处理器相关的内容,后面章节不做介绍。
2.准备页表,主要是清除段映射(16K一级页表),将对应于内核映像下方以及内核空间的页目录项pmd段均清空为0
3.真正创建页表,重新建立从物理地址起始点到high_mem的起始点的一一映射
4.根据arm_lowmem_limit来作为ZONE_NORMAL的终点
5.建立DMA映射表
6.为设备IO空间和中断向量表创建页表,并刷新TLB和缓存
7.进行永久内存映射的初始化,存储在pkmap_page_table中,高64K是用来存放中断向量表
8.TCM初始化,TCM是一个固定大小的RAM,紧密地耦合至处理器内核,提供与cache相当的性能,相比于cache的优点是,程序代码可以精确地控制什么函数和代码放在哪里。
9.bootmem_init初始化内存管理
10.分配一个0页,该页用于写时复制机制。zero_page是全局变量,刷新D-CAHCE内容进RAM中。empty_zero_page是一个全局的页面数组,主要作用就是只要用户引用一个只读的匿名页面并没有进行写操作,缺页中断处理中内核就不会给用户进程分配新的页面。
1. 设置内存类型表
在build_mem_type_table函数中,根据ARM版本及内存类型,对struct mem_type结构体类型的全局数组mem_type进行初始化。mem_types结构体数组是具有L1、L2列表和缓存策略、域属性信息的内核数据结构。
为了按内存类型使用虚拟地址空间,Linux对内核使用的内存进行了分类,内核定义在arch/arm/mm/mmu.c中,其类型如下

内存类型    使用目的
MT_DEVICE    ARMV6的共享设备
MT_DEVICE_NONSHARED    ARMV6的f非共享设备
MT_DEVICE_CACHED    使用写缓冲及缓存设备
MT_DEVICE_WC    未使用写缓冲及缓存设备
根据内存使用的不同目的,内存类型对是否使用缓存,是否使用写缓冲,是否共享,域等信息定义了不同的设置,这里面只是列了部分。

2. 准备页表
在内核使用内存之前,需要初始化内核的页表,初始化页表主要在map_lowmem()函数中。在映射页表之前,需要把页表的页表项清零,主要在prepare_page_table()函数中实现。

static inline void prepare_page_table(void)
{
    unsigned long addr;
    phys_addr_t end;

    /*
     * Clear out all the mappings below the kernel image.
     */
    for (addr = 0; addr < MODULES_VADDR; addr += PMD_SIZE)                     ----------------(1)
        pmd_clear(pmd_off_k(addr));

#ifdef CONFIG_XIP_KERNEL
    /* The XIP kernel is mapped in the module area -- skip over it */
    addr = ((unsigned long)_exiprom + PMD_SIZE - 1) & PMD_MASK;               
#endif
    for ( ; addr < PAGE_OFFSET; addr += PMD_SIZE)                             ----------------(2)
        pmd_clear(pmd_off_k(addr));

    /*
     * Find the end of the first block of lowmem.
     */
    end = memblock.memory.regions[0].base + memblock.memory.regions[0].size;
    if (end >= arm_lowmem_limit)
        end = arm_lowmem_limit;

    /*
     * Clear out all the kernel space mappings, except for the first
     * memory bank, up to the vmalloc region.
     */
    for (addr = __phys_to_virt(end);                                         ----------------(3)
         addr < VMALLOC_START; addr += PMD_SIZE)
        pmd_clear(pmd_off_k(addr));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
1.模块加载的范围应该是在MODULES_VADDR到MODULES_END之间,而MODULES_VADDR在文件arch/arm/include/asm/memory.h定义#define MODULES_VADDR (CONFIG_PAGE_OFFSET - SZ_8M),则该函数pmd_clear清理0~MODULES_VADDR所对应的一级页表项内容,所对应的地址为0x0 ~~~ bf000000

2.对PAGE_OFFSET之前的页目录项执行初始化,PAGE_OFFSET表示内核空间的起始地址,在32位系统地址空间最多为4G,在编译的时候通过Kconfig分为内核空间和用户空间,一般比为1:3,所以内核空间的起始地址为0xc000 0000。pmd_clear清理MODULES_VADDR~ PAGE_OFFSET 所对应的一级页表项内容,所对应的地址为 bf000000 ~~~ c0000000
通过下面的方式配置
config PAGE_OFFSET
hex
default PHYS_OFFSET if !MMU
default 0x40000000 if VMSPLIT_1G
default 0x80000000 if VMSPLIT_2G
default 0xB0000000 if VMSPLIT_3G_OPT
default 0xC0000000

3.pmd_clear清理第一个0xe0000000~0xe0800000所对应对应的8M空间的一级页表项内容

该函数是在建立完整页表前,需要对一级页目录进行清空操作,便于建立页表时,对空页表目录项进行分配。我们以imx6上模拟器为例,第一块也是唯一一块Membank是0x80000000起始地址,大小为512MB,arm_lowmem_limit也是0xa0000000。

为了初始化页目录项,需要获得要初始化的项地址,从上述的代码可以看出,pmd_clear函数将pmd_off_k函数作为输入值。正是通过pmd_off_k函数获得项地址

static inline pmd_t *pmd_off_k(unsigned long virt)
{
    return pmd_offset(pud_offset(pgd_offset_k(virt), virt), virt);
}

#define pgd_offset_k(address) pgd_offset(&init_mm, (address))
#define pgd_offset(mm, addr)    ((mm)->pgd + pgd_index(addr))
/* to find an entry in a page-table-directory */
#define pgd_index(addr)        ((addr) >> PGDIR_SHIFT)
1
2
3
4
5
6
7
8
9
pdg_offset_k调用pgd_offset函数,传递的参数init_mm地址,pgd_index将输入地址addr以PGDIR_SHIFT的大小向右移动,因此会求出对应于输入地址的页目录项号,并通过pdg_offset获得管理ADDR所属内存块的页目录的相应项地址。

Init_mm根据INIT_MM进行初始化,其定义如下

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)
};

成员    说明
mm_rb    虚拟内存各个区域用vm_area_struct进行说明,而进程区域用2中方式排列,单链表和红黑树方式,mm_rb指向红黑树的root节点全局页目录,指向页目录
pgd    swapper_pg_dir全局页目录,指向页目录
mm_users    表示使用该内存管理结构体的处理器数量
mm_count    用于mm_struct的计数,mm_user=mm_count+1
mmap_sem    读写信号量变量
page_table_lock    用于保护页表或计数器值的自旋锁变量
mmlist    系统内所有mm_struct连接到双向链表,其第一个节点通过INIT_MM()进行初始化
cpu_vm_mask    以相同内CPU数量位cpumask_t类型的cpu_vm_mask
下面我们接着看以下pmd_clear函数

#define pmd_clear(pmdp)            \
    do {                \
        pmdp[0] = __pmd(0);    \
        pmdp[1] = __pmd(0);    \
        clean_pmd_entry(pmdp);    \
    } while (0)
1
2
3
4
5
6
传递给pmd_clear的参数pmdp为2M单位,并且将pmdp分为2个,并初始化为0,之后变更了页目录值,因此调用clean_pmd_entry函数

static inline void clean_pmd_entry(void *pmd)
{
    const unsigned int __tlb_flag = __cpu_tlb_flags;

    tlb_op(TLB_DCLEAN, "c7, c10, 1    @ flush_pmd", pmd);
    tlb_l2_op(TLB_L2CLEAN_FR, "c15, c9, 1  @ L2 flush_pmd", pmd);
}
1
2
3
4
5
6
7
在代码中清空对应于虚拟地址pmd的MMU数据缓存,总而言之,prepare_page_table函数的作用是将页目录项的pmd段初始化为0,其对应的关系如下图所示

那么从图中可以看出prepare_page_table完成了以下的工作

1 对虚拟地址0到MODULES_VADDR(0xc0000000以下8MB或16MB的地址)的一级页目录项进行清空
2 对MODULES_VADDR到PAGE_OFFSET(0xc0000000)的一级页目录项进行清空
3 对lowmem顶端到VMALLOC_START的一级页目录项进行清空

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值