【技术分享】QEMU内存虚拟化源码分析
2017-07-12 10:10:48
阅读:377次
点赞(0)
收藏
来源: 安全客
作者:360GearTeam
作者:Terenceli @ 360 Gear Team
投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿
传送门
【技术分享】探索QEMU-KVM中PIO处理的奥秘
内存虚拟化就是为虚拟机提供内存,使得虚拟机能够像在物理机上正常工作,这需要虚拟化软件为虚拟机展示一种物理内存的假象,内存虚拟化是虚拟化技术中关键技术之一。qemu+kvm的虚拟化方案中,内存虚拟化是由qemu和kvm共同完成的。qemu的虚拟地址作为guest的物理地址,一句看似轻描淡写的话幕后的工作确实非常多,加上qemu本身可以独立于kvm,成为一个完整的虚拟化方案,所以其内存虚拟化更加复杂。本文试图全方位的对qemu的内存虚拟化方案进行源码层面的介绍。本文主要介绍qemu在内存虚拟化方面的工作,之后的文章会介绍内存kvm方面的内存虚拟化。
零. 概述
内存虚拟化就是要让虚拟机能够无缝的访问内存,这个内存哪里来的,qemu的进程地址空间分出来的。有了ept之后,CPU在vmx non-root状态的时候进行内存访问会再做一个ept转换。在这个过程中,qemu扮演的角色。1. 首先需要去申请内存用于虚拟机; 2. 需要将虚拟1中申请的地址的虚拟地址与虚拟机的对应的物理地址告诉给kvm,就是指定GPA->HVA的映射关系;3. 需要组织一系列的数据结构去管理控制内存虚拟化,比如,设备注册需要分配物理地址,虚拟机退出之后需要根据地址做模拟等等非常多的工作,由于qemu本身能够支持tcg模式的虚拟化,会显得更加复杂。
首先明确内存虚拟化中QEMU和KVM工作的分界。KVM的ioctl中,设置虚拟机内存的为KVM_SET_USER_MEMORY_REGION,我们看到这个ioctl需要传递的参数是:
/*forKVM_SET_USER_MEMORY_REGION*/
structkvm_userspace_memory_region{
__u32slot;
__u32flags;
__u64guest_phys_addr;
__u64memory_size;/*bytes*/
__u64userspace_addr;/*startoftheuserspaceallocatedmemory*/
};
这个ioctl主要就是设置GPA到HVA的映射。看似简单的工作在qemu里面却很复杂,下面逐一剖析之。
一. 相关数据结构
首先,qemu中用AddressSpace用来表示CPU/设备看到的内存,一个AddressSpace下面包含多个MemoryRegion,这些MemoryRegion结构通过树连接起来,树的根是AddressSpace的root域。
structAddressSpace{
/*Allfieldsareprivate.*/
structrcu_headrcu;
char*name;
MemoryRegion*root;
intref_count;
boolmalloced;
/*AccessedviaRCU.*/
structFlatView*current_map;
intioeventfd_nb;
structMemoryRegionIoeventfd*ioeventfds;
structAddressSpaceDispatch*dispatch;
structAddressSpaceDispatch*next_dispatch;
MemoryListenerdispatch_listener;
QTAILQ_HEAD(memory_listeners_as,MemoryListener)listeners;
QTAILQ_ENTRY(AddressSpace)address_spaces_link;
};
structMemoryRegion{
Objectparent_obj;
/*Allfieldsareprivate-violatorswillbeprosecuted*/
/*Thefollowingfieldsshouldfitinacacheline*/
boolromd_mode;
boolram;
boolsubpage;
boolreadonly;/*ForRAMregions*/
boolrom_device;
boolflush_coalesced_mmio;
boolglobal_locking;
uint8_tdirty_log_mask;
RAMBlock*ram_block;
...
constMemoryRegionOps*ops;
void*opaque;
MemoryRegion*container;
Int128size;
hwaddraddr;
...
MemoryRegion*alias;
hwaddralias_offset;
int32_tpriority;
QTAILQ_HEAD(subregions,MemoryRegion)subregions;
QTAILQ_ENTRY(MemoryRegion)subregions_link;
QTAILQ_HEAD(coalesced_ranges,CoalescedMemoryRange)coalesced;
...
};
MemoryRegion有多种类型,可以表示一段ram,rom,MMIO,alias,alias表示一个MemoryRegion的一部分区域,MemoryRegion也可以表示一个container,这就表示它只是其他若干个MemoryRegion的容器。在MemoryRegion中,'ram_block'表示的是分配的实际内存。
structRAMBlock{
structrcu_headrcu;
structMemoryRegion*mr;
uint8_t*host;
ram_addr_toffset;
ram_addr_tused_length;
ram_addr_tmax_length;
void(*resized)(constchar*,uint64_tlength,void*host);
uint32_tflags;
/*Protectedbyiothreadlock.*/
charidstr[256];
/*RCU-enabled,writesprotectedbytheramlistlock*/
QLIST_ENTRY(RAMBlock)next;
intfd;
size_tpage_size;
};
在这里,'host'指向了动态分配的内存,用于表示实际的虚拟机物理内存,而offset表示了这块内存在虚拟机物理内存中的偏移。每一个ram_block还会被连接到全局的'ram_list'链表上。Address, MemoryRegion, RAMBlock关系如下图所示。
AddressSpace下面root及其子树形成了一个虚拟机的物理地址,但是在往kvm进行设置的时候,需要将其转换为一个平坦的地址模型,也就是从0开始的。这个就用FlatView表示,一个AddressSpace对应一个FlatView。
structFlatView{
structrcu_headrcu;
unsignedref;
FlatRange*ranges;
unsignednr;
unsignednr_allocated;
};
在FlatView中,FlatRange表示按照需要被切分为了几个范围。
在内存虚拟化中,还有一个重要的结构是MemoryRegionSection,这个结构通过函数section_from_flat_range可由FlatRange转换过来。
structMemoryRegionSection{
MemoryRegion*mr;
AddressSpace*address_space;
hwaddroffset_within_region;
Int128size;
hwaddroffset_within_address_space;
boolreadonly;
};
MemoryRegionSection表示的是MemoryRegion的一部分。这个其实跟FlatRange差不多。这几个数据结构关系如下:
为了监控虚拟机的物理地址访问,对于每一个AddressSpace,会有一个MemoryListener与之对应。每当物理映射(GPA->HVA)发生改变时,会回调这些函数。所有的MemoryListener都会挂在全局变量memory_listeners链表上。同时,AddressSpace也会有一个链表连接器自己注册的MemoryListener。
structMemoryListener{
void(*begin)(MemoryListener*listener);
void(*commit)(MemoryListener*listener);
void(*region_add)(MemoryListener*listener,MemoryRegionSection*section);
void(*region_del)(MemoryListener*listener,MemoryRegionSection*section);
void(*region_nop)(MemoryListener*listener,MemoryRegionSection*section);
void(*log_start)(MemoryListener*listener,MemoryRegionSection*section,
intold,intnew);
void(*log_stop)(MemoryListener*listener,MemoryRegionSection*section,
intold,intnew);
void(*log_sync)(MemoryListener*listener,MemoryRegionSection*section);
void(*log_global_start)(MemoryListener*listener);
void(*log_global_stop)(MemoryListener*listener);
void(*eventfd_add)(MemoryListener*listener,MemoryRegionSection*section,
boolmatch_data,uint64_tdata,EventNotifier*e);
void(*eventfd_del)(MemoryListener*listener,MemoryRegionSection*section,
boolmatch_data,uint64_tdata,EventNotifier*e);
void(*coalesced_mmio_add)(MemoryListener*listener,MemoryRegionSection*section,
hwaddraddr,hwaddrlen);
void(*coalesced_mmio_del)(MemoryListener*listener,MemoryRegionSection*section,
hwaddraddr,hwaddrlen);
/*Lower=earlier(duringadd),later(duringdel)*/
unsignedpriority;
AddressSpace*address_space;
QTAILQ_ENTRY(MemoryListener)link;
QTAILQ_ENTRY(MemoryListener)link_as;
};
为了在虚拟机退出时,能够顺利根据物理地址找到对应的HVA地址,qemu会有一个AddressSpaceDispatch结构,用来在AddressSpace中进行位置的找寻,继而完成对IO/MMIO地址的访问。
structAddressSpaceDispatch{
structrcu_headrcu;
MemoryRegionSection*mru_section;
/*Thisisamulti-levelmaponthephysicaladdressspace.
*ThebottomlevelhaspointerstoMemoryRegionSections.
*/
PhysPageEntryphys_map;
PhysPageMapmap;
AddressSpace*as;
};
这里面有一个PhysPageMap,这其实也是保存了一个GPA->HVA的一个映射,通过多层页表实现,当kvm exit退到qemu之后,通过这个AddressSpaceDispatch里面的map查找对应的MemoryRegionSection,继而找到对应的主机HVA。这几个结构体的关系如下:
下面对流程做一些分析。
二. 初始化
首先在main->cpu_exec_init_all->memory_map_init中对全局的memory和io进行初始化,system_memory作为address_space_memory的根MemoryRegion,大小涵盖了整个64位空间的大小,当然,这是一个pure contaner,并不会分配空间的,system_io作为address_space_io的根MemoryRegion,大小为65536,也就是平时的io port空间。
staticvoidmemory_map_init(void)
{
system_memory=g_malloc(sizeof(*system_memory));
memory_region_init(system_memory,NULL,"system",UINT64_MAX);
address_space_init(&address_space_memory,system_memory,"memory");
system_io=g_malloc(sizeof(*system_io));
memory_region_init_io(system_io,NULL,&unassigned_io_ops,NULL,"io",
65536);
address_space_init(&address_space_io,system_io,"I/O");
}
在随后的cpu初始化之中,还会初始化多个AddressSpace,这些很多都是disabled的,对虚拟机意义不大。重点在随后的main->pc_init_v2_8->pc_init1->pc_memory_init中,这里面是分配系统ram,也是第一次真正为虚拟机分配物理内存。整个过程中,分配内存也不会像MemoryRegion那么频繁,mr很多时候是创建一个alias,指向已经存在的mr的一部分,这也是alias的作用,就是把一个mr分割成多个不连续的mr。真正分配空间的大概有这么几个,pc.ram, pc.bios, pc.rom, 以及设备的一些ram, rom等,vga.vram, vga.rom, e1000.rom等。
分配pc.ram的流程如下:
memory_region_allocate_system_memory
allocate_system_memory_nonnuma
memory_region_init_ram
qemu_ram_alloc
ram_block_add
phys_mem_alloc
qemu_anon_ram_alloc
qemu_ram_mmap
mmap
可以看到,qemu通过使用mmap创建一个内存映射来作为ram。
继续pc_memory_init,函数在创建好了ram并且分配好了空间之后,创建了两个mr alias,ram_below_4g以及ram_above_4g,这两个mr分别指向ram的低4g以及高4g空间,这两个alias是挂在根system_memory mr下面的。以后的情形类似,创建根mr,创建AddressSpace,然后在根mr下面加subregion。
三. 内存的提交
当我们每一次更改上层的内存布局之后,都需要通知到kvm。这个过程是通过一系列的MemoryListener来实现的。首先系统有一个全局的memory_listeners,上面挂上了所有的MemoryListener,在address_space_init->address_space_init_dispatch->memory_listener_register这个过程中完成MemoryListener的注册。
voidaddress_space_init_dispatch(AddressSpace*as)
{
as->dispatch=NULL;
as->dispatch_listener=(MemoryListener){
.begin=mem_begin,
.commit=mem_commit,
.region_add=mem_add,
.region_nop=mem_add,
.priority=0,
};
memory_listener_register(&as->dispatch_listener,as);
}
这里有初始化了listener的几个回调,他们的的调用时间之后讨论。 值得注意的是,并不是只有AddressSpace初始化的时候会注册回调,kvm_init同样会注册回调。
staticintkvm_init(MachineState*ms)
{
...
kvm_memory_listener_register(s,&s->memory_listener,
&address_space_memory,0);
memory_listener_register(&kvm_io_listener,
&address_space_io);
...
}
voidkvm_memory_listener_register(KVMState*s,KVMMemoryListener*kml,
AddressSpace*as,intas_id)
{
inti;
kml->slots=g_malloc0(s->nr_slots*sizeof(KVMSlot));
kml->as_id=as_id;
for(i=0;i<s->nr_slots;i++){
kml->slots[i].slot=i;
}
kml->listener.region_add=kvm_region_add;
kml->listener.region_del=kvm_region_del;
kml->listener.log_start=kvm_log_start;
kml->listener.log_stop=kvm_log_stop;
kml->listener.log_sync=kvm_log_sync;
kml->listener.priority=10;
memory_listener_register(&kml->listener,as);
}
在这里我们看到kvm也注册了自己的MemoryListener。
在上面看到MemoryListener之后,我们看看什么时候需要更新内存。 进行内存更新有很多个点,比如我们新创建了一个AddressSpace address_space_init,再比如我们将一个mr添加到另一个mr的subregions中memory_region_add_subregion,再比如我们更改了一端内存的属性memory_region_set_readonly,将一个mr设置使能或者非使能memory_region_set_enabled, 总之一句话,我们修改了虚拟机的内存布局/属性时,就需要通知到各个Listener,这包括各个AddressSpace对应的,以及kvm注册的,这个过程叫做commit,通过函数memory_region_transaction_commit实现。
voidmemory_region_transaction_commit(void)
{
AddressSpace*as;
assert(memory_region_transaction_depth);
--memory_region_transaction_depth;
if(!memory_region_transaction_depth){
if(memory_region_update_pending){
MEMORY_LISTENER_CALL_GLOBAL(begin,Forward);
QTAILQ_FOREACH(as,&address_spaces,address_spaces_link){
address_space_update_topology(as);
}
MEMORY_LISTENER_CALL_GLOBAL(commit,Forward);
}elseif(ioeventfd_update_pending){
QTAILQ_FOREACH(as,&address_spaces,address_spaces_link){
address_space_update_ioeventfds(as);
}
}
memory_region_clear_pending();
}
}
#defineMEMORY_LISTENER_CALL_GLOBAL(_callback,_direction,_args...)\
do{\
MemoryListener*_listener;\
\
switch(_direction){\
caseForward:\
QTAILQ_FOREACH(_listener,&memory_listeners,link){\
if(_listener->_callback){\
_listener->_callback(_listener,##_args);\
}\
}\
break;\
caseReverse:\
QTAILQ_FOREACH_REVERSE(_listener,&memory_listeners,\
memory_listeners,link){\
if(_listener->_callback){\
_listener->_callback(_listener,##_args);\
}\
}\
break;\
default:\
abort();\
}\
}while(0)
MEMORY_LISTENER_CALL_GLOBAL对memory_listeners上的各个MemoryListener调用指定函数。commit中最重要的是address_space_update_topology调用。
staticvoidaddress_space_update_topology(AddressSpace*as)
{
FlatView*old_view=address_space_get_flatview(as);
FlatView*new_view=generate_memory_topology(as->root);
address_space_update_topology_pass(as,old_view,new_view,false);
address_space_update_topology_pass(as,old_view,new_view,true);
/*WritesareprotectedbytheBQL.*/
atomic_rcu_set(&as->current_map,new_view);
call_rcu(old_view,flatview_unref,rcu);
/*NotethatalltheoldMemoryRegionsarestillaliveuptothis
*point.ThisrelievesmostMemoryListenersfromtheneedto
*ref/unreftheMemoryRegionstheyget---unlesstheyusethem
*outsidetheiothreadmutex,inwhichcaseprecisereference
*countingisnecessary.
*/
flatview_unref(old_view);
address_space_update_ioeventfds(as);
}
前面我们已经说了,as->root会被展开为一个FlatView,所以在这里update topology中,首先得到上一次的FlatView,之后调用generate_memory_topology生成一个新的FlatView,
staticFlatView*generate_memory_topology(MemoryRegion*mr)
{
FlatView*view;
view=g_new(FlatView,1);
flatview_init(view);
if(mr){
render_memory_region(view,mr,int128_zero(),
addrrange_make(int128_zero(),int128_2_64()),false);
}
flatview_simplify(view);
returnview;
}
最主要的是render_memory_region生成view,这个render函数很复杂,需要递归render子树,具体以后有机会单独讨论。在生成了view之后会调用flatview_simplify进行简化,主要是合并相邻的FlatRange。在生成了当前as的FlatView之后,我们就可以更新了,这在函数address_space_update_topology_pass中完成,这个函数就是逐一对比新旧FlatView的差别,然后进行更新。
staticvoidaddress_space_update_topology_pass(AddressSpace*as,
constFlatView*old_view,
constFlatView*new_view,
booladding)
{
unsignediold,inew;
FlatRange*frold,*frnew;
/*Generateasymmetricdifferenceoftheoldandnewmemorymaps.
*Killrangesintheoldmap,andinstantiaterangesinthenewmap.
*/
iold=inew=0;
while(iold<old_view->nr||inew<new_view->nr){
if(iold<old_view->nr){
frold=&old_view->ranges[iold];
}else{
frold=NULL;
}
if(inew<new_view->nr){
frnew=&new_view->ranges[inew];
}else{
frnew=NULL;
}
if(frold
&&(!frnew
||int128_lt(frold->addr.start,frnew->addr.start)
||(int128_eq(frold->addr.start,frnew->addr.start)
&&!flatrange_equal(frold,frnew)))){
/*Inoldbutnotinnew,orinbothbutattributeschanged.*/
if(!adding){
MEMORY_LISTENER_UPDATE_REGION(frold,as,Reverse,region_del);
}
++iold;
}elseif(frold&&frnew&&flatrange_equal(frold,frnew)){
/*Inbothandunchanged(exceptloggingmayhavechanged)*/
if(adding){
MEMORY_LISTENER_UPDATE_REGION(frnew,as,Forward,region_nop);
if(frnew->dirty_log_mask&~frold->dirty_log_mask){
MEMORY_LISTENER_UPDATE_REGION(frnew,as,Forward,log_start,
frold->dirty_log_mask,
frnew->dirty_log_mask);
}
if(frold->dirty_log_mask&~frnew->dirty_log_mask){
MEMORY_LISTENER_UPDATE_REGION(frnew,as,Reverse,log_stop,
frold->dirty_log_mask,
frnew->dirty_log_mask);
}
}
++iold;
++inew;
}else{
/*Innew*/
if(adding){
MEMORY_LISTENER_UPDATE_REGION(frnew,as,Forward,region_add);
}
++inew;
}
}
}
最重要的当然是MEMORY_LISTENER_UPDATE_REGION宏,这个宏会将每一个FlatRange转换为一个MemoryRegionSection,之后调用这个as对应的各个MemoryListener的回调函数。这里我们以kvm对象注册Listener为例,从kvm_memory_listener_register,我们看到其region_add回调为kvm_region_add。
staticvoidkvm_region_add(MemoryListener*listener,
MemoryRegionSection*section)
{
KVMMemoryListener*kml=container_of(listener,KVMMemoryListener,listener);
memory_region_ref(section->mr);
kvm_set_phys_mem(kml,section,true);
}
这个函数看似复杂,主要是因为,需要判断变化的各种情况是否与之前的重合,是否是脏页等等情况。我们只看最开始的情况。
staticvoidkvm_set_phys_mem(KVMMemoryListener*kml,
MemoryRegionSection*section,booladd)
{
KVMState*s=kvm_state;
KVMSlot*mem,old;
interr;
MemoryRegion*mr=section->mr;
boolwriteable=!mr->readonly&&!mr->rom_device;
hwaddrstart_addr=section->offset_within_address_space;
ram_addr_tsize=int128_get64(section->size);
void*ram=NULL;
unsigneddelta;
/*kvmworksinpagesizechunks,butthefunctionmaybecalled
withsub-pagesizeandunalignedstartaddress.Padthestart
addresstonextandtruncatesizetopreviouspageboundary.*/
delta=qemu_real_host_page_size-(start_addr&~qemu_real_host_page_mask);
delta&=~qemu_real_host_page_mask;
if(delta>size){
return;
}
start_addr+=delta;
size-=delta;
size&=qemu_real_host_page_mask;
if(!size||(start_addr&~qemu_real_host_page_mask)){
return;
}
if(!memory_region_is_ram(mr)){
if(writeable||!kvm_readonly_mem_allowed){
return;
}elseif(!mr->romd_mode){
/*Ifthememorydeviceisnotinromd_mode,thenweactuallywant
*toremovethekvmmemoryslotsoallaccesseswilltrap.*/
add=false;
}
}
ram=memory_region_get_ram_ptr(mr)+section->offset_within_region+delta;
...
if(!size){
return;
}
if(!add){
return;
}
mem=kvm_alloc_slot(kml);
mem->memory_size=size;
mem->start_addr=start_addr;
mem->ram=ram;
mem->flags=kvm_mem_flags(mr);
err=kvm_set_user_memory_region(kml,mem);
if(err){
fprintf(stderr,"%s:errorregisteringslot:%s\n",__func__,
strerror(-err));
abort();
}
}
这个函数主要就是得到MemoryRegionSection在address_space中的位置,这个就是虚拟机的物理地址,函数中是start_addr, 然后通过memory_region_get_ram_ptr得到对应其对应的qemu的HVA地址,函数中是ram,当然还有大小的size以及这块内存的flags,这些参数组成了一个KVMSlot,之后传递给kvm_set_user_memory_region。
staticintkvm_set_user_memory_region(KVMMemoryListener*kml,KVMSlot*slot)
{
KVMState*s=kvm_state;
structkvm_userspace_memory_regionmem;
mem.slot=slot->slot|(kml->as_id<<16);
mem.guest_phys_addr=slot->start_addr;
mem.userspace_addr=(unsignedlong)slot->ram;
mem.flags=slot->flags;
if(slot->memory_size&&mem.flags&KVM_MEM_READONLY){
/*Settheslotsizeto0beforesettingtheslottothedesired
*value.ThisisneededbasedonKVMcommit75d61fbc.*/
mem.memory_size=0;
kvm_vm_ioctl(s,KVM_SET_USER_MEMORY_REGION,&mem);
}
mem.memory_size=slot->memory_size;
returnkvm_vm_ioctl(s,KVM_SET_USER_MEMORY_REGION,&mem);
}
通过层层抽象,我们终于完成了GPA->HVA的对应,并且传递到了KVM。
四. kvm exit之后的内存寻址
在address_space_init_dispatch函数中,我们可以看到,每一个通过AddressSpace都会注册一个Listener回调,回调的各个函数都一样,mem_begin, mem_add等。
voidaddress_space_init_dispatch(AddressSpace*as)
{
as->dispatch=NULL;
as->dispatch_listener=(MemoryListener){
.begin=mem_begin,
.commit=mem_commit,
.region_add=mem_add,
.region_nop=mem_add,
.priority=0,
};
memory_listener_register(&as->dispatch_listener,as);
}
我们重点看看mem_add
staticvoidmem_add(MemoryListener*listener,MemoryRegionSection*section)
{
AddressSpace*as=container_of(listener,AddressSpace,dispatch_listener);
AddressSpaceDispatch*d=as->next_dispatch;
MemoryRegionSectionnow=*section,remain=*section;
Int128page_size=int128_make64(TARGET_PAGE_SIZE);
if(now.offset_within_address_space&~TARGET_PAGE_MASK){
uint64_tleft=TARGET_PAGE_ALIGN(now.offset_within_address_space)
-now.offset_within_address_space;
now.size=int128_min(int128_make64(left),now.size);
register_subpage(d,&now);
}else{
now.size=int128_zero();
}
while(int128_ne(remain.size,now.size)){
remain.size=int128_sub(remain.size,now.size);
remain.offset_within_address_space+=int128_get64(now.size);
remain.offset_within_region+=int128_get64(now.size);
now=remain;
if(int128_lt(remain.size,page_size)){
register_subpage(d,&now);
}elseif(remain.offset_within_address_space&~TARGET_PAGE_MASK){
now.size=page_size;
register_subpage(d,&now);
}else{
now.size=int128_and(now.size,int128_neg(page_size));
register_multipage(d,&now);
}
}
}
mem_add在添加了内存区域之后会被调用,调用路径为
address_space_update_topology_pass
MEMORY_LISTENER_UPDATE_REGION(frnew,as,Forward,region_add);
#defineMEMORY_LISTENER_UPDATE_REGION(fr,as,dir,callback,_args...)\
do{\
MemoryRegionSectionmrs=section_from_flat_range(fr,as);\
MEMORY_LISTENER_CALL(as,callback,dir,&mrs,##_args);\
}while(0)
如果新增加了一个FlatRange,则会调用将该fr转换为一个MemroyRegionSection,然后调用Listener的region_add。
回到mem_add,这个函数主要是调用两个函数如果是添加的地址落到一个页内,则调用register_subpage,如果是多个页,则调用register_multipage,先看看register_multipage,因为最开始注册都是一波大的,比如pc.ram。首先now.offset_within_address_space并不会落在一个页内。所以直接进入while循环,之后进入register_multipage,d这个AddressSpaceDispatch是在mem_begin创建的。
staticvoidregister_multipage(AddressSpaceDispatch*d,
MemoryRegionSection*section)
{
hwaddrstart_addr=section->offset_within_address_space;
uint16_tsection_index=phys_section_add(&d->map,section);
uint64_tnum_pages=int128_get64(int128_rshift(section->size,
TARGET_PAGE_BITS));
assert(num_pages);
phys_page_set(d,start_addr>>TARGET_PAGE_BITS,num_pages,section_index);
}
首先分一个d->map->sections空间出来,其index为section_index。
staticvoidphys_page_set(AddressSpaceDispatch*d,
hwaddrindex,hwaddrnb,
uint16_tleaf)
{
/*Wildlyoverreserve-itdoesn'tmattermuch.*/
phys_map_node_reserve(&d->map,3*P_L2_LEVELS);
phys_page_set_level(&d->map,&d->phys_map,&index,&nb,leaf,P_L2_LEVELS-1);
}
之后start_addr右移12位,计算出总共需要多少个页。这里说一句,qemu在这里总共使用了6级页表,最后一级长度12,然后是5 * 9 + 7。phys_map_node_reserve首先分配页目录项。
staticvoidphys_map_node_reserve(PhysPageMap*map,unsignednodes)
{
staticunsignedalloc_hint=16;
if(map->nodes_nb+nodes>map->nodes_nb_alloc){
map->nodes_nb_alloc=MAX(map->nodes_nb_alloc,alloc_hint);
map->nodes_nb_alloc=MAX(map->nodes_nb_alloc,map->nodes_nb+nodes);
map->nodes=g_renew(Node,map->nodes,map->nodes_nb_alloc);
alloc_hint=map->nodes_nb_alloc;
}
}
phys_page_set_level填充页表。初始调用时,level为5,因为要从最开始一层填充。
staticvoidphys_page_set_level(PhysPageMap*map,PhysPageEntry*lp,
hwaddr*index,hwaddr*nb,uint16_tleaf,
intlevel)
{
PhysPageEntry*p;
hwaddrstep=(hwaddr)1<<(level*P_L2_BITS);
if(lp->skip&&lp->ptr==PHYS_MAP_NODE_NIL){
lp->ptr=phys_map_node_alloc(map,level==0);
}
p=map->nodes[lp->ptr];
lp=&p[(*index>>(level*P_L2_BITS))&(P_L2_SIZE-1)];
while(*nb&&lp<&p[P_L2_SIZE]){
if((*index&(step-1))==0&&*nb>=step){
lp->skip=0;
lp->ptr=leaf;
*index+=step;
*nb-=step;
}else{
phys_page_set_level(map,lp,index,nb,leaf,level-1);
}
++lp;
}
}
这个函数主要就是建立一个多级页表。如图所示
![【技术分享】QEMU内存虚拟化源码分析]()
structPhysPageEntry{
/*Howmanybitsskiptonextlevel(inunitsofL2_SIZE).0foraleaf.*/
uint32_tskip:6;
/*indexintophys_sections(!skip)orphys_map_nodes(skip)*/
uint32_tptr:26;
};
简单说说PhysPageEntry, skip表示需要移动多少步到下一级页表,如果skip为0,说明这是最末级页表了,ptr指向的是map->sections数组的某一项。如果skip不为0,则ptr指向的是哪一个node,也就是页目录。总而言之,这个函数的作用就是建立起一个多级页表,最末尾的页表项表示的是MemoryRegionSection,这跟OS里面的页表是一个道理,而AddressSpaceDispatch中的phys_map域则相当于CR3寄存器,用来最开始的寻址。
好了,我们已经分析好了register_multipage。现在看看register_subpage。
为什么会有在一个页面内注册的需求呢,我的理解是这样的 我们来看一下io port的分布,很明显在一个page里面会有多个MemoryRegion,所以这些内存空间需要分开的MemroyRegionSection,但是呢,这种情况又不是很普遍的,对于内存来说,很多时候1页,2页都是同一个MemoryRegion,总不能对于所有的地址都来一个MemoryRegionSection,所以呢,才会有这么一个subpage,有需要的时候再创建,没有就是整个mutipage。
0000000000000000-0000000000000007(prio0,RW):dma-chan
0000000000000008-000000000000000f(prio0,RW):dma-cont
0000000000000020-0000000000000021(prio0,RW):kvm-pic
0000000000000040-0000000000000043(prio0,RW):kvm-pit
0000000000000060-0000000000000060(prio0,RW):i8042-data
0000000000000061-0000000000000061(prio0,RW):pcspk
0000000000000064-0000000000000064(prio0,RW):i8042-cmd
0000000000000070-0000000000000071(prio0,RW):rtc
有subpage的情况如下图:
好了,有了上面的知识,我们可以来看对于kvm io exit之后的寻址过程了。
intkvm_cpu_exec(CPUState*cpu)
{
switch(run->exit_reason){
caseKVM_EXIT_IO:
DPRINTF("handle_io\n");
/*CalledoutsideBQL*/
kvm_handle_io(run->io.port,attrs,
(uint8_t*)run+run->io.data_offset,
run->io.direction,
run->io.size,
run->io.count);
ret=0;
break;
caseKVM_EXIT_MMIO:
DPRINTF("handle_mmio\n");
/*CalledoutsideBQL*/
address_space_rw(&address_space_memory,
run->mmio.phys_addr,attrs,
run->mmio.data,
run->mmio.len,
run->mmio.is_write);
ret=0;
break;
}
这里我们以KVM_EXIT_IO为例说明
staticvoidkvm_handle_io(uint16_tport,MemTxAttrsattrs,void*data,intdirection,
intsize,uint32_tcount)
{
inti;
uint8_t*ptr=data;
for(i=0;i<count;i++){
address_space_rw(&address_space_io,port,attrs,
ptr,size,
direction==KVM_EXIT_IO_OUT);
ptr+=size;
}
}
可以看到是在全局的address_space_io中寻址,这里我们只看寻址过程,找到HVA之后数据拷贝这些就不说了。
address_space_rw->address_space_write->address_space_translate->address_space_translate_internal
直接看最后一个函数
address_space_translate_internal(AddressSpaceDispatch*d,hwaddraddr,hwaddr*xlat,
hwaddr*plen,boolresolve_subpage)
{
MemoryRegionSection*section;
MemoryRegion*mr;
Int128diff;
section=address_space_lookup_region(d,addr,resolve_subpage);
/*ComputeoffsetwithinMemoryRegionSection*/
addr-=section->offset_within_address_space;
/*ComputeoffsetwithinMemoryRegion*/
*xlat=addr+section->offset_within_region;
mr=section->mr;
if(memory_region_is_ram(mr)){
diff=int128_sub(section->size,int128_make64(addr));
*plen=int128_get64(int128_min(diff,int128_make64(*plen)));
}
returnsection;
}
最重要的当然是找到对应的MemroyRegionSection
staticMemoryRegionSection*address_space_lookup_region(AddressSpaceDispatch*d,
hwaddraddr,
boolresolve_subpage)
{
MemoryRegionSection*section=atomic_read(&d->mru_section);
subpage_t*subpage;
boolupdate;
if(section&§ion!=&d->map.sections[PHYS_SECTION_UNASSIGNED]&&
section_covers_addr(section,addr)){
update=false;
}else{
section=phys_page_find(d->phys_map,addr,d->map.nodes,
d->map.sections);
update=true;
}
if(resolve_subpage&§ion->mr->subpage){
subpage=container_of(section->mr,subpage_t,iomem);
section=&d->map.sections[subpage->sub_section[SUBPAGE_IDX(addr)]];
}
if(update){
atomic_set(&d->mru_section,section);
}
returnsection;
}
d->mru_section作为一个缓存,由于局部性原理,这样可以提高效率。我们看到phys_page_find,类似于一个典型的页表查询过程,通过addr一步一步查找到最后的MemoryRegionSection。
staticMemoryRegionSection*phys_page_find(PhysPageEntrylp,hwaddraddr,
Node*nodes,MemoryRegionSection*sections)
{
PhysPageEntry*p;
hwaddrindex=addr>>TARGET_PAGE_BITS;
inti;
for(i=P_L2_LEVELS;lp.skip&&(i-=lp.skip)>=0;){
if(lp.ptr==PHYS_MAP_NODE_NIL){
return§ions[PHYS_SECTION_UNASSIGNED];
}
p=nodes[lp.ptr];
lp=p[(index>>(i*P_L2_BITS))&(P_L2_SIZE-1)];
}
if(section_covers_addr(§ions[lp.ptr],addr)){
return§ions[lp.ptr];
}else{
return§ions[PHYS_SECTION_UNASSIGNED];
}
}
回到address_space_lookup_region,接着解析subpage,如果之前的subpage部分理解了,这里就很容易了。这样就返回了我们需要的MemoryRegionSection。
五. 总结
写这篇文章算是对qemu内存虚拟化的一个总结,参考了网上大神的文章,感谢之,当然,自己也有不少内容。这篇文章也有很多细节没有写完,比如从mr renader出FlatView,比如,根据前后的FlatView进行memory的commit,如果以后有时间补上。
六. 参考
1. 六六哥的博客
2. OENHAN
传送门
【技术分享】探索QEMU-KVM中PIO处理的奥秘
![【技术分享】QEMU内存虚拟化源码分析]()
![【技术分享】QEMU内存虚拟化源码分析]()
本文由 安全客 原创发布,如需转载请注明来源及本文地址。
本文地址:http://bobao.360.cn/learning/detail/4092.html