解决方案

《从0开始写一个微内核操作系统》5-页表映射

seo靠我 2023-09-24 04:25:31

ChinOS

https://github.com/jingjin666/GN-base/tree/chinos

页表需要多少空间

在第4节中,我们了解到,每一级页表实际上就是一个512大小的unsigneSEO靠我d long数组,一个页表本身占用512*8=4K空间

/* PGD */ /* 每个ENTRY包含512G内存区域 */ typedef struct page_tabSEO靠我le {unsigned long entry[PTRS_PER_PGD]; } aligned_data(PAGE_SIZE) pgtable_pgd_t;#if CSEO靠我ONFIG_PGTABLE_LEVELS > 3 /* PUD */ /* 每个ENTRY包含1G内存区域 */ typedef struct pgtaSEO靠我ble_pud {unsigned long entry[PTRS_PER_PUD]; } aligned_data(PAGE_SIZE) pgtable_pud_t;SEO靠我 #endif#if CONFIG_PGTABLE_LEVELS > 2 /* PMD */ /* 每个ENTRY包含2M内存区域 */ SEO靠我 typedef struct pgtable_pmd {unsigned long entry[PTRS_PER_PMD]; } aligned_data(PAGE_SEO靠我SIZE) pgtable_pmd_t; #endif/* PTE */ /* 每个ENTRY包含4K内存区域 */ typedef struct pgSEO靠我table_pte {unsigned long entry[PTRS_PER_PTE]; } aligned_data(PAGE_SIZE) pgtable_pte_SEO靠我t;

4K-48bit,4级页表

如果全部采用4K页映射,映射2M内存需要一个PTE页表=5128=4K空间,映射1G内存需要5124K=2M空间,映射1T内存需要10242M=2G内存,页表占用内存比为SEO靠我%0.2

如果全部采用2M段映射,映射1G内存只需要一个PMD页表=5128=4K空间,映射1T内存需要1024*4K=4M内存,页表占用内存比为%0.0004

4K-39bit,3级页表

在实际使用中,3SEO靠我9位可以位我们提供512G空间的内存映射,这对我们来说已经够用了,所以在实际系统中,基本上都是采用4K-39bit,3级页表映射,这样PUD就被PGD替换

减少一级页表,除了能够节约空间以外,在MMU寻SEO靠我址速度上也可以减少一级的寻址时间,可以提高CPU寻址效率

配置MMU

在页表映射之前,我们需要根据需求对MMU做基本的配置

ID_AA64MMFR0_EL1

查询一下CPU支持的物理地址空间范围

/* 当前CoSEO靠我re支持的物理地址范围 */u64 mmfr0, pa_range;MRS("ID_AA64MMFR0_EL1", mmfr0);pa_range = bitfield_get(mmfr0, ID_ASEO靠我A64MMFR0_PARANGE_SHIFT, 4);kprintf("ID_AA64MMFR0_EL1.PARANGE = 0x%lx\n", pa_range);

TCR_EL1

配置MMU的核心寄存SEO靠我器,包括物理地址空间范围,虚拟地址空间范围,颗粒度,以及内存共享和缓存属性

u64 tcr;MRS("TCR_EL1", tcr);kprintf("TCR_EL1 = 0x%lx\n", tcr);MSEO靠我SR("TCR_EL1", (pa_range << TCR_IPS_SHIFT) | \TCR_T0SZ(VA_BITS) | \TCR_T1SZ(VA_BITS) | \TCR_TG0_4K | SEO靠我\TCR_TG1_4K | \TCR_SH0_INNER | \TCR_SH1_INNER | \TCR_ORGN0_WBWA | \TCR_ORGN1_WBWA | \TCR_IRGN0_WBWA SEO靠我| \TCR_IRGN1_WBWA);MRS("TCR_EL1", tcr);kprintf("TCR_EL1 = 0x%lx\n", tcr);

MAIR_EL1

Linux下预定义的几种内存属性

