内存虚拟化——hyp

本文涉及的产品
公网NAT网关,每月750个小时 15CU
简介: 本文主要就从这两个方面来讲述内存虚拟化的第一节,主要是 hypervisor 一层的内容。这里对后文讲述做一些约定,在本项目中,minos、hypervisor、kernel、host os 指的是同一个东西,指的是直接运行在硬件上,运行在 EL2 异常级别的那一层软件。(没有虚拟化的情况下,minos 本身也可以被编译为运行在 EL1 上的 kernel,某些地方容易引起歧义我再详述)
  • 首发微信公号:Rand_cs

minos 1.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 阶段对内存的初始化,直接来看初始化后的结果,具体初始化流程放在后面启动来讲述。

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

        memory@40000000 {
                reg = < 0x00 0x40000000 0x01 0x00 >;
                device_type = "memory";
        };

上述代码是 minos 运行在 qemu 平台上使用的设备树文件中关于内存节点的描述,我们可以知道该物理内存的起始地址为 $$0x00 << 32 + 0x4000\ 0000$$ ,大小为 $$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 是最大的“内存单位”

#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 链表当中

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

// 从 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 表中分配内存,分配的每个大小内存都属于一个哈希桶,然后从中寻找是否有空闲的内存

// 从 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 分配内存,更简单了,就是一个上移指针的操作

// 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 分配。

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 结构来记录将要释放的这块内存信息

// 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 的方式来分配整页内存

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

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 相关函数

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

// 建立 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 中二级页表的流程。

// 获取地址 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 定义如下:

struct vspace {
    pgd_t *pgdp;
    spinlock_t lock;
};

static struct vspace host_vspace;

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

// 将虚拟地址 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
目录
相关文章
|
3月前
|
KVM 虚拟化
KVM的热添加技术之内存
文章介绍了KVM虚拟化技术中如何通过命令行调整虚拟机内存配置,包括调小和调大内存的步骤,以及一些相关的注意事项。
98 4
KVM的热添加技术之内存
|
3月前
|
KVM 虚拟化
计算虚拟化之CPU——qemu解析
【9月更文挑战10天】本文介绍了QEMU命令行参数的解析过程及其在KVM虚拟化中的应用。展示了QEMU通过多个`qemu_add_opts`函数调用处理不同类型设备和配置选项的方式,并附上了OpenStack生成的一个复杂KVM参数实例。
|
4月前
|
Java 数据安全/隐私保护
一种优秀的虚拟机内存架构 - AQ
【8月更文挑战第8天】AQ虚拟机内存架构是一种创新设计,旨在提供高效、可靠及灵活的内存管理。它通过精细划分内存并采用智能分配策略,动态调整以适应应用需求。对于高内存消耗任务,AQ预留足够连续空间避免碎片化;引入内存压缩技术以增加可用空间;具备精准垃圾回收机制提高内存利用率;同时加强安全性与稳定性防止因内存错误导致的问题。总之,AQ通过先进技术提升了虚拟机性能与稳定性。
|
6月前
|
Linux 虚拟化 内存技术
minos 1.2 内存虚拟化——guest
本文继续讲述 minos 中的内存虚拟化中关于 guest 的部分,主要弄清楚一个问题,minos 如何管理 guest vm 的内存。
71 5
minos 1.2 内存虚拟化——guest
|
安全 Linux 调度
倚天虚拟化:CPU虚拟化原理介绍
虚拟化技术中最关键的技术之一就是CPU虚拟化。在没有硬件辅助虚拟化技术出来之前,通常都是通过TCG(软件进行指令翻译)的方式实现CPU虚拟化。但是由于TCG方式的虚拟化层开销太大,性能太差,因此引入了硬件辅助虚拟化技术。
1468 1
|
存储 KVM 文件存储
【KVM虚拟化】· 存储池、存储卷
【KVM虚拟化】· 存储池、存储卷
680 0
|
Linux 虚拟化 开发者
内存虚拟化技术 | 学习笔记
快速学习内存虚拟化技术 。
内存虚拟化技术 | 学习笔记
|
虚拟化 芯片 开发者
CPU虚拟化技术|学习笔记
快速学习CPU虚拟化技术。
CPU虚拟化技术|学习笔记
|
Linux 虚拟化
内存虚拟化技术
大型操作系统(比如Linux)的内存管理的内容是很丰富的,而内存的虚拟化技术在OS内存管理的基础上又叠加了一层复杂性,比如我们常说的虚拟内存(virtual memory),如果使用虚拟内存的OS是运行在虚拟机中的,那么需要对虚拟内存再进行虚拟化,也就是vitualizing virtualized memory。
内存虚拟化技术

热门文章

最新文章