minos 1.1 内存虚拟化——hyp

  • 首发微信公号:Rand_cs

minos 2.1 内存虚拟化------hyp

内存虚拟化,目前理解主要两方面:

  1. 内存管理,没有虚拟化的情况时,对于 Linux 内核运行在物理硬件之上,内核需要管理物理内存,需要管理进程的虚拟内存。类似,type1 类型的 hypervisor/minos 运行在物理硬件上,minos 需要对物理内存管理,需要对虚机使用的内存进行管理。这里的管理,可以简单理解为内存的组织形式,内存分配与回收方式
  2. 地址转换,这部分主要与硬件相关,围绕页表的一系列的 ARMv8 硬件知识。对于虚拟化,ARMv2 支持硬件的 stage2 地址转换

本文主要就从这两个方面来讲述内存虚拟化的第一节,主要是 hypervisor 一层的内容。这里对后文讲述做一些约定,在本项目中,minos、hypervisor、kernel、host os 指的是同一个东西,指的是直接运行在硬件上,运行在 EL2 异常级别的那一层软件。(没有虚拟化的情况下,minos 本身也可以被编译为运行在 EL1 上的 kernel,某些地方容易引起歧义我再详述)

Address Translation

先讨论没有虚拟化的情况,对于 os 来说,用户态程序和内核使r用不同的页表,用户态的页表存放在 TTBR0_EL1,内核页表存放在 TTBR1_EL1。而 x86 架构下用户态和内核态使用一张页表,存放在 CR3 寄存器中。

内核位于虚拟地址空间中的高处,其地址都是以 111... 开头,经过 TTBR1_EL1 中的页表转换成物理地址。用户态的应用程序都是位于虚拟地址空间中的低处,其地址以 000... 开头,经过 TTBR0_EL1 中的页表转换成物理地址

内核地址空间+用户地址空间并不是整个虚拟地址空间,它们的大小由 TCR_EL1.TxSZ 控制。TCR_EL1,Translation Control Register(EL1),顾名思义,专门来控制地址转换的一个寄存器,而且是控制 EL0 和 EL2 的地址转换。

TCR_EL1.TxSZ 用来控制内核/用户虚拟地址空间的大小,T1SZ, bits [21:16] T1SZ,T0SZ, bits [5:0],它们都占据 6 bits,可以简单理解为它们控制最高有效 1/0 的起始位置,举个例子如下图所示:

对于实际的地址转换流程,相信大家已经很熟悉了,直接来看一下 arm 平台地址转换的一个例子:

上述是页大小为 64K,虚拟地址为 42 位的情况下,虚拟地址转换到物理地址的流程。对于上图中页大小、TTBR select 的位数等信息都可以通过系统寄存器来设置,具体的后面分析到代码再说,这里只是简单再过一下地址转换的流程。对比以前 x86 架构下地址转换的流程,区别就在于 arm 地址转换时,mmu 会根据地址的最高几位来选取页表基址寄存器,如果最高位以 1 开头那么是内核地址,选取 TTBR1_EL1,反之以 0 开头的地址为用户态地址选取 TTBR0_EL0

Stage2 Translation

上述讲述的是无虚拟化的情况,如果存在虚拟化的话。前面所说的 os 为 guest os,其内的虚拟地址 va 不变,但是经过 TTBRx_EL1 转换的地址并不是真实的物理地址,这里我们称为 ipa(Intermediate Physical Address,中间物理地址),要再经过存放在 VTTBR0_EL2 中的页表(Virtula Tranlation Table Base Register,虚拟页表基址寄存器)的转换,才是最后真正的物理地址 pa。也就是会经过 va->ipa->pa 的转换

每个进程有自己的页表,每次切换进程的时候由 guest os 将其页表基址写进 TTBR0_EL1。而虚拟页表是每个虚拟机一个,切换虚拟机的时候,由 Hyp 将该虚拟机的虚拟页表基址写到 VTTBR_EL2.

