转:qemu-kvm内存管理
记得很早之前分析过KVM内部内存虚拟化的原理,仅仅知道KVM管理⼀个个slot并以此为基础转换GPA到HVA,却忽略了qemu端最初内存的申请,⽽今有时间借助于qemu源码分析下qemu在最初是如何申请并管理虚拟机内存的,坦⽩讲,还真挺复杂的。
⼀、概述
qemu-kvm 模型下的虚拟化引擎,内存虚拟化部分要说简单也挺简单,在虚拟机启动时,有qemu在qemu进程地址空间申请内存,即内存的申请是在⽤户空间完成的。通过kvm提供的API,把地址信息注册到KVM中,这样KVM中维护有虚拟机相关的slot,这些slot构成了⼀个完成的虚拟机物理地址空间。slot中记录了其对应的HVA,页⾯数、起始GPA等,利⽤它可以把⼀个GPA转化成HVA,想到这⼀点⾃然和硬件虚拟化下的地址转换机制EPT联系起来,不错,这正是KVM维护EPT的技术核⼼。整个内存虚拟化可以分为两部分:qemu部分和kvm 部分。qemu完成内存的申请,kvm实现内存的管理。看起来简单,但是内部实现机制也并⾮那么简单。本⽂重点介绍qemu部分。
美国大学排名2019⼆、涉及数据结构
qemu中内存管理的数据结构主要涉及:MemoryRegion、AddressSpace、FlatView、FlatRange、MemoryRegionSection、RAMList、RAMBlock、KVMSlot、kvm_urspace_memory_region等
这⼏个数据结构的确不太容易滤清,⼀下是个⼈的⼀些见解。 怎么可以把qemu层的内存管理再分为三个层次,MemoryRegion就位于顶级抽象层或者说⽐较偏向于host端,qemu中两个全局的MemoryRegion,分别是system_memory和system_io,不过两个均是以指针的形式存在,在地址空间的时候才会对对其分配具体的内存并初始化。MemoryRegion负责管理host的内存,理论上是树状结构,但是实际上根据代码来看仅仅有两层,
struct MemoryRegion {
/* All fields are private - violators will be procuted */
const MemoryRegionOps *ops;
koron
const MemoryRegionIOMMUOps *iommu_ops;
void *opaque;
struct Object *owner;
MemoryRegion *parent;//⽗区域指针
初亏是什么意思
Int128 size;//区域的⼤⼩
hwaddr addr;
void (*destructor)(MemoryRegion *mr);
ram_addr_t ram_addr;//区域关联的ram地址,GPA
bool subpage;
bool terminates;
bool romd_mode;
bool ram;//是否是ram
环球雅思学校bool readonly; /* For RAM regions */
bool enabled;
bool rom_device;
bool warning_printed; /* For rervations */
bool flush_coalesced_mmio;
MemoryRegion *alias;
hwaddr alias_offt;
int priority;
bool may_overlap;
QTAILQ_HEAD(subregions, MemoryRegion) subregions;//⼦区域链表头
QTAILQ_ENTRY(MemoryRegion) subregions_link;//⼦区域链表节点
QTAILQ_HEAD(coalesced_ranges, CoalescedMemoryRange) coalesced;
loveagainconst char *name;
uint8_t dirty_log_mask;
unsigned ioeventfd_nb;
MemoryRegionIoeventfd *ioeventfds;
NotifierList iommu_notify;
};
MemoryRegion结构如上,相关注释已经列举,其中parent指向⽗MR,默认是NULL,size表⽰区域的⼤⼩;默认是64位下的最⼤地址;ram_addr⽐较重要,是区域关联的客户机物理地址空间的偏移,也就是客户机物理地址。alias表明该区域是某⼀类型的区域(先这么说吧),这么说不知是否合适,实际上虚拟机的ram申请时时⼀次性申请的⼀个完成的ram,记录在⼀个MR中,之后⼜对此ram按照size 进⾏了划分,形成subregion,⽽subregion 的alias便指向原始的MR,⽽alias_offt 便是在原始ram中的偏移。对于系统地址空间的ram,会把刚才得到的subregion注册到系统中,⽗MR是刚才提到的全局MR system_memory,subregions_link是链表节点。前⾯提到,实际关联host内存的是subregion->alias指向的MR,其ram_addr是该MR在虚拟机的物理内存中的偏移,具体是由RAMBlock-
>offt获得的,RAMBlock最直接的接触host的内存,看下其结构
typedef struct RAMBlock {
struct MemoryRegion *mr;
uint8_t *host;/*block关联的内存,HVA*/
ram_addr_t offt;/*在vm物理内存中的偏移 GPA*/
ram_addr_t length;/*block的长度*/
uint32_t flags;
char idstr[256];
/* Reads can take either the iothread or the ramlist lock.
* Writes must take both locks.
*/
QTAILQ_ENTRY(RAMBlock) next;
int fd;
} RAMBlock;
仅有的⼏个字段意义⽐较明确,理论上⼀个RAMBlock就代表host的⼀段虚拟 内存,host指向申请的ram的虚拟地址,是HVA。所有的RAMBlock通过next字段连接起来,表头保存在⼀个全局的RAMList结构中,但是根据代码来看,原始MR分配内存时分配的是⼀整块block,之所以这样做也许是为了扩展⽤吧!!RAMList中有个字段mru_block指针,指向最近使⽤的block,这样需要遍历所有的block 时,先判断指定的block是否是mru_block,如果不是再进⾏遍历从⽽提⾼效率。townhou
qemu的内存管理在交付给KVM管理时,中间⼜加了⼀个抽象层,叫做address_space.如果说MR管理的host的内存,那么
address_space管理的更偏向于虚拟机。正如其名字所描述的,它是管理地址空间的,qemu中有⼏个全局的
AddressSpace,address_space_memory和address_space_io,很明显⼀个是管理系统地址空间,⼀个是IO地址空间。它是如何进⾏管理的呢?展开下AddressSpace的结构;
struct AddressSpace {
/* All fields are private. */
char *name;
MemoryRegion *root;
struct FlatView *current_map;/*对应的flatview*/
int ioeventfd_nb;
struct MemoryRegionIoeventfd *ioeventfds;
struct AddressSpaceDispatch *dispatch;
struct AddressSpaceDispatch *next_dispatch;
MemoryListener dispatch_listener;
QTAILQ_ENTRY(AddressSpace) address_spaces_link;
};
对于该结构,源码的注释或许更能解释:AddressSpace: describes a mapping of address to #MemoryRegion objects,很明显是把MR映射到虚拟机的物理地址空间。root指向根MR,对于address_space_memory来讲,root指向系统全局的MR
system_memory,current_map指向⼀个FlatView结构,其他的字段咱们先暂时忽略,所有的AddressSpace通过结构中的
address_spaces_link连接成链表,表头保存在全局的AddressSpace结构中。FlatView管理MR展开后得到的所有FlatRange,看下FlatView
struct FlatView {
unsigned ref;//引⽤计数,为0时就销毁
FlatRange *ranges;/*对应的flatrange数组*/
unsigned nr;/*flatrange 的数⽬*/
unsigned nr_allocated;//当前数组的项数
};
各个字段的意义就不说了,ranges是⼀个数组,记录FlatView下所有的FlatRange,每个FlatRange对应⼀段虚拟机物理地址区间,各个FlatRange不会重叠,按照地址的顺序保存在数组中。FlatRange结构如下
struct FlatRange {
MemoryRegion *mr;/*指向所属的MR*/
hwaddr offt_in_region;/*在MR中的offt*/
AddrRange addr;/*本FR代表的区间*/
uint8_t dirty_log_mask;
bool romd_mode;
bool readonly;/*是否是只读*/
};
具体的范围由⼀个AddrRange结构描述,其描述了地址和⼤⼩,offt_in_region表⽰该区间在全局的MR中的offt,根据此可以进⾏GPA到HVA的转换,mr指向所属的MR。
到此为⽌,负责管理的结构基本就介绍完毕,剩余⼏个主要起中介的作⽤,MemoryRegionSection对应于FlatRange,⼀个FlatRange代表⼀个物理地址空间的⽚段,但是其偏向于address-space,⽽MemoryRegionSection则在MR端显⽰的表明了分⽚,其结构如下
struct MemoryRegionSection {
MemoryRegion *mr;//所属的MemoryRegion
AddressSpace *address_space;//region关联的AddressSpace
hwaddr offt_within_region;//在region内部的偏移
Int128 size;//ction的⼤⼩
hwaddr offt_within_address_space;//⾸个字节的地址在ction中的偏移
bool readonly;//是否是只读
};
其中注意两个偏移,offt_within_region和offt_within_address_space。前者描述的是该ction在整个MR中的偏移,⼀个address_space可能有多个MR构成,因此该offt是局部的。⽽offt_within_address_space是在整个地址空间中的偏移,是全局的offt。
KVMSlot也是⼀个中介,只不过更加接近kvm了,
southporttypedef struct KVMSlot
{
hwaddr start_addr;//客户机物理地址 GPA
ram_addr_t memory_size;//内存⼤⼩
void *ram;//HVA qemu⽤户空间地址
int slot;//slot编号
int flags;
} KVMSlot;
kvm_urspace_memory_region是和kvm共享的⼀个结构,说共享不太恰当,但是其实最终作为参数给kvm使⽤的,kvm获取控制权后,从栈中复制该结构到内核,其中字段意思就很简单,不在赘述。
整体布局⼤致如图所⽰
三、具体实现机制
qemu部分的内存申请流程上可以分为三⼩部分,分成三⼩部分主要是我在看代码的时候觉得这三部分耦合性不是很⼤,相对⽽⾔⽐较独⽴。众所周知,qemu起始于vl.c中的main函数,那么这三部分也按
照在main函数中的调⽤顺序分别介绍。
3.1 回调函数的注册diwali festival
涉及函数:configure_accelerator() -->kvm_init()-->memory_listener_register ()
这⾥所说的accelerator在这⾥就是kvm,初始化函数⾃然调⽤了kvm_init,该函数主要完成对kvm的初始化,包括⼀些常规检查如CPU个数、kvm版本等,还会通过ioctl和内核交互创建kvm结构,这些并⾮本⽂重点,不在赘述。在kvm_init函数的结尾调⽤了
memory_listener_register
memory_listener_register(&kvm_memory_listener, &address_space_memory);
memory_listener_register(&kvm_io_listener, &address_space_io);
通过memory_listener_register函数,针对地址空间注册了linter,linter本⾝是⼀组函数表,当地址空间发⽣变化的时候,会调⽤到listener中的相应函数,从何保持内核和⽤户空间的内存信息的⼀致性。虚拟机包含有两个地址空间address_space_memory和
address_space_io,很容易理解,正常系统也包含系统地址空间和IO地址空间。
memory_listener_register函数不复杂,咱们看下
void memory_listener_register(MemoryListener *listener, AddressSpace *filter)
{
MemoryListener *other = NULL;
描写桂林山水的作文
AddressSpace *as;
listener->address_space_filter = filter;
/*如果listener为空或者当前listener的优先级⼤于最后⼀个listener的优先级,则可以直接插⼊*/
if (QTAILQ_EMPTY(&memory_listeners)
|| listener->priority >= QTAILQ_LAST(&memory_listeners,
memory_listeners)->priority) {
QTAILQ_INSERT_TAIL(&memory_listeners, listener, link);
} el {
/*listener按照优先级升序排列*/longly
QTAILQ_FOREACH(other, &memory_listeners, link) {
if (listener->priority < other->priority) {
break;
}
}
/*插⼊listener*/
QTAILQ_INSERT_BEFORE(other, listener, link);
}
/
*全局address_spaces-->as*/
/*对于每个address_spaces,设置listener*/
QTAILQ_FOREACH(as, &address_spaces, address_spaces_link) {
listener_add_address_space(listener, as);
}
}
系统中可以存在多个listener,listener之间有着明确的优先级关系,通过链表进⾏组织,链表头是全局的memory_listeners。函数中,如果memory_listeners为空或者当前listener的优先级⼤于最后⼀个listener的优先级,即直接把当前listener插⼊。否则需要挨个遍历链表,找到合适的位置。具体按照优先级升序查找。在函数最后还针对每个address_space,调⽤listener_add_address_space函数,该函数对其对应的address_space管理的flatrange向KVM注册。当然,实际上此时address_space尚未经过初始化,所以这⾥的循环其实是空循环。
3.2 Address_Space的初始化
涉及函数:cpu_exec_init_all() memory_map_init()
在第⼀节中已经注册了listener,但是addressspace尚未初始化,本节就介绍下其初始化流程。从上节的configure_accelerator()函数往下⾛,会执⾏cpu_exec_init_all()函数,该函数主要初始化了IO地址空间和系统地址空间。memory_map_init()函数初始化系统地址空间,有⼀个全局的MemoryRegion指针system_memory指向该区域的MemoryRegion结构。