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

news2024/11/30 10:55:47

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页表=512
8=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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/3072.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JavaWeb传统商城(MVC三层架构)的促销功能模块【进阶版】

文章目录一.JavaWeb商城项目的促销功能模块【进阶版】开发过程记录1.1 项目背景1.2 需求分析1.3 开发流程/顺序二.促销页面(0.1颗星)2.1 需求介绍2.2 JSP页面2.3效果展示三,商品详情页面(0.2颗星)3.1 需求介绍和效果图3.2 数据库分析3.2 Servlet层3.3 Service层3.4 DAO层3.5 JS…

一本通1064;奥运奖牌计数

#include <iostream> using namespace std; int main() {int n, Jin, Yin, Tong;int JinSum 0, YinSum 0, TongSum 0, sum;cin >> n;for (int i 1; i < n; i) // 循环n次{cin >> Jin >> Yin >> Tong; // 输入一天获得的金银铜牌数JinSum …

InfluxDB学习记录(三)——influxdb的flux语法

什么是Flux Flux 是 InfluxData 的功能性数据脚本语言&#xff0c;设计用于查询、分析和处理数据&#xff0c;它是InfluxQL 和其他类似 SQL 的查询语言的替代品。 每个 Flux 查询都需要以下内容&#xff1a; 数据源时间范围数据过滤器 Flux代码示例 from(bucket:"example…

重装系统后打印机状态已暂停如何恢复

​当我们在使用打印机打印文件的时候&#xff0c;有时候会发现打印机状态已暂停&#xff0c;打印不下去了&#xff0c;这时候怎么恢复呢&#xff0c;其实只需要取消掉打印暂停就可以了&#xff0c;下面就和大家讲讲重装系统后打印机状态已暂停如何恢复吧。 打印机状态已暂停怎…

【前端】Vue+Element UI案例:通用后台管理系统-Home组件:卡片、表格

文章目录目标代码0.布局1.左上User卡片2.左下table卡片2.1数据&#xff1a;TableData.js2.2table2.3代码优化&#xff1a;循环3.右上数据卡片3.1数据&#xff1a;CountData3.2结构3.3布局3.4样式总代码Home.vue参考目标 红框内部分都是卡片&#xff0c;鼠标悬停会有阴影左下是表…

java计算机毕业设计基于安卓Android的天文观星系统app uniapp 小程序

项目介绍 信息技术的发展带来了大量的数据内容,在这些数据中,想要找到自己需要的只有通过搜索引擎。如今,通过百度去查找信息成为大众的首选,然而在经济利益的驱动下,许多百度来的信息都是商业内容,很难找到真实有用的实际信息。在互联网中平台,天文信息交流和资源共享是一个非…

【每日训练】进制转换

目录 题目链接&#xff1a; 测试用例&#xff1a; 解析&#xff1a; 程序&#xff1a; 题目链接&#xff1a; 进制转换_牛客题霸_牛客网 (nowcoder.com) 测试用例&#xff1a; 解析&#xff1a; 题目描述&#xff1a; 输入一个十进制数&#xff0c;转化为对应输入的几进制数…

微服务及其在app自动化领域的应用

微服务是一种软件开发技术- 面向服务的体系结构&#xff08;SOA&#xff09;架构样式的一种变体&#xff0c;它提倡将单一应用程序划分成一组小的服务&#xff0c;服务之间互相协调、互相配合&#xff0c;为用户提供最终价值。每个服务运行在其独立的进程中&#xff0c;服务与服…

5款可视化工具优缺点比对,谁赢了?

利用Excel表格进行汇报&#xff0c;底下坐着的领导可能会看起来眼花缭乱&#xff0c;但如果是以图表可视化的形式展现出来&#xff0c;那可简洁明了多了&#xff0c;不仅仅可以看到某个项目近几个月的走势&#xff0c;并且还能知道之后的决策。 可视化图表用什么工具做&#xf…

FP8训练调研

FP8训练调研 一、FP8训练相关技术要点总结 1、基于块的累加技术&#xff0c;减小低精度数之间相加的累积误差 2、随机舍入技术代替四舍五入&#xff0c;降低舍入误差 3、混合FP8技术&#xff0c;用1-4-3进行前向&#xff0c;1-5-2进行反向 4、设置指数偏移&#xff0c;使F…