对于 EL2 层的 hypervisor 和 EL3 层的 Secure Monitor 来说,没有 stage2 translation。如果当前位于 EL2,Hyp 中的虚拟地址经过位于 TTBR0_EL2 中存放的页表直接就转换为了物理地址。EL3 的 Secure Monitor 同理。

内存概览

minos 是 type1 类型的虚拟机,minos 这个 hyp 是直接运行在物理硬件上的,它就相当于没有虚拟化时候的操作系统,需要实现内存管理,进程管理等等。

在 boot 阶段首先需要对整个物理内存进行初始化(分析设备树节点,记录内存起始位置,建立初始映射等等操作),但本文我们先略过 boot 阶段对内存的初始化,直接来看初始化后的结果,具体初始化流程放在后面启动来讲述。

这是内存初始化后,物理内存总览图。

C 复制代码
        memory@40000000 {
                reg = < 0x00 0x40000000 0x01 0x00 >;
                device_type = "memory";
        };

上述代码是 minos 运行在 qemu 平台上使用的设备树文件中关于内存节点的描述,我们可以知道该物理内存的起始地址为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 x 00 < < 32 + 0 x 4000 0000 0x00 << 32 + 0x4000\ 0000 </math>0x00<<32+0x4000 0000 ,大小为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 x 01 < < 32 + 0 x 00 = 0 x 1 0000 0000 0x01 << 32 + 0x00 = 0x1\ 0000\ 0000 </math>0x01<<32+0x00=0x1 0000 0000,也就是 4 个 G,但实际大小还要看 qemu 的启动参数,比如说我现在启动时通过 -m 2G 指定内存大小为 2G,实际的大小以这个 qemu 启动时的参数为准。

然后这 2G 的内存在初始化后又被分为 5 个部分:

  1. 0x4000 0000 ~ 0x4400 0000 是 minos 本身所处区域,其中包括了 minos 镜像、dtb 镜像,以及预留了一部分内存用来分配页表,minos 相关数据结构等等。简单来说,minos 的 malloc 会从中分配内存
  2. 0x4400 0000 ~ 0x4645 a000 是 ramdisk 区域,minos 设计了一个 ramdisk,里面存放的是 host vm 的 kernel 镜像和 dtb 镜像。当我们创建 hvm 的时候,就会从中读取镜像数据,然后加载到内存特定位置
  3. 0x4645 a000 ~ 0x4660 0000、0x8660 0000 ~ 0x1 4000 0000 两部分区域内存可以看作是空闲内存,后面我们会了解到,这两部分内存会全部转换成 block 的形式,当创建普通 vm 的时候,就会从中分配内存
  4. 0x4660 0000 -> 0x8660 0000,这部分内存分配给了 hvm,hvm 也是启动时期就创建了

这一部分都是物理地址空间的布局,虚拟地址空间呢?我们知道启动的时候会开启 MMU,在这之前肯定需要先创建一张页表(启动的时候位于 EL2,创建了一张 EL2 的页表给 minos 使用),然后将其基址赋值给 TTBR_EL2,最后再使能 MMU,随后便会使用虚拟地址来访问物理内存。

minos 的映射关系很简单,物理内存直接映射到虚拟内存,比如说 0x4000 0000 这个虚拟地址就是对应着物理地址 0x4000 0000。其他几大区域基本上也是直接映射,具体情况我们后面再讨论,只要知道 minos 区域是直接映射的。

左侧图片,则是关于第一部分 minos 区域的详细布局,这里暂不具体展开,同样待到启动的时候讲述,这里只需要注意 page_base 与 minos_end 两个指针所指位置。另外上面所说建立 minos 区域直接映射,这个页表就是左侧图中的 pgd pud pmd pte。

mem_region

上面提到了 mem_region,在 minos 中,mem_region 是最大的"内存单位"

C 复制代码
#define MAX_MEMORY_REGION 16

