在内存的初始化阶段,在初始化页表后,调用相关的函数来创建映射:
start_kernel() --> setup_arch() --> paging_init() --> map_lowmem() --> create_mapping()
先来看看这个函数的真身:
/* * Create the page directory entries and any necessary * page tables for the mapping specified by `md'. We * are able to cope here with varying sizes and address * offsets, and we take full advantage of sections and * supersections. */ static void __init create_mapping(struct map_desc *md) { if (md->virtual != vectors_base() && md->virtual < TASK_SIZE) { pr_warn("BUG: not creating mapping for 0x%08llx at 0x%08lx in user region\n", (long long)__pfn_to_phys((u64)md->pfn), md->virtual); return; } if ((md->type == MT_DEVICE || md->type == MT_ROM) && md->virtual >= PAGE_OFFSET && md->virtual < FIXADDR_START && (md->virtual < VMALLOC_START || md->virtual >= VMALLOC_END)) { pr_warn("BUG: mapping for 0x%08llx at 0x%08lx out of vmalloc space\n", (long long)__pfn_to_phys((u64)md->pfn), md->virtual); } __create_mapping(&init_mm, md, early_alloc, false); }
这个函数的入参是一个叫 md 的东东,这个玩意用来表征一个映射关系的结构:
struct map_desc { unsigned long virtual; // 虚拟地址 unsigned long pfn; // 物理地址起始帧号 unsigned long length; // 映射长度 unsigned int type; // 类型 };
这个好理解吧,给出一个虚拟地址起始地址,物理地址的起始帧号,以及映射的长度和类型即可。
这个关键函数,最后走到了 __create_mapping(),这个地方的入参比较多:
static void __init __create_mapping(struct mm_struct *mm, struct map_desc *md, void *(*alloc)(unsigned long sz), bool ng);
指定了 mm_struct 结构,然后和 map_desc 结构,同时指定了内存分配函数。因为在
create_mapping() --> __create_mapping() 的时候,其实传入的 alloc 分配函数,是 early_alloc ,也就是从 memblock 分配的,早期分配类型。同样的 mm_struct 结构 传入的是全局的 init_mm 指针。
我们接下来看这个 __create_mapping(&init_mm, md, early_alloc, false); 记住他的入参哈:
static void __init __create_mapping(struct mm_struct *mm, struct map_desc *md, void *(*alloc)(unsigned long sz), bool ng) { unsigned long addr, length, end; phys_addr_t phys; const struct mem_type *type; pgd_t *pgd; type = &mem_types[md->type]; #ifndef CONFIG_ARM_LPAE /* * Catch 36-bit addresses */ if (md->pfn >= 0x100000) { create_36bit_mapping(mm, md, type, ng); return; } #endif addr = md->virtual & PAGE_MASK; phys = __pfn_to_phys(md->pfn); length = PAGE_ALIGN(md->length + (md->virtual & ~PAGE_MASK)); if (type->prot_l1 == 0 && ((addr | phys | length) & ~SECTION_MASK)) { pr_warn("BUG: map for 0x%08llx at 0x%08lx can not be mapped using pages, ignoring.\n", (long long)__pfn_to_phys(md->pfn), addr); return; } pgd = pgd_offset(mm, addr); end = addr + length; do { unsigned long next = pgd_addr_end(addr, end); alloc_init_pud(pgd, addr, next, phys, type, alloc, ng); phys += next - addr; addr = next; } while (pgd++, addr != end); }
首先做了一些数据的处理:虚拟地址的 Mask,长度对其等等,接下来便是调用这个 pdg_offset 拉:
#define pgd_offset(mm, addr) ((mm)->pgd + pgd_index(addr))
还记得之前的入参么?这里的 mm->pgd 根据入参来展开,就是 init_mm->pgd ,这个定义:
/* * For dynamically allocated mm_structs, there is a dynamically sized cpumask * at the end of the structure, the size of which depends on the maximum CPU * number the system can see. That way we allocate only as much memory for * mm_cpumask() as needed for the hundreds, or thousands of processes that * a system typically runs. * * Since there is only one init_mm in the entire system, keep it simple * and size this cpu_bitmask to NR_CPUS. */ struct mm_struct init_mm = { .mm_rb = RB_ROOT, .pgd = swapper_pg_dir, // pgd 的入口 .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) };
这个 swapper_pg_dir 是 pgd 的入口,在汇编阶段就定义好了存放位置,在 arch/arm/kernel/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 .macro pgtbl, rd, phys add \rd, \phys, #TEXT_OFFSET sub \rd, \rd, #PG_DIR_SIZE .endm
所以他的位置在 KERNEL_RAM_VADDR - PG_DIR_SIZE 的地方。
KERNEL_RAM_VADDR 定义在上面,在 PAGE_OFFSET + TEXT_OFFSET 的位置
对于没有大物理地址扩展的情况下,PG_DIR_SIZE 为 0x4000 大小。
我们接下来看 pgd_offset 这个函数的意义:
他返回了 pgd 表的入口(swapper_pg_dir)加上一个 pgd_index(addr),这个 pgd_index(addr) 就是通过虚拟地址,转换到对应的 pgd 表项的索引值:
/* to find an entry in a page-table-directory */ #define pgd_index(addr) ((addr) >> PGDIR_SHIFT)
好了,这个通过这个宏,咱们可以定位到了这个虚拟地址开始的地方的 pgd 的地址了。也就是虚拟地址对应到的那个 pgd 。
接着看 _create_mapping():
pgd = pgd_offset(mm, addr); end = addr + length; do { unsigned long next = pgd_addr_end(addr, end); /// <---这里 alloc_init_pud(pgd, addr, next, phys, type, alloc, ng); phys += next - addr; addr = next; } while (pgd++, addr != end);
获取到了 addr 虚拟地址对应的 pgd 的位置后,在计算得到虚拟地址的结束地点,end = addr + length
在文件 include/asm-generic/pgtable.h 中:
/* * When walking page tables, get the address of the next boundary, * or the end address of the range if that comes earlier. Although no * vma end wraps to 0, rounded up __boundary may wrap to 0 throughout. */ #define pgd_addr_end(addr, end) \ ({ unsigned long __boundary = ((addr) + PGDIR_SIZE) & PGDIR_MASK; \ (__boundary - 1 < (end) - 1)? __boundary: (end); \ })
这里的定义,是取了 addr 为开始,PGDIR_SIZE 为步长,来进行 while 的循环。
这个 PGDIR_SIZE 的定义,在 pgtable-2level.h 中的定义为:
#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))
也就是 0x01 << 21 的值。这个地方为何是 21 的 SHIFT,后面在聊。
换言之,就是将虚拟地址到物理地址的地址映射关系,通过 N 次循环来对 pgd 表进行配置,每次循环都哦配置一个 pgd,那么循环次数怎么确定呢?当然需要看咱们的映射的时候,是否需要多个 pgd 啦,因为在 Linux 管理这些表的时候,pgd 的 OFFSET 是 0x01 << 21 开始的,所以,咱们就需要以这个为步长,来判断我们映射的地址是否需要更多的 pgd 表项。(不知道罗嗦这么多,讲清楚没)。
好了,那么我们假设映射的虚拟地址和物理地址的 Range 不大,那么一格 pgd 就能搞定,好,那么接下来接着这仅有的一次循环都做了些什么事情(猜测应该是配置 pte 表这些的,不急,慢慢来):
pgd = pgd_offset(mm, addr); end = addr + length; do { unsigned long next = pgd_addr_end(addr, end); alloc_init_pud(pgd, addr, next, phys, type, alloc, ng); // <--- 看这里 phys += next - addr; addr = next; } while (pgd++, addr != end);
接下来调用 alloc_init_pud 函数(其余的都是用于循环控制的,可以不管了)
他的代码实现是:
static void __init alloc_init_pud(pgd_t *pgd, unsigned long addr, unsigned long end, phys_addr_t phys, const struct mem_type *type, void *(*alloc)(unsigned long sz), bool ng) { pud_t *pud = pud_offset(pgd, addr); unsigned long next; do { next = pud_addr_end(addr, end); alloc_init_pmd(pud, addr, next, phys, type, alloc, ng); phys += next - addr; } while (pud++, addr = next, addr != end); }
这里首先是调用了 pud_offset 函数,入参是 pgd 和 addr:
#define pud_offset(pgd, start) (pgd)
直接返回 pgd,也就是说,没有 pud。
#define pud_addr_end(addr, end) (end)
这个也没有,相当于直接调用了 alloc_init_pmd 函数
这个函数的实现为:
static void __init alloc_init_pmd(pud_t *pud, unsigned long addr, unsigned long end, phys_addr_t phys, const struct mem_type *type, void *(*alloc)(unsigned long sz), bool ng) { pmd_t *pmd = pmd_offset(pud, addr); unsigned long next; do { /* * With LPAE, we must loop over to map * all the pmds for the given range. */ next = pmd_addr_end(addr, end); /* * Try a section mapping - addr, next and phys must all be * aligned to a section boundary. */ if (type->prot_sect && ((addr | next | phys) & ~SECTION_MASK) == 0) { __map_init_section(pmd, addr, next, phys, type, ng); } else { alloc_init_pte(pmd, addr, next, __phys_to_pfn(phys), type, alloc, ng); } phys += next - addr; } while (pmd++, addr = next, addr != end); }
在 ARM32 的 2-Level 页表映射中(arch/arm/include/asm/pgtable-2level.h):
static inline pmd_t *pmd_offset(pud_t *pud, unsigned long addr) { return (pmd_t *)pud; }
从前面的分析可知,pud 没有,这里的 pud 其实就是 pgd 了。
#define pmd_addr_end(addr, end) (end)
然后调用到了 alloc_init_pte 函数
这个是个关键函数了:
static void __init alloc_init_pte(pmd_t *pmd, unsigned long addr, unsigned long end, unsigned long pfn, const struct mem_type *type, void *(*alloc)(unsigned long sz), bool ng) { pte_t *pte = arm_pte_alloc(pmd, addr, type->prot_l1, alloc); // <--(1)先看这里 do { set_pte_ext(pte, pfn_pte(pfn, __pgprot(type->prot_pte)), ng ? PTE_EXT_NG : 0); pfn++; } while (pte++, addr += PAGE_SIZE, addr != end); }
这个函数进来首先进入了: arm_pte_alloc 函数,让我们先走进去瞧瞧。
static pte_t * __init arm_pte_alloc(pmd_t *pmd, unsigned long addr, unsigned long prot, void *(*alloc)(unsigned long sz)) { if (pmd_none(*pmd)) { pte_t *pte = alloc(PTE_HWTABLE_OFF + PTE_HWTABLE_SIZE); __pmd_populate(pmd, __pa(pte), prot); } BUG_ON(pmd_bad(*pmd)); return pte_offset_kernel(pmd, addr); }
首先使用宏来判断 pmd 中的数据是不是空的,显然之前看过了,pmd 就是 pud,就是 pgd,最开始没初始化的时候呢,这个地方的值就没有,所以进到了这个 if 里面的语句。
接下来就是使用这个 alloc 来进行内存的分配了,这个 alloc 是一个内存分配的函数指针,从最最开始的函数,也就是 __create_mapping 传进来的,回头看,其实就是使用早期的分配函数 memblock 来进行内存分配。
这里分配的空间是 == PTE_HWTABLE_OFF + PTE_HWTABLE_SIZE
在 pgtable-2level.h 中:
#define PTRS_PER_PTE 512 #define PTRS_PER_PMD 1 #define PTRS_PER_PGD 2048 #define PTE_HWTABLE_PTRS (PTRS_PER_PTE) #define PTE_HWTABLE_OFF (PTE_HWTABLE_PTRS * sizeof(pte_t)) #define PTE_HWTABLE_SIZE (PTRS_PER_PTE * sizeof(u32))
所以这里可以看到,分配了 512 + 512 个 pte,每个 pte 是 4个字节,也就是分配了 (512+512)×4 = 4K 的空间(1个page)。好了,聊到这里,插播一条重要的内容:
------------------------------------------------ 插播 ------------------------------------------------
这里分配 512 + 512 的原因:
1. 在 ARMv7-A 的处理器,处理器 MMU 支持的内存映射关系是:
也就是,最开始使用 va[31:20] 一共 12 bits 来表征 1 级表项的 index_1,va[19:12] 8 bits 表征 2级表项 index_2,也就是说,1 级表项一共有 2 的 12 次幂这么多个 entry,也就是 4096 个,2 级表项有 2 的 8 次幂个 entry,也就是 256 个。这个特性是 ARM 的 MMU 硬件特性。
2. Linux 页表
但是在 Linux 中呢,还记得么,那个 PGD 的 OFFSET 定义成为了 21,也就是 2048 个 pgd 条目,pte 定义了 512 个。这样不是和 ARM 的硬件定义不一样了么?那即便是这样,512 个,那为何在 alloc 分配的时候,分配了 2 个 512 呢?
我们看看 Linux 的 pgtable-2level.h 的部分代码注释:
/* * 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. * * 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. * * ...... */
这里说了他的原因,其实 Linux 使用了 2048 个 pgd entry,但是 each 是 8 bytes。其实是一样的。
针对二级页表呢,分配了 512 + 512 个,其实真正的 ARM MMU 的二级是 256 个,他们的对应关系如上面的简要的图所示,pgd 对应到了 h/w pt 0 和 h/w pt 1,他们都是 256 的(每个 pte 是 4 个 Bytes,所以图中看到是 1K 的 Step),另外的两个是 Linux OS 对页面的一些描述信息,同他们放到一起,正好组成了 4K ,即一个页面,不浪费~~。
------------------------------------------------ 插播结束 -----------------------------------------------
好了,分配好了内存后,继续往前走哦。
static inline void __pmd_populate(pmd_t *pmdp, phys_addr_t pte, pmdval_t prot) { pmdval_t pmdval = (pte + PTE_HWTABLE_OFF) | prot; pmdp[0] = __pmd(pmdval); #ifndef CONFIG_ARM_LPAE pmdp[1] = __pmd(pmdval + 256 * sizeof(pte_t)); #endif flush_pmd_entry(pmdp); }
这里,入参pmdp 是 pmd,pte 是刚刚从上面分配的 4K 的还很热乎的物理地址,然后呢,软件将 pte + PTE_HWTABLE_OFF | prot 这个咋个理解呢?还是看 Linux 官方的那个注释的图解:
* pgd pte * | | * +--------+ * | | +------------+ +0 * +- - - - + | Linux pt 0 | * | | +------------+ +1024 * +--------+ +0 | Linux pt 1 | * | |-----> +------------+ +2048 ------- PTE_HWTABLE_OFF * +- - - - + +4 | h/w pt 0 | * | |-----> +------------+ +3072 * +--------+ +8 | h/w pt 1 | * | | +------------+ +4096
这个 PTR_HWTABLE_OFF=512,每个pte 4个 Bytes,所以就是 2048 的位置咯 (复制很多遍,不过这里为了清晰,在搞一遍):
#define PTRS_PER_PTE 512 #define PTRS_PER_PMD 1 #define PTRS_PER_PGD 2048 #define PTE_HWTABLE_PTRS (PTRS_PER_PTE) #define PTE_HWTABLE_OFF (PTE_HWTABLE_PTRS * sizeof(pte_t)) #define PTE_HWTABLE_SIZE (PTRS_PER_PTE * sizeof(u32))
然后,这个 pmd,pud 都是指向的 pgd,所以,这里呢,把我们分配的 pte 的4K页面的中间位置的地址,交给了pgd,这就是 table walk 的一个逆过程(正向的是,硬件根据 pgd 来寻找 pte 的基地址,这里就赋值给了他)。
然后呢,与上来 prot,应该是一些属性,这个还没太搞懂呢,以后研究清楚了在补上。
最后调用 flush_pmd_entry,来刷 Flush a PMD entry,这个暂时也没太搞清楚,以后清楚了在补上。
好了,让我们在回到刚刚的地方,调用完这个分配 pte 后
从 __pmd_populate 返回后,会调用到 pte_offset_kernel :
#define pte_offset_kernel(pmd,addr) (pmd_page_vaddr(*(pmd)) + pte_index(addr))
这里,pmd 就是 pud 也就是 pgd,这里已经被 pte 的物理基地址填充过了
入参的 addr 是虚拟地址
#define pte_index(addr) (((addr) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))
获取 pte 的 index。
返回了相应的 PTE 表项。
从 arm_pte_alloc 函数返回到 alloc_init_pte 后,继续调用 set_pte_ext,这个和结构体系相关,在 ARMv7-A架构的处理器,在:
arch/arm/mm/proc-v7-2level.S
/* * cpu_v7_set_pte_ext(ptep, pte) * * Set a level 2 translation table entry. * * - ptep - pointer to level 2 translation table entry * (hardware version is stored at +2048 bytes) * - pte - PTE value to store * - ext - value for extended PTE bits */ ENTRY(cpu_v7_set_pte_ext) #ifdef CONFIG_MMU str r1, [r0] @ linux version bic r3, r1, #0x000003f0 bic r3, r3, #PTE_TYPE_MASK orr r3, r3, r2 orr r3, r3, #PTE_EXT_AP0 | 2 tst r1, #1 << 4 orrne r3, r3, #PTE_EXT_TEX(1) eor r1, r1, #L_PTE_DIRTY tst r1, #L_PTE_RDONLY | L_PTE_DIRTY orrne r3, r3, #PTE_EXT_APX tst r1, #L_PTE_USER orrne r3, r3, #PTE_EXT_AP1 tst r1, #L_PTE_XN orrne r3, r3, #PTE_EXT_XN tst r1, #L_PTE_YOUNG tstne r1, #L_PTE_VALID eorne r1, r1, #L_PTE_NONE tstne r1, #L_PTE_NONE moveq r3, #0 ARM( str r3, [r0, #2048]! ) // 写入页表 THUMB( add r0, r0, #2048 ) THUMB( str r3, [r0] ) ALT_SMP(W(nop)) ALT_UP (mcr p15, 0, r0, c7, c10, 1) @ flush_pte #endif bx lr ENDPROC(cpu_v7_set_pte_ext)
cpu_v7_set_pte_ext 中 r0 代表了入参第一个,即 pte 指针,(这里请注意,ARM+Linux 上的页表结构 r0+2048),r0 代表了 Linux 版本页面地址。接着设置了一些标志位。最后写入 pte 的页表,完成 pte 页表的初始化。
至此,整个 table walk 的逆过程完成,这个过程,根据需要创建映射的虚拟地址,物理地址,以及长度,来分配了物理的 pte 页面,并且设置了 pgd 到 pte 的关系,以及根据需要映射的相关地址,来设置了 pte 的值,同时设置了相关的属性。