windows搭建WebDAV服务,并内网穿透公网访问【无公网IP】

自己用Windows Server搭建了家用NAS主机&#xff0c;WebDAV的文件共享方式当然也是必不可少的。 本文使用的是WIN10 专业版。 1. 安装IIS必要WebDav组件 1.1 打开控制面板&#xff0c;查看方式改为“类别”&#xff0c;进入“程序”&#xff0c;“启用或关闭Windows功能” 1…

数据结构链表之无头单向循环链表的实现

文章目录前言1.链表的相关介绍1.什么是节点2.链表和顺序表的对比3.链表的种类2.链表的实现1.节点的定义和创建2.链表的相关函数接口的实现1.链表的创建2.数据的插入头插尾插指定位置插入3.数据的删除头删尾删指定位置删除4.打印显示节点数据5.数据查找6.链表销毁3.总结前言 之…

2022新版加壳工具-支持.NET虚拟化加密

.NET 虚拟化保护 .NET 程序的保护技术在对抗中不断演进&#xff0c;出现了控制流混淆、名称混淆、文件加壳、动态方法、JIT 加密等保护技术&#xff0c;这些保护技术都有其各自的优缺点&#xff0c;虽然组合起来也能达到一定的效果&#xff0c;但近几年已经流传出一些脱壳机和…

单点架构、集群架构、服务化架构、SOA、微服务到底有什么联系和关系?

本篇参考总结 IT老齐的学习笔记 原视频SOA面向服务架构 原视频智慧城市实践指南 &#xff08;书籍-SOA概述&#xff09; 最近我在负责研发智慧园区的智慧平台产品&#xff0c;目前需求阶段和设计阶段已经完成&#xff0c;正式开始开发阶段&#xff0c;但是作为一个算法类学习者…

redies基本数据结构

nosql数据库 和sql结构的数据库 1.结构化的&#xff0c;不设置表之间的结构 2.没有主外键约束&#xff0c;之间没有关联 3.nosql&#xff0c;其他的数据库不能使用redies的语法 4.没有事务&#xff0c;不符合ACID 5.redies存储在内存中&#xff0c;速度非常快 是一个键值…

Docker入门学习:基本概念、安装、命令、简单使用

前言 一、基本概念 1、Docker镜像 镜像就是一个文件&#xff0c;例如我们的应用镜像、环境镜像&#xff08;例如nginx、mysql等&#xff09;&#xff0c;镜像是创建Docker容器的基础。 2、Docker容器 Docker容器类似于一个沙箱&#xff08;例如做支付的时候的支付宝的沙箱…

【Java|golang】1668. 最大重复子字符串

给你一个字符串 sequence &#xff0c;如果字符串 word 连续重复 k 次形成的字符串是 sequence 的一个子字符串&#xff0c;那么单词 word 的 重复值为 k 。单词 word 的 最大重复值 是单词 word 在 sequence 中最大的重复值。如果 word 不是 sequence 的子串&#xff0c;那么重…

系统学习SpringFramework:SpringBean的注入方式

本篇内容包括&#xff1a;Spring 容器简介&#xff08;什么是容器、容器如何工作&#xff09;、SpringBean 注入方式&#xff08;SpringBean 注入方式分类、Autowiring 自动绑定&#xff09;以及 获取 Spring Bean 的工具类&#xff01; 一、Spring 容器 1、什么是容器 Sprin…

薪资17K,在字节外包工作是一种什么体验...

我17年毕业于一个普通二本学校&#xff0c;电子信息工程学院&#xff0c;是一个很不出名的小本科。大学期间专业知识也没有去认真的学习&#xff0c;所以毕业的时候就随便找了一份工作&#xff0c;在一个小公司做功能测试。 记得那时候税前薪资大概是7k左右&#xff0c;因为是…

SpringBoot:(四)底层注解详解

笔记来源&#xff1a;【尚硅谷】SpringBoot2零基础入门教程&#xff08;spring boot2干货满满&#xff09; 文章目录4.1 Configuration详解总结4.2 Component、Controller、Service、Repository、Bean4.3 Import导入组件4.4 Conditional条件装配4.5 ImportResource导入Spring配…