static struct memory_region memory_regions[MAX_MEMORY_REGION];
// 下一个空闲 memory_region 下标
static int current_region_id;
// 使用中的 memory_region 组成了一个链表,mem_list 是其头节点
LIST_HEAD(mem_list);

enum {
    MEMORY_REGION_TYPE_NORMAL = 0,
    MEMORY_REGION_TYPE_DMA,
    MEMORY_REGION_TYPE_RSV,
    MEMORY_REGION_TYPE_VM,
    MEMORY_REGION_TYPE_DTB,
    MEMORY_REGION_TYPE_KERNEL,
    MEMORY_REGION_TYPE_RAMDISK,
    MEMORY_REGION_TYPE_MAX
};

struct memory_region {
    int type;
    int vmid;       // 0 is host
    phy_addr_t phy_base;
    size_t size;
    struct list_head list;
};

整个系统最多定义 16 个 memory_region,全局静态定义,memory_region 目前总共 7 种类型。struct memory_region 主要就是记录了内存段的起始位置和大小。

所有正使用的 memory_region 组成一个链表,头节点为 mem_list

add_memory_region 函数将会新增一个 memory_region,然后注册到 mem_list(就是申请一个 memory_region,记录信息,然后链接到 mem_list 当中去)

split_memory_region 将会从现有的 memory_region 中分割出一个新的 memory_region,然后注册到 mem_list。

在当前的 minos 整个系统中,add_memory_region 函数实际只会调用一次,就是在启动分析设备树 memory 节点时,将这个内存节点信息记录在第一个 memory_region 中,然后注册到 mem_list。后续的所有 memory_region,都是从第一个 memory_region 中分割出来的。

如果 minos 这个 hypervisor 后续继续迭代,应该会有内存热插拔等功能,到时候可能就有 delete_memory_region,add_memory_region 也会在增加内存的时候调用。

minos KERNEL 区域内存管理

minos 对 KERNEL 区域的内存管理主要几种在 memory.c 文件中,对于此部分的内存管理很简单

  1. malloc 和 free 接口,minos 使用哈希表来维护了一个简易的内存池,当使用 malloc 分配内存时,先从哈希表中分配,如果没有,那么上移 slab_base 指针来分配内存。释放的时候直接释放到哈希表中
  2. alloc_page 和 free_page 用来分配整页(4096 整数倍),同样的使用 used_page_head、free_page_head 来维护了一个简易的内存池。当使用 alloc_page 分配整页内存的时候,先从 free_page_head 链表中查看是否有空闲页,如果没有下移 page_base 指针来分配。释放的时候直接将相关信息记录到 struct page,然后将其插入到 free_page_head 链表当中

逻辑很简单,我们快速过一下这部分代码

C++ 复制代码
// 从 hashtable 中分配内存
static void *malloc_from_hash_table(size_t size)
{   
    // 当前分配的 size 属于哪一个 hash 桶
    int id = hash_id(size);
    struct slab_type *st;
    struct slab_header *sh;

    /*
     * find the related slab mem id and try to fetch
     * a free slab memory from the hash cache.
     */
    
    // 遍历该 hash 桶指向的链表
    list_for_each_entry(st, &slab_hash_table[id], list) {
        //寻找大小相等的节点
        if (st->size != size)
            continue;

        if (st->head == NULL)
            return NULL;
        

        sh = st->head;
        st->head = sh->next;
        sh->magic = SLAB_MAGIC;

        // 返回给"用户"使用的内存起点
        return ((void *)sh + SLAB_HEADER_SIZE);
    }

    return NULL;
}

从 hash 表中分配内存,分配的每个大小内存都属于一个哈希桶,然后从中寻找是否有空闲的内存

