ChinOS
https://github.com/jingjin666/GN-base/tree/chinos
页表需要多少空间
在第4节中,我们了解到,每一级页表实际上就是一个512大小的unsigned long数组,一个页表本身占用512*8=4K空间
/* PGD */
/* 每个ENTRY包含512G内存区域 */
typedef struct page_table
{
unsigned long entry[PTRS_PER_PGD];
} aligned_data(PAGE_SIZE) pgtable_pgd_t;
#if CONFIG_PGTABLE_LEVELS > 3
/* PUD */
/* 每个ENTRY包含1G内存区域 */
typedef struct pgtable_pud
{
unsigned long entry[PTRS_PER_PUD];
} aligned_data(PAGE_SIZE) pgtable_pud_t;
#endif
#if CONFIG_PGTABLE_LEVELS > 2
/* PMD */
/* 每个ENTRY包含2M内存区域 */
typedef struct pgtable_pmd
{
unsigned long entry[PTRS_PER_PMD];
} aligned_data(PAGE_SIZE) pgtable_pmd_t;
#endif
/* PTE */
/* 每个ENTRY包含4K内存区域 */
typedef struct pgtable_pte
{
unsigned long entry[PTRS_PER_PTE];
} aligned_data(PAGE_SIZE) pgtable_pte_t;
4K-48bit,4级页表
如果全部采用4K页映射,映射2M内存需要一个PTE页表=5128=4K空间,映射1G内存需要5124K=2M空间,映射1T内存需要10242M=2G内存,页表占用内存比为%0.2
如果全部采用2M段映射,映射1G内存只需要一个PMD页表=5128=4K空间,映射1T内存需要1024*4K=4M内存,页表占用内存比为%0.0004
4K-39bit,3级页表
在实际使用中,39位可以位我们提供512G空间的内存映射,这对我们来说已经够用了,所以在实际系统中,基本上都是采用4K-39bit,3级页表映射,这样PUD就被PGD替换
减少一级页表,除了能够节约空间以外,在MMU寻址速度上也可以减少一级的寻址时间,可以提高CPU寻址效率
配置MMU
在页表映射之前,我们需要根据需求对MMU做基本的配置
ID_AA64MMFR0_EL1
查询一下CPU支持的物理地址空间范围
/* 当前Core支持的物理地址范围 */
u64 mmfr0, pa_range;
MRS("ID_AA64MMFR0_EL1", mmfr0);
pa_range = bitfield_get(mmfr0, ID_AA64MMFR0_PARANGE_SHIFT, 4);
kprintf("ID_AA64MMFR0_EL1.PARANGE = 0x%lx\n", pa_range);
TCR_EL1
配置MMU的核心寄存器,包括物理地址空间范围,虚拟地址空间范围,颗粒度,以及内存共享和缓存属性
u64 tcr;
MRS("TCR_EL1", tcr);
kprintf("TCR_EL1 = 0x%lx\n", tcr);
MSR("TCR_EL1", (pa_range << TCR_IPS_SHIFT) | \
TCR_T0SZ(VA_BITS) | \
TCR_T1SZ(VA_BITS) | \
TCR_TG0_4K | \
TCR_TG1_4K | \
TCR_SH0_INNER | \
TCR_SH1_INNER | \
TCR_ORGN0_WBWA | \
TCR_ORGN1_WBWA | \
TCR_IRGN0_WBWA | \
TCR_IRGN1_WBWA);
MRS("TCR_EL1", tcr);
kprintf("TCR_EL1 = 0x%lx\n", tcr);
MAIR_EL1
Linux下预定义的几种内存属性
/*
* Memory types available.
*/
#define MT_DEVICE_nGnRnE 0
#define MT_DEVICE_nGnRE 1
#define MT_DEVICE_GRE 2
#define MT_NORMAL_NC 3
#define MT_NORMAL 4
#define MT_NORMAL_WT 5
#define MAIR_EL1_SET \
(MAIR_ATTRIDX(MAIR_ATTR_DEVICE_nGnRnE, MT_DEVICE_nGnRnE) | \
MAIR_ATTRIDX(MAIR_ATTR_DEVICE_nGnRE, MT_DEVICE_nGnRE) | \
MAIR_ATTRIDX(MAIR_ATTR_DEVICE_GRE, MT_DEVICE_GRE) | \
MAIR_ATTRIDX(MAIR_ATTR_NORMAL_NC, MT_NORMAL_NC) | \
MAIR_ATTRIDX(MAIR_ATTR_NORMAL, MT_NORMAL) | \
MAIR_ATTRIDX(MAIR_ATTR_NORMAL_WT, MT_NORMAL_WT))
配置内存属性
/* 内存属性标识 */
u64 mair;
MSR("MAIR_EL1", MAIR_EL1_SET);
MRS("MAIR_EL1", mair);
kprintf("MAIR_EL1 = 0x%lx\n", mair);
TTBRx_EL1
配置用户和内核的TTBR,也就是第0级的页表基地址,在系统启动阶段我们一般需要进行恒等映射,所以第一个页表就是恒等映射表identifymap_pg_dir
/* TTBR0 */
u64 ttbr0;
MSR("TTBR0_EL1", (u64)&identifymap_pg_dir);
MRS("TTBR0_EL1", ttbr0);
kprintf("TTBR0_EL1 = 0x%lx\n", ttbr0);
/* TTBR1 */
u64 ttbr1;
MSR("TTBR1_EL1", (u64)&identifymap_pg_dir);
MRS("TTBR1_EL1", ttbr1);
kprintf("TTBR1_EL1 = 0x%lx\n", ttbr1);
打开MMU
配置SCTLR_ELx寄存器,使能MMU
SCTLR_EL1
使能MMU
unsigned long sctlr;
MRS("sctlr_el1", sctlr);
kprintf("sctlr_el1 = %p\n", sctlr);
sctlr |= SCTLR_ELx_M;
MSR("sctlr_el1", sctlr);
isb();
dsb();
恒等映射
什么需要恒等映射
在打开mmu之前,我们一直使用的是物理地址,执行完这条指令后,CPU就开始工作在虚拟地址环境下,但是在实际代码里,后面任然还有一些代码和数据是工作在物理地址下的,这些代码要正常执行,我们就必须在页表中给他们建立虚拟地址和物理地址相等的页表映射,简称恒等映射
建立恒等映射
静态分配一个恒等映射表的一级页表identifymap_pg_dir
// 恒等映射的PGD页表
static BOOTDATA struct page_table identifymap_pg_dir;
页表的内存管理
在建立页表映射的过程中,二级和三级页表是动态分配的,所以我们需要一个管理页表空间的物理内存管理分配器,这个分配器不需要太复杂,能实现4K对齐的物理内存分配即可,这个分配器只服务于恒等映射页表的分配,后续内核的内存管理将不再使用!!!所以针对固定chunk大小的内存分配需求,我们可以用一个数组early_pgtable_mem来实现内存管理,数组的大小EARLY_PGTABLE_NR_PAGES代表可分配的页表个数为1024个,代表这个内存管理最大可分配1024个页表,数组元素为0代表内存可用,为1代表此内存已经被分配
#define EARLY_PGTABLE_NR_PAGES 1024
// 页表内存管理的数据结构
static BOOTDATA char early_pgtable_mem[EARLY_PGTABLE_NR_PAGES];
内存的管理的策略也很简单,遍历early_pgtable_mem数组,查询数组元素为0的数组index,然后返回页表内存即可,如果没有可用内存返回0
u64 BOOTPHYSIC early_pgtable_alloc(u64 size)
{
for (int i = 0; i < EARLY_PGTABLE_NR_PAGES; i++) {
if (early_pgtable_mem[i] == 0) {
early_pgtable_mem[i] = 1;
return (u64)&early_pgtable_space[i * PAGE_SIZE];
}
}
return 0;
}
静态分配页表的物理地址空间early_pgtable_space,大小为4M,和页表管理early_pgtable_mem对应
#define EARLY_PGTABLE_MEM_SIZE (PAGE_SIZE * EARLY_PGTABLE_NR_PAGES)
// 页表空间
static BOOTDATA char early_pgtable_space[EARLY_PGTABLE_MEM_SIZE];
4M空间够吗?
前面已经说了如果全部采取4K映射,4M空间可用映射2G内存,全部采用段氏映射,4M可用映射1T内存,对于恒等映射来说,完全足够了,一般恒等映射都采用段式映射
开始建立恒等映射
建立映射之前,我们需要知道映射的起始虚拟地址和物理地址,这个地址我们要从linker.lds中获取
ENTRY(_start);
SECTIONS
{
. = 0xffffff8000000000;
kernel_start = .;
boot_physic_start = .;
.boot.physic : AT(ADDR(.boot.physic) - 0xffffff8000000000 + 0x40000000)
{
*(.entry)
*(.boot.physic)
. = ALIGN(4096);
. += 4096;
boot_stack = .;
}
. = ALIGN(4096);
boot_physic_end = .;
.boot : AT(ADDR(.boot) - 0xffffff8000000000 + 0x40000000)
{
*(.boot);
}
.boot.data : AT(ADDR(.boot.data) - 0xffffff8000000000 + 0x40000000)
{
*(.boot.data);
}
. = ALIGN(4096);
boot_end = .;
.text : AT(ADDR(.text) - 0xffffff8000000000 + 0x40000000)
{
*(.text*)
}
.rodata : AT(ADDR(.rodata) - 0xffffff8000000000 + 0x40000000)
{
*(.rodata*)
}
. = ALIGN(4096);
.data : AT(ADDR(.data) - 0xffffff8000000000 + 0x40000000)
{
*(.data*)
}
. = ALIGN(4096);
.stack : AT(ADDR(.stack) - 0xffffff8000000000 + 0x40000000)
{
*(.stack*)
}
. = ALIGN(4096);
.bss : AT(ADDR(.bss) - 0xffffff8000000000 + 0x40000000)
{
*(.bss*)
}
. = ALIGN(4096);
kernel_end = .;
/DISCARD/ :
{
*(.note.gnu.build-id)
*(.comment)
}
}
我们只需要恒等映射boot_physic_start ~ boot_end中间的这一段内存,因为打开MMU的那一section(.entry)代码包含在这段区域。
static void BOOTPHYSIC boot_identify_mapping(void)
{
// 注意此刻内存环境都在物理地址空间下进行
extern unsigned long boot_physic_start;
extern unsigned long boot_end;
u64 vaddr, paddr, end, size, attr;
vaddr = (u64)&boot_physic_start;
paddr = (u64)&boot_physic_start;
end = (u64)&boot_end;
size = end - paddr;
attr = pgprot_val(PAGE_KERNEL_EXEC);
kprintf("%s:%d#pg_map: %p, %p, %p, %p\n", __FUNCTION__, __LINE__, vaddr, paddr, size, attr);
pg_map((pgd_t *)&identifymap_pg_dir, vaddr, paddr, size, attr, early_pgtable_alloc);
//dump_pgtable_verbose((pgd_t *)&identifymap_pg_dir);
}
注意:
为什么对boot_physic_start取地址,boot_physic_start定义在linker.lds中,查看反汇编你可以看到他的值为0xffffff8000000000,但此刻还没有开启MMU,在物理地址环境下,&boot_physic_start的值为0x40000000,同理&boot_end也是如此,这样我们就建立了一个[VA:PA] = [0x4000_0000 : 0x4000_0000]的恒等映射,这样打开MMU后,依旧可以正常执行后面的代码
内核空间映射
前面不是已经做了恒等映射了吗?为什么还要再次建立内核空间映射呢?
记得我们前面恒等映射只映射了seciotn{.boot.physic,.boot,.boot.data},当进入内核后,内核的代码和数据都分配在section{text,data,stack,bss},这些才是真正的内核代码和数据,当我们进入init_kernel后,就会去使用这些section,如果此刻不做映射,那么后面我们将无法正常访问。当然,一级页表当然还是使用恒等映射的那个一级页表identifymap_pg_dir
映射内核普通内存
普通内存,也就是linker.lds中的text,data,stack,bss,这些需要做映射,[VA:PA] = [0xffffff8000000000:0x40000000]
/* Kernel内存空间映射 */
const struct mem_region kernel_normal_ram[] = {
{
.pbase = 0x400000000,
.vbase = 0xffffff8000000000,
.size = 0,
},
{.size = 0}, // end of regions
};
// 映射内核普通内存
vaddr = kernel_normal_ram[0].vbase;
paddr = kernel_normal_ram[0].pbase;
size = (u64)&kernel_end - (u64)&kernel_start;
attr = pgprot_val(PAGE_KERNEL_EXEC);
kprintf("%s:%d#pg_map: %p, %p, %p, %p\n", __FUNCTION__, __LINE__, vaddr, paddr, size, attr);
pg_map((pgd_t *)&identifymap_pg_dir, vaddr, paddr, size, attr, early_pgtable_alloc);
注意:
这里的内核普通内存大小,根据(u64)&kernel_end - (u64)&kernel_start来获取,注意直接使用(&kernel_end - &kernel_start)会出错,一定要强制转换为u64再做操作
映射内核设备内存
设备内存,也就是MMIO,打开MMU后我们需要访问串口,中断等外设,这些需要做映射,[VA:PA] = [0xffffff8f00000000:0x00000000]
/* Kernel设备空间映射*/
const struct mem_region kernel_dev_ram[] = {
// MMIO
{
.pbase = 0,
.vbase = 0xffffff8f00000000,
.size = 0x40000000,
},
{.size = 0}, // end of regions
};
// 映射内核设备内存
vaddr = kernel_dev_ram[0].vbase;
paddr = kernel_dev_ram[0].pbase;
size = kernel_dev_ram[0].size;
attr = PROT_SECT_DEVICE_nGnRE;
kprintf("%s:%d#pg_map: %p, %p, %p, %p\n", __FUNCTION__, __LINE__, vaddr, paddr, size, attr);
pg_map((pgd_t *)&identifymap_pg_dir, vaddr, paddr, size, attr, early_pgtable_alloc);
真正进入虚拟地址空间
做好恒等映射和内核空间映射后,打开MMU,我们要想进入虚拟地址空间,那么我们跳转到一个虚拟地址mmu_enabled,注意这里不能再使用b/bl,我们需要绝对跳转,前面介绍过,需要使用br/blr来跳转,同时后面虚拟地址空间要用到C语言,也需要虚拟地址空间的堆栈,也就是把堆栈空间设置为虚拟地址空间,这里我们使用idlestack来作为虚拟地址空间的堆栈
// 设置MMU后设置C堆栈,注意此刻SP为虚拟地址
ldr x0, = idlestack
ldr x1, = CONFIG_IDLE_TASK_STACKSIZE
add x0, x0, x1
mov sp, x0
// 正式进入虚拟地址空间
ldr x2, = mmu_enabled
br x2
正式进入虚拟地址空间,后面的所有相对跳转,调用都是基于此虚拟地址来进行了
mmu_enabled:
bl init_kernel
// 跳转到idle
bl idle
b .
页表映射/寻址模拟器
想详细了解页表映射与CPU如何寻址的可以参考这个链接,来单步调试
https://github.com/jingjin666/mmu-map-address-simulator.git