一般情况下,Linux系统中进程的4GB内存空间被划分为2个部分-------用户空间和内核空间,大小分别为0~3G,3~4G。用户进程通常,只能访问用户空间的虚拟地址,不能访问内核空间的虚拟地址。
指通过物理内存条获得的内存空间,主要作用为在计算机运行时为操作系统和程序提供临时储存。
虚拟内存对内存架构进行管理的一种手段。
内存架构 = 主存 + 缓存 + 硬盘
管理 = 内存管理系统
虚拟内存可以将物理主存扩大到便宜、容量大的磁盘上,即将磁盘空间也看作是主存空间的一部分。
提出了虚拟内存的概念,其抽象了物理内存,相当于对物理内存进行了虚拟化,保证每个进程都被赋予一块连续的,超大的(根据系统结构来定,32 位系统寻址空间为 2^32,64 位系统为 2^64)虚拟内存空间,进程可以毫无顾忌地使用内存,不用担心申请内存会和别的进程冲突,因为底层有机制帮忙处理这种冲突,能够将虚拟地址根据一个页表映射成相应的物理地址。
这种机制正是虚拟化软件做的事,也就是 MMU 内存管理单元。
内存虚拟化也分为基于软件的内存虚拟化和硬件辅助的内存虚拟化,其中,常用的基于软件的内存虚拟化技术为「影子页表」技术,硬件辅助内存虚拟化技术为 Intel 的 EPT(Extend Page Table,扩展页表)技术。
虚拟机本质上是Host机上的一个进程,按理说可以使用Host机的虚拟地址空间,但是在虚拟化模式下,虚拟机处于非root模式,无法直接访问 Root 模式下的 Host 机上的内存。
这个时候需要VMM的介入,VMM 需要 intercept (截获)虚拟机的内存访问指令,然后 virtualize(模拟)Host 上的内存,相当于 VMM 在虚拟机的虚拟地址空间和 Host 机的虚拟地址空间中间增加了一层,即虚拟机的物理地址空间,也可以看作是 Qemu 的虚拟地址空间(稍微有点绕,但记住一点,虚拟机是由 Qemu 模拟生成的就比较清楚了)。
所以内存软件虚拟化的目标是要将虚拟机的虚拟地址(Guest Virtual Address, GVA)转化成Host的物理地址(Host Phyiscal Address, HPA),中间要经过虚拟机的物理地址(Guest Physical Address, GPA)和 Host 虚拟地址(Host Virtual Address)的转化,即
GVA -> GPA -> HVA -> HPA
其中前两步由虚拟机的系统页表完成,中间两步由 VMM 定义的映射表(由数据结构 kvm_memory_slot 记录)完成,它可以将连续的虚拟机物理地址映射成非连续的 Host 机虚拟地址,后面两步则由 Host 机的系统页表完成。如下图所示:
当程序不分段时,找到堆中虚拟地值的物理地址很简单,物理地址 = 基地址 + 虚拟地址
当程序分段时,找到堆中物理地址会复杂一些,物理地址 = 基地址 + (虚拟地址 - 该段的开头的虚拟地址)
当不分段时:整个程序的内存空间连续(无论是程序以为的内存空间还是物理内存都是连续的),所以虚拟地址即表明了其是第几个内存空间。显然 物理地址 = 基地址 + 虚拟地址
当分段时:整个程序的内存空间不再连续,每一段都有自己独特的基地址,但是虚拟地址还是相对于之前只有一个基地址时的值,那么此时虚拟地址就无法直接表示其在第几个内存空间了(因为程序以为的连续内存空间映射成的物理内存并不连续)。所以,我们需要虚拟地址相对于每个段自己的基地址的值,要完成这个操作只需要将虚拟地址 - 段开头的虚拟地址。因此 物理地址 = 基地址 + 虚拟地址 - 段开头的虚拟地址
这块代码就是GVA->GPA的过程:
逐步解析下面代码:
1. 通过虚拟地址计算出该地址相对于起始地址的页号
代码块uint64\_t offset = (virt / 0x1000) \* 8;
第一次看到这段代码真是一头雾水,这是个啥啊!在计算机虚拟内存的概念中,页、内存页或者虚拟页是指内存中的一段固定长度的快,这个内存块在物理地址和虚拟内存地址上都是连续的。一个页通常是以下操作的最小单元:操作系统为程序分配空间;内存和外存传输,比如说硬盘。page\_size代码这个页的基础大小。
virtual_index = virtual_address / getpagesize()
如何在操作系统中查看page_size的大小呢,通过命令getconf PAGESIZE
通过10进制转16进制可以得到,4096 -> 0x1000
此处得到结论:page_size = 0x1000
接下来我们就可以确定要映射到页面地图中的偏移量:
#define PAGEMAP_LENGTH 8
offset = (unsigned long)virtual_address / getpagesize() * PAGEMAP_LENGTH
2. 读取内核文件pagemap对应的qemu
lseek(fd, offset, SEEK_SET);
头文件:#include <sys/types.h> #include <unistd.h>
用 法: off_t lseek(int handle, off_t offset, int fromwhere);
EEK_SET:将读写位置指向文件头后再增加offset个位移量。
SEEK_CUR:以目前的读写位置往后增加offset个位移量。
SEEK_END:将读写位置指向文件尾后再增加offset个位移量。
返回值:当调用成功时则返回目前的读写位置,也就是距离文件开头多少个字节。若有错误则返回-1,errno 会存放错误代码。
3. 计算物理地址phys = (phys & ((1ULL << 54) - 1)) * 0x1000+(virt&0xfff);
首先我们了解下计算物理地址的公式:
physcial_addr = (page_frame_number << PAGE_SHIFT) + distance_from_page_boundary_of_buffer
拆解下:
phy = phy_index * pagesize + page_offset;
phy = (pfn_item & PFN_MASK) * page_size + virtual_address % page_size;
此处的page_frame_number = 54
page_offset = virtual_address % page size
在 pagemap 中通过页号查找对应项,查看该页是否在内存中;
若当前页存在,则取 bits 0~54 加上偏移量,可得所对应的物理地址。
phy_index = pfn_item & PFN_MASK
PFN_MASK = ((1ULL<<54)-1))
这里的 virt & 0xfff 等同于virt % page_size.
最终得到物理地址.
1. 内存虚拟化
3. Linux上地址映射
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。