C++ 复制代码
// 从 slab_heap 中分配内存
static void *malloc_from_slab_heap(size_t size)
{
    unsigned long slab_size;
    struct slab_header *sh;

    if (ULONG(slab_base) >= ULONG(page_base)) {
        pr_err("no more memory for slab\n");
        return NULL;
    }

    // 计算当前 slab size 总大小
    slab_size = ULONG(page_base) - ULONG(slab_base);
    size += SLAB_HEADER_SIZE;
    // 如果小于要分配的大小,返回空
    if (slab_size < size) {
        pr_err("no enough memory for slab 0x%x 0x%x\n",
                size, slab_size);
        return NULL;
    }

    sh = (struct slab_header *)slab_base;
    sh->magic = SLAB_MAGIC;
    sh->size = size - SLAB_HEADER_SIZE;

    slab_base += size;

    return ((void *)sh + SLAB_HEADER_SIZE);
}

从 slab base 分配内存,更简单了,就是一个上移指针的操作

C++ 复制代码
// malloc 分配内存,先从 hash table 里面分配,再从 slab heap 中分配
static void *__malloc(size_t size)
{
    void *mem;

    // 先从 hash table 里面分配
    mem = malloc_from_hash_table(size);
    if (mem != NULL)
        return mem;
    // 没有,再从 slab heap 里面分配
    return malloc_from_slab_heap(size);
}

void *malloc(size_t size)
{
    void *mem;

    ASSERT(size != 0);
    // 对齐
    size = get_slab_alloc_size(size);

    spin_lock(&mm_lock);
    // 分配
    mem =  __malloc(size);
    spin_unlock(&mm_lock);
    // 检查
    if (!mem) {
        pr_err("malloc fail for 0x%x\n");
        dump_stack(NULL, NULL);
    }
    // 返回
    return mem;
}

malloc 接口,首先对齐,然后尝试从 hash table 里面分配,没分配到再从 slab_heap 分配。

C++ 复制代码
void free(void *addr)
{
    ASSERT(addr != NULL);
    spin_lock(&mm_lock);
    free_slab(addr);
    spin_unlock(&mm_lock);
}

// 释放 addr 处的内存,释放到 hash table
static void free_slab(void *addr)
{
    struct slab_header *header;
    struct slab_type *st;
    int id;

    ASSERT(ULONG(addr) < ULONG(slab_base));
    header = (struct slab_header *)((unsigned long)addr -
            SLAB_HEADER_SIZE);
    ASSERT(header->magic == SLAB_MAGIC);
    id = hash_id(header->size);

    list_for_each_entry(st, &slab_hash_table[id], list) {
        if (st->size != header->size)
            continue;

        header->next = st->head;
        st->head = header;
        return;
    }

    /*
     * create new slab type and add the new slab header
     * to the slab cache.
     */
    // 创建新的 描述符,然后插入到哈希表中
    st = malloc_from_slab_heap(sizeof(struct slab_type));
    if (!st) {
        pr_err("alloc memory for slab type failed\n");
        return;
    }

    st->size = header->size;
    st->head = NULL;
    list_add_tail(&slab_hash_table[id], &st->list);

    header->next = st->head;
    st->head = header;
}

释放内存的操作,找到该内存大小对应的哈希桶,然后插入到相关链表就行了。另外有意思的是在释放内存的时候,可能需要上移 slab_base 指针分配一个 struct slab_type 结构来记录将要释放的这块内存信息

C++ 复制代码
// page_base 向下移来分配实际的页面
static struct page *alloc_new_pages(int pages, unsigned long align)
{
    unsigned long tmp = (unsigned long)page_base;
    struct page *recycle = NULL;
    unsigned long base, rbase;
    struct page *page;

    // page base 向下移动来实际分配页面
    base = tmp - pages * PAGE_SIZE;
    base = ALIGN(base, align);

