Linux 内存管理窥探(10):内存初始化(创建页表映射 create_mapping)
在内存的初始化阶段,在初始化页表后,调用相关的函数来创建映射:start_kernel() --> setup_arch() --> paging_init() --> map_lowmem() --> create_mapping()也就是说,在系统初始化的时候呢,已经根据系统的内存情况,建立好了所有低端内存的映射表,后续伙伴系统分配的内存便可以直接使用啦!!...
在内存的初始化阶段,在初始化页表后,调用相关的函数来创建映射:
start_kernel() --> setup_arch() --> paging_init() --> map_lowmem() --> create_mapping()
也就是说,在系统初始化的时候呢,已经根据系统的内存情况,建立好了所有低端内存的映射表,后续伙伴系统分配的内存便可以直接使用啦!!
1. 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; // 类型
};
这个好理解吧,给出一个虚拟地址起始地址,物理地址的起始帧号,以及映射的长度和类型即可。
2. __create_mapping()
这个关键函数,最后走到了 __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);
}
2.1 pgd_offset
首先做了一些数据的处理:虚拟地址的 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);
2.2 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 表这些的,不急,慢慢来):
2.3 alloc_init_pud
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 函数
2.4 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 函数
2.5 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 函数,让我们先走进去瞧瞧。
2.6 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 ,即一个页面,不浪费~~。
------------------------------------------------ 插播结束 -----------------------------------------------
好了,分配好了内存后,继续往前走哦。
2.7 __pmd_populate
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 后
2.8 pte_offset_kernel
从 __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 表项。
2.9 set_pte_ext
从 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 的值,同时设置了相关的属性。
更多推荐
所有评论(0)