/**SEO靠我 Memory types available.*/ #define MT_DEVICE_nGnRnE 0 #define MT_DEVICE_nGnRE 1 SEO靠我 #define MT_DEVICE_GRE 2 #define MT_NORMAL_NC 3 #define MT_NORMAL 4 #defiSEO靠我ne MT_NORMAL_WT 5#define MAIR_EL1_SET \(MAIR_ATTRIDX(MAIR_ATTR_DEVICE_nGnRnE, MT_DEVICE_nGnRnE) | \MSEO靠我AIR_ATTRIDX(MAIR_ATTR_DEVICE_nGnRE, MT_DEVICE_nGnRE) | \MAIR_ATTRIDX(MAIR_ATTR_DEVICE_GRE, MT_DEVICESEO靠我_GRE) | \MAIR_ATTRIDX(MAIR_ATTR_NORMAL_NC, MT_NORMAL_NC) | \MAIR_ATTRIDX(MAIR_ATTR_NORMAL, MT_NORMALSEO靠我) | \MAIR_ATTRIDX(MAIR_ATTR_NORMAL_WT, MT_NORMAL_WT))

配置内存属性

/* 内存属性标识 */u64 mair;MSR("MAIR_EL1", MAIRSEO靠我_EL1_SET);MRS("MAIR_EL1", mair);kprintf("MAIR_EL1 = 0x%lx\n", mair);

TTBRx_EL1

配置用户和内核的TTBR,也就是第0级的页表基SEO靠我地址,在系统启动阶段我们一般需要进行恒等映射,所以第一个页表就是恒等映射表identifymap_pg_dir

/* TTBR0 */u64 ttbr0;MSR("TTBR0_EL1", (u64)&iSEO靠我dentifymap_pg_dir);MRS("TTBR0_EL1", ttbr0);kprintf("TTBR0_EL1 = 0x%lx\n", ttbr0);/* TTBR1 */u64 ttbrSEO靠我1;MSR("TTBR1_EL1", (u64)&identifymap_pg_dir);MRS("TTBR1_EL1", ttbr1);kprintf("TTBR1_EL1 = 0x%lx\n", SEO靠我ttbr1);

打开MMU

配置SCTLR_ELx寄存器,使能MMU

SCTLR_EL1

使能MMU

unsigned long sctlr;MRS("sctlr_el1", sctlr);kprintf("sSEO靠我ctlr_el1 = %p\n", sctlr);sctlr |= SCTLR_ELx_M;MSR("sctlr_el1", sctlr);isb();dsb();

恒等映射

什么需要恒等映射

在打开mmuSEO靠我之前,我们一直使用的是物理地址,执行完这条指令后,CPU就开始工作在虚拟地址环境下,但是在实际代码里,后面任然还有一些代码和数据是工作在物理地址下的,这些代码要正常执行,我们就必须在页表中给他们建立虚SEO靠我拟地址和物理地址相等的页表映射,简称恒等映射

建立恒等映射

静态分配一个恒等映射表的一级页表identifymap_pg_dir

// 恒等映射的PGD页表 static BOOTDATA SEO靠我struct page_table identifymap_pg_dir;

页表的内存管理

在建立页表映射的过程中,二级和三级页表是动态分配的,所以我们需要一个管理页表空间的物理内存管理分配器,这个分配器SEO靠我不需要太复杂,能实现4K对齐的物理内存分配即可,这个分配器只服务于恒等映射页表的分配,后续内核的内存管理将不再使用!!!所以针对固定chunk大小的内存分配需求,我们可以用一个数组early_pgtaSEO靠我ble_mem来实现内存管理,数组的大小EARLY_PGTABLE_NR_PAGES代表可分配的页表个数为1024个,代表这个内存管理最大可分配1024个页表,数组元素为0代表内存可用,为1代表此内存SEO靠我已经被分配

#define EARLY_PGTABLE_NR_PAGES 1024 // 页表内存管理的数据结构 static BOOTDATA char early_pSEO靠我gtable_mem[EARLY_PGTABLE_NR_PAGES];

内存的管理的策略也很简单,遍历early_pgtable_mem数组,查询数组元素为0的数组index,然后返回页表内存即可,如果SEO靠我没有可用内存返回0

u64 BOOTPHYSIC early_pgtable_alloc(u64 size) {for (int i = 0; i < EARLY_PGTABLE_NR_SEO靠我PAGES; i++) {if (early_pgtable_mem[i] == 0) {early_pgtable_mem[i] = 1;return (u64)&early_pgtable_spaSEO靠我ce[i * PAGE_SIZE];}}return 0; }