    // 这种情况是说内存不够了,如果分配的话,page_base 和 slab_base 都相撞了
    if (base < (unsigned long)slab_base) {
        pr_err("no more pages %d 0x%x\n", pages, align);
        return NULL;
    }
    // 这种情况应是 page_base 的对齐级别和 align 要求有差,比如说 align 要求 8K 对齐
    // 但是 page_base 只是 4K 对齐,这样 rbase 的值就会小于初始的 page_base
    // 此时对于 [rabse, page_base] 之间的内存我们放入 free_list_head
    rbase = base + pages * PAGE_SIZE;
    if (rbase != tmp) {
        recycle = __malloc(sizeof(struct page));
        if (!recycle) {
            pr_err("can not allocate memory for page\n");
            return NULL;
        }

        recycle->pfn = rbase >> PAGE_SHIFT;
        recycle->flags = 0;
        recycle->align = 1;
        recycle->cnt = (tmp - rbase) >> PAGE_SHIFT;
        recycle->next = NULL;
        __free_page(recycle);
    }

    // 分配 page 结构体来记录页属性
    page = __malloc(sizeof(struct page));
    if (!page) {
        pr_err("can not allocate memory for page\n");
        if (recycle)
            free_slab(recycle);
        return NULL;
    }

    page->pfn = base >> PAGE_SHIFT;  //页号
    page->flags = 0;  //也属性
    page->cnt = pages; //共几页
    page->align = align >> PAGE_SHIFT; //几页对齐
    page->next = NULL;

    page_base = (void *)base;  // 更新 page_base 指针

    return page;
}

下移 page_base 的方式来分配整页内存

C++ 复制代码
static struct page *__alloc_pages(int pages, int align)
{
    struct page *page = NULL;
    struct page *tmp, *prev = NULL;

    switch (align) {
    case 1:
    case 2:
    case 4:
    case 8:
        break;
    default:
        pr_err("%s:unsupport align value %d\n", __func__, align);
        return NULL;
    }

    spin_lock(&mm_lock);
    tmp = free_page_head;

    /*
     * try to get the free page from the free list.
     */
    // 先从 free_page_head 中寻找是否有合适的 page 页面
    while (tmp) {
        if ((tmp->cnt == pages) && (tmp->align == align)) {
            page = tmp;
            break;
        }

        prev = tmp;
        tmp = tmp->next;
    }

    // 没有找到的话,直接使得 page_base 向下移动来分配页面
    if (!page) {
        page = alloc_new_pages(pages, PAGE_SIZE * align);
    // 如果在 free_page_head 中找到了合适的 page,直接返回
    } else {
        if (prev != NULL) {
            prev->next = page->next;
            page->next = NULL;
        } else {
            free_page_head = NULL;
        }
    }

    // 向 used_page_head 中记录分配出去的页面
    add_used_page(page);
    spin_unlock(&mm_lock);

    return page;
}

此函数便会先尝试在 free_page_head 中分配内存,没有分配到的话,下移 page_base 指针分配,并将其对应的 struct page 插入到 used_page_head

C 复制代码
void *__get_free_pages(int pages, int align)
{
    struct page *page = NULL;

    page = __alloc_pages(pages, align);
    if (!page)
        return NULL;

    return (void *)ptov(pfn2phy(page->pfn));
}

static inline void *get_free_pages(int pages)
{
    return __get_free_pages(pages, 1);
}

static void *stage1_get_free_page(unsigned long flags)
{
    return get_free_page();
}

minos 中会有很多类似 get_free_page 来获取一页内存,底层都是调用 __alloc_pages

页表

这部分来看一下 ARMv8 架构下的页表结构,以及如何建立虚拟地址到物理地址的映射关系。

页表结构

页表结构主要就是了解页表项,在 ARM 平台有 4 种页表项,如下图所示:

现在 64 位系统几乎都使用 4 级页表,从高到低(level 0 ~ level 3)我们通常称为 PGD(Page Global Directory)、 PUD(Page Upper Directory)、PMD(Page Middle Directory)、PT(Page Table),页表每一项称作 PTE(page table entry)。

页表项有 4 种,以结尾 bit0-1 来区分,如上图所示。level0 只能是 Table Descriptor,输出下一级页表的物理地址

页表操作

有关页表的操作函数存放在 stage1.c 文件里面,这里说明一下 minos 中的 stage1 的含义,根据我的理解,stage1 转换指的是虚拟机中的 "虚拟地址"(va) 到 "物理地址"(ipa)的转换,stage2 是 "中间物理地址"(ipa)到 "真正物理地址"(pa)的转换。

但是 minos 项目中所述的 stage1 应当不是此含义,minos 中的 stage1 指的是 hypervisor 层级地址转换,也就是说当 cpu 运行在 el2 时的地址转换,此时使用 TTBR_EL2。反过来想,stage1 的地址转换相关函数应该位于 guest,也就是 Linux 内核代码,不应该出现于 minos 代码中。所以 minos 的 stage1 转换实际指的是 hypervisor 层级的地址转换,这也恰好对应这 host 这个词汇

从 create_host_mapping 可以看出,host 的 mapping,然后调用 stage1 相关函数

下面我们直接来看相关的页表操作相关函数的实现

C 复制代码
// 建立 level3 页表映射,pte 级别,对 start ~ end 之间所有的页进行映射
static int stage1_map_pte_range(struct vspace *vs, pte_t *ptep, unsigned long start,
        unsigned long end, unsigned long physical, unsigned long flags)
{
    unsigned long pte_attr;
    pte_t *pte;
    pte_t old_pte;
    // 获取地址 start 对应的 pte
    // ((pte_t *)(ptep) + (((((unsigned long)start) & 0x0000fffffffff000UL) >> 12) & (512 - 1)))
    pte = stage1_pte_offset(ptep, start);
    // 根据 flags 参数设置页表项属性
    pte_attr = stage1_pte_attr(0, flags);

    do {
        old_pte = *pte;
        if (old_pte)
            pr_err("error: pte remaped 0x%x\n", start);
        // 写 pte,pte 的数据就是 属性+物理地址
        stage1_set_pte(pte, pte_attr | physical);
    // 循环,知道 start ~ end 区间内的页面都映射了
    } while (pte++, start += PAGE_SIZE, physical += PAGE_SIZE, start != end);

    return 0;
}
// 建立 pmd 级别的页表映射
static int stage1_map_pmd_range(struct vspace *vs, pmd_t *pmdp, unsigned long start,
        unsigned long end, unsigned long physical, unsigned long flags)
{
    unsigned long next;
    unsigned long attr;
    pmd_t *pmd;
    pmd_t old_pmd;
    pte_t *ptep;
    size_t size;
    int ret;

    // 获取 addr 对应的 pmd 表项
    pmd = stage1_pmd_offset(pmdp, start);
    do {
        next = stage1_pmd_addr_end(start, end);
        size = next - start;
        old_pmd = *pmd;

        /*
         * virtual memory need to map as PMD huge page
         */
        // 如果要映射成大页,直接设置 pmd 表项完事儿
        if (stage1_pmd_huge_page(old_pmd, start, physical, size, flags)) {
            attr = stage1_pmd_attr(physical, flags);
            stage1_set_pmd(pmd, attr);
        // 
        } else {
            // 如果原来的 pmd 表项是空的
            if (stage1_pmd_none(old_pmd)) {
                // 获取一页
                ptep = (pte_t *)stage1_get_free_page(flags);
                if (!ptep)
                    return -ENOMEM;
                // 初始化清零
                memset(ptep, 0, PAGE_SIZE);
                // 填充 pmd 表项,地址指向新分配的页面
                stage1_pmd_populate(pmd, (unsigned long)ptep, flags);
            // 否则原来的pmd 表项非空
            } else {
                // 直接获取 pmd 指向的 pte 级页表地址
                ptep = (pte_t *)ptov(stage1_pte_table_addr(old_pmd));
            }

            // 调用 stage1_map_pte_range,对 start ~ next 之间的所有页面建立映射
            ret = stage1_map_pte_range(vs, ptep, start, next, physical, flags);
            if (ret)
                return ret;
        }
    } while (pmd++, physical += size, start = next, start != end);

    return 0;
}