静态分配页表的物理地址空间early_pgtable_space,大小为4M,和页表管理early_pgtable_memSEO靠我对应

#define EARLY_PGTABLE_MEM_SIZE (PAGE_SIZE * EARLY_PGTABLE_NR_PAGES) // 页表空间 staticSEO靠我 BOOTDATA char early_pgtable_space[EARLY_PGTABLE_MEM_SIZE];

4M空间够吗?

前面已经说了如果全部采取4K映射,4M空间可用映射2G内存,全部采用SEO靠我段氏映射,4M可用映射1T内存,对于恒等映射来说,完全足够了,一般恒等映射都采用段式映射

开始建立恒等映射

建立映射之前,我们需要知道映射的起始虚拟地址和物理地址,这个地址我们要从linker.lds中获SEO靠我

ENTRY(_start);SECTIONS {. = 0xffffff8000000000;kernel_start = .;boot_physic_start = .;.bootSEO靠我.physic : AT(ADDR(.boot.physic) - 0xffffff8000000000 + 0x40000000){*(.entry)*(.boot.physic). = ALIGNSEO靠我(4096);. += 4096;boot_stack = .;}. = ALIGN(4096);boot_physic_end = .;.boot : AT(ADDR(.boot) - 0xffffSEO靠我ff8000000000 + 0x40000000){*(.boot);}.boot.data : AT(ADDR(.boot.data) - 0xffffff8000000000 + 0x40000SEO靠我000){*(.boot.data);}. = ALIGN(4096);boot_end = .;.text : AT(ADDR(.text) - 0xffffff8000000000 + 0x400SEO靠我00000){*(.text*)}.rodata : AT(ADDR(.rodata) - 0xffffff8000000000 + 0x40000000){*(.rodata*)}. = ALIGNSEO靠我(4096);.data : AT(ADDR(.data) - 0xffffff8000000000 + 0x40000000){*(.data*)}. = ALIGN(4096);.stack : SEO靠我AT(ADDR(.stack) - 0xffffff8000000000 + 0x40000000){*(.stack*)}. = ALIGN(4096);.bss : AT(ADDR(.bss) -SEO靠我 0xffffff8000000000 + 0x40000000){*(.bss*)}. = ALIGN(4096);kernel_end = .;/DISCARD/ :{*(.note.gnu.buSEO靠我ild-id)*(.comment)} }

我们只需要恒等映射boot_physic_start ~ boot_end中间的这一段内存,因为打开MMU的那一section(.entry)SEO靠我代码包含在这段区域。