上述是建立 level3 level2 页表的两个函数,应该很简单,看注释就能懂。唯一注意的地方是 next = stage1_pmd_addr_end(start, end);这里是获取大于 start 的下一个 2M 对齐的地址,具体实现见其位操作。

对于该文件中其他级别的 map、unmap 函数,这里也就不细说了,都是类似重复性的操作。如果实在没明白,兄弟回去补一补页表的基本知识,可以走一遍 xv6 中二级页表的流程。

C 复制代码
// 获取地址 va 对应的叶子表项
// 如果是大页,那么 pmd 就是叶子节点,否则就是 pte
static int stage1_get_leaf_entry(struct vspace *vs,
        unsigned long va, pmd_t **pmdpp, pte_t **ptepp)
{
    pud_t *pudp;
    pmd_t *pmdp;
    pte_t *ptep;

    // 查表 pgd,获取地址 va 对应的 pud 指针
    pudp = stage1_pud_offset(vs->pgdp, va);
    if (stage1_pud_none(*pudp))
        return -ENOMEM;
    
    // 查表 pud,再获取地址 va 对应的 pmd 指针
    pmdp = stage1_pmd_offset(stage1_pmd_table_addr(*pudp), va);
    if (stage1_pmd_none(*pmdp))
        return -ENOMEM;

    // 如果是大页,说明地址 va 对应的 pmd 就是叶子节点了,返回它的地址
    if (stage1_pmd_huge(*pmdp)) {
        *pmdpp = pmdp;
        return 0;
    }

    // 否则 pte 肯定是叶子节点了,获取地址 va 对应的 pte 表项
    ptep = stage1_pte_offset(stage1_pte_table_addr(*pmdp), va);
    *ptepp = ptep;

    return 0;
}

参数中出现的 struct vspace 定义如下:

C 复制代码
struct vspace {
    pgd_t *pgdp;
    spinlock_t lock;
};

static struct vspace host_vspace;

该结构体就只有 host_vspace 一个实例,表示 minos 这个 hypervisor 或者说宿主机的虚拟地址空间,由 host_vspace.pgdp 所指向的页表映射

C 复制代码
// 将虚拟地址 vir 重新映射到一个新的物理地址 phy,就是将叶子结点表项的内容更改为 phy | flags
int arch_host_change_map(struct vspace *vs, unsigned long vir,
        unsigned long phy, unsigned long flags)
{
    int ret;
    pmd_t *pmdp = NULL;
    pte_t *ptep = NULL;

    // 获取地址 vir 对应的叶子表项
    ret = stage1_get_leaf_entry(vs, vir, &pmdp, &ptep);
    if (ret)
        return ret;
    
    // 如果是大页,即如果叶子节点是 pmd 表项
    if (pmdp && !ptep) {
        // 将该 pmd 表项清 0
        stage1_set_pmd(pmdp, 0);
        // flush tlb
        flush_tlb_va_range(vir, S1_PMD_SIZE);
        // 重新设置 pmd 表项内容为 phy
        stage1_set_pmd(pmdp, stage1_pmd_attr(phy, flags));
        return 0;
    }

    // 否则叶子结点为普通的 pte 表项
    stage1_set_pte(ptep, 0);
    // flush tlb
    flush_tlb_va_range(vir, S1_PTE_SIZE);
    // 重新设置 pte 表项内容为 phy
    stage1_set_pte(ptep, stage1_pte_attr(phy, flags));

    return 0;
}

// hyp/宿主机的地址转换,将 va 转换为 pa
static inline phy_addr_t stage1_va_to_pa(struct vspace *vs, unsigned long va)
{
    unsigned long pte_offset = va & ~S1_PTE_MASK;
    unsigned long pmd_offset = va & ~S1_PMD_MASK;
    unsigned long phy = 0;
    pud_t *pudp;
    pmd_t *pmdp;
    pte_t *ptep;

    // 获取地址 va 对应的 pud 指针
    pudp = stage1_pud_offset(vs->pgdp, va);
    if (stage1_pud_none(*pudp))
        return 0;
    
    // 获取地址 va 对应的 pmd 指针
    pmdp = stage1_pmd_offset(ptov(stage1_pmd_table_addr(*pudp)), va);
    if (stage1_pmd_none(*pmdp))
        return 0;
    
    // 如果是大页,那么转换后的物理地址就是 pmd 表项中记录的内容 + 偏移量
    if (stage1_pmd_huge(*pmdp)) {
        phy = ((*pmdp) & S1_PHYSICAL_MASK) + pmd_offset;
        return 0;
    }

    // 否则是普通的 4K 页面,获取 pte 页表项
    ptep = stage1_pte_offset(ptov(stage1_pte_table_addr(*pmdp)), va);
    // 转换后的物理地址为 pte 中记录的页框地址 + 偏移量
    phy = *ptep & S1_PHYSICAL_MASK;
    if (phy == 0)
        return 0;

    return phy + pte_offset;
}

phy_addr_t arch_translate_va_to_pa(struct vspace *vs, unsigned long va)
{
    return stage1_va_to_pa(vs, va);
}

// hyp 将 start~end 的虚拟地址映射到 物理地址为 physical 开始的一段空间
int arch_host_map(struct vspace *vs, unsigned long start, unsigned long end,
        unsigned long physical, unsigned long flags)
{
    if (end == start)
        return -EINVAL;

    ASSERT((start < S1_VIRT_MAX) && (end <= S1_VIRT_MAX));
    ASSERT(physical < S1_PHYSICAL_MAX);
    ASSERT(IS_PAGE_ALIGN(start) && IS_PAGE_ALIGN(end) && IS_PAGE_ALIGN(physical));

    // 直接调用 pud 映射
    return stage1_map_pud_range(vs, start, end, physical, flags);
}

上述是该文件中其他的一些值得说说的函数,都有详细的注释,不赘述

本文主要内容就先到这里,其实就是一张图:

再简单总结下:

  1. EL0/1 中,ARMv8 的内核页表与用户态页表是分开的,内核页表存放在 TTBR0_EL1,用户态页表存放在 TTBR0_EL0。minos 运行在 EL2,minos 本身使用 TTBR0_EL2 寄存器来存放 minos 本身的页表
  2. 存在虚拟化的情况下,多了一个页表寄存器 VTTBR_EL2,其中存放虚机 stage2 地址转换使用的页表。地址转换流程为 va->ipa->pa,va 通过 TTBR0/1_EL1 的页表转化 ipa,ipa 通过 VTTBR_EL2 中的页表转换为 pa
  3. minos 最大的内存单位是 mem_region,本文简单讲述了 mem_region 的组织形式,以及分配回收方式
  4. 最后本文还讲述了 ARMv8 页表格式,以及各类页表项操作

好了,本文就先到这里,有什么问题欢迎来找我讨论交流

  • 首发微信公号:Rand_cs
相关推荐
潘多编程26 分钟前
Spring Boot微服务架构设计与实战
spring boot·后端·微服务
2402_8575893631 分钟前
新闻推荐系统:Spring Boot框架详解
java·spring boot·后端
2401_8576226633 分钟前
新闻推荐系统:Spring Boot的可扩展性
java·spring boot·后端
Amagi.2 小时前
Spring中Bean的作用域
java·后端·spring
2402_857589362 小时前
Spring Boot新闻推荐系统设计与实现
java·spring boot·后端
J老熊3 小时前
Spring Cloud Netflix Eureka 注册中心讲解和案例示范
java·后端·spring·spring cloud·面试·eureka·系统架构
Benaso3 小时前
Rust 快速入门(一)
开发语言·后端·rust
sco52823 小时前
SpringBoot 集成 Ehcache 实现本地缓存
java·spring boot·后端
原机小子3 小时前
在线教育的未来:SpringBoot技术实现
java·spring boot·后端
吾日三省吾码3 小时前
详解JVM类加载机制
后端