static void BOOTPHYSIC boot_identify_mapping(void) {// 注意此刻内存环境都在物理地址空间下进行extern uSEO靠我nsigned long boot_physic_start;extern unsigned long boot_end;u64 vaddr, paddr, end, size, attr;vaddrSEO靠我 = (u64)&boot_physic_start;paddr = (u64)&boot_physic_start;end = (u64)&boot_end;size = end - paddr;aSEO靠我ttr = pgprot_val(PAGE_KERNEL_EXEC);kprintf("%s:%d#pg_map: %p, %p, %p, %p\n", __FUNCTION__, __LINE__,SEO靠我 vaddr, paddr, size, attr);pg_map((pgd_t *)&identifymap_pg_dir, vaddr, paddr, size, attr, early_pgtaSEO靠我ble_alloc);//dump_pgtable_verbose((pgd_t *)&identifymap_pg_dir); }

注意:

为什么对boot_physic_start取地SEO靠我址,boot_physic_start定义在linker.lds中,查看反汇编你可以看到他的值为0xffffff8000000000,但此刻还没有开启MMU,在物理地址环境下,&boot_physicSEO靠我_start的值为0x40000000,同理&boot_end也是如此,这样我们就建立了一个[VA:PA] = [0x4000_0000 : 0x4000_0000]的恒等映射,这样打开MMU后,依旧SEO靠我可以正常执行后面的代码

内核空间映射

前面不是已经做了恒等映射了吗?为什么还要再次建立内核空间映射呢?

记得我们前面恒等映射只映射了seciotn{.boot.physic,.boot,.boot.dataSEO靠我},当进入内核后,内核的代码和数据都分配在section{text,data,stack,bss},这些才是真正的内核代码和数据,当我们进入init_kernel后,就会去使用这些section,如果SEO靠我此刻不做映射,那么后面我们将无法正常访问。当然,一级页表当然还是使用恒等映射的那个一级页表identifymap_pg_dir

映射内核普通内存

普通内存,也就是linker.lds中的text,dataSEO靠我,stack,bss,这些需要做映射,[VA:PA] = [0xffffff8000000000:0x40000000]

/* Kernel内存空间映射 */ const struct SEO靠我mem_region kernel_normal_ram[] = {{.pbase = 0x400000000,.vbase = 0xffffff8000000000,.size = 0,},{.siSEO靠我ze = 0}, // end of regions };// 映射内核普通内存vaddr = kernel_normal_ram[0].vbase;paddr = kernel_normal_ramSEO靠我[0].pbase;size = (u64)&kernel_end - (u64)&kernel_start;attr = pgprot_val(PAGE_KERNEL_EXEC);kprintf("SEO靠我%s:%d#pg_map: %p, %p, %p, %p\n", __FUNCTION__, __LINE__, vaddr, paddr, size, attr);pg_map((pgd_t *)&SEO靠我identifymap_pg_dir, vaddr, paddr, size, attr, early_pgtable_alloc);

注意:

这里的内核普通内存大小,根据(u64)&kernel_endSEO靠我 - (u64)&kernel_start来获取,注意直接使用(&kernel_end - &kernel_start)会出错,一定要强制转换为u64再做操作

映射内核设备内存

设备内存,也就是MMIO,SEO靠我打开MMU后我们需要访问串口,中断等外设,这些需要做映射,[VA:PA] = [0xffffff8f00000000:0x00000000]

/* Kernel设备空间映射*/ const structSEO靠我 mem_region kernel_dev_ram[] = {// MMIO{.pbase = 0,.vbase = 0xffffff8f00000000,.size = 0x40000000,}SEO靠我,{.size = 0}, // end of regions };// 映射内核设备内存vaddr = kernel_dev_ram[0].vbase;paddr = kernel_dev_ram[SEO靠我0].pbase;size = kernel_dev_ram[0].size;attr = PROT_SECT_DEVICE_nGnRE;kprintf("%s:%d#pg_map: %p, %p, SEO靠我%p, %p\n", __FUNCTION__, __LINE__, vaddr, paddr, size, attr);pg_map((pgd_t *)&identifymap_pg_dir, vaSEO靠我ddr, paddr, size, attr, early_pgtable_alloc);

真正进入虚拟地址空间

做好恒等映射和内核空间映射后,打开MMU,我们要想进入虚拟地址空间,那么我们跳转到一个虚拟SEO靠我地址mmu_enabled,注意这里不能再使用b/bl,我们需要绝对跳转,前面介绍过,需要使用br/blr来跳转,同时后面虚拟地址空间要用到C语言,也需要虚拟地址空间的堆栈,也就是把堆栈空间设置为虚拟SEO靠我地址空间,这里我们使用idlestack来作为虚拟地址空间的堆栈

// 设置MMU后设置C堆栈,注意此刻SP为虚拟地址ldr x0, = idlestackldr x1, = CONFISEO靠我G_IDLE_TASK_STACKSIZEadd x0, x0, x1mov sp, x0// 正式进入虚拟地址空间ldr x2, = mmu_enabledbr xSEO靠我2

正式进入虚拟地址空间,后面的所有相对跳转,调用都是基于此虚拟地址来进行了

mmu_enabled:bl init_kernel// 跳转到idlebl idleb .

页表映射/寻址模拟器

想详细了解页表SEO靠我映射与CPU如何寻址的可以参考这个链接,来单步调试

https://github.com/jingjin666/mmu-map-address-simulator.git
“SEO靠我”的新闻页面文章、图片、音频、视频等稿件均为自媒体人、第三方机构发布或转载。如稿件涉及版权等问题,请与 我们联系删除或处理,客服邮箱:html5sh@163.com,稿件内容仅为传递更多信息之目的,不代表本网观点,亦不代表本网站赞同 其观点或证实其内容的真实性。

网站备案号:浙ICP备17034767号-2