qemu内存管理——树状视图

前言

  • qemu模拟虚机内存,核心是维护虚机物理地址空间。这个地址空间既要方便qemu管理,向虚机侧提供内存,又要方便展示和导出,向平台侧提供内存视图。因此qemu抽象的内存区域有两种组织结构,一种是树状的,用于qemu管理并模拟内存,一种是扁平的,用于展示和导出内存视图,也方便传递给KVM
  • 树状视图有两个元素,一是AddressSpace,表示一个cpu可访问的地址空间;一是MemoryRegion,表示一段逻辑内存区域。AddressSpace地址空间由许多逻辑内存区域MemoryRegion组成
  • 扁平化视图同样有两个元素,一是FlatView,cpu可访问地址空间的扁平化表示;一是FlatRange,逻辑内存区域的扁平化描述,表示一段段内存区域。同样地,FlatView由许多FlatRange组成。
  • 本章介绍树状视图

MemoryRegion

数据结构

struct MemoryRegion {
   	......
    bool ram;						/* 标记是否为ram类型的MR */
    bool readonly; 					/* For RAM regions,标记是否为ROM类型的MR */
    RAMBlock *ram_block;			/* 是否关联一段真实的虚拟内存 */
    const MemoryRegionOps *ops;		/* 是否为MMIO类型的MR */
    MemoryRegion *container;		/* 指向包含此MR的容器MR */
    Int128 size;					/* 虚机内存的物理地址大小 */
    hwaddr addr;					/* 虚机内存的绝对物理地址 */
    MemoryRegion *alias;			/* 指向别名MR的MR */
    hwaddr alias_offset;			/* 别名MR在所属MR内的偏移*/
    QTAILQ_HEAD(, MemoryRegion) subregions;		/* 容器MR的子MR组成的链表头部 */
    QTAILQ_ENTRY(MemoryRegion) subregions_link;	/* 用于将子MR组织成链表的成员 */
    unsigned ioeventfd_nb;						/* MR包含的ioeventfd个数 */
    MemoryRegionIoeventfd *ioeventfds;			/* MR包含的ioeventfd数组,用于和内核通信 */
    ......
};

分类

  • MemoryRegion表示一段逻辑内存区域,它的后端可以是以下类型:
  1. RAM:普通内存,qemu通过向主机申请虚拟内存来实现。一旦qemu成功申请这段内存并给出虚机地址,guest对这段内存的读写就和普通进程对虚拟内存的读写一样,没有物理页就触发缺页,有物理页就正常读写。guest对RAM内存的读写不需要qemu干涉,qemu也感知不到有guest对RAM内存进行了读写,qemu成功申请到虚拟内存给guest就算完成了自己的任务,其它的事情就交给了内核的内存管理来做。普通内存的ram字段必为true。
  2. MMIO:映射内存,MMIO模拟的不是普通内存。想像pci设备配置空间的第一个字段device id,它是只读的,通过读取这个字段可以获取设备id用来识别是什么类型的pci设备(网卡,显卡,virtio设备)。在物理环境下,设备id的信息由硬件设备提供,在qemu模拟的环境下,信息就需要qemu提供。因此当guest读取这个字段时,qemu需要做一些“动作”来模拟这个信息。所以读取这个字段时会触发callback,callback种实现对信息的模拟。再考虑配置空间的command字段,它的功能是用来控制pci设备,使能pci设备的某些硬件特性。它是可读写的,当guest写这个字段时,意味着对这个设备进行了配置,qemu需要感知这个“配置”从而模拟该配置的结果,因此也要触发callback,callback种实现对配置结果的模拟。简单讲,MMIO内存在读写时会触发一系列的操作,这些操作由MemoryRegionOps实现,初始化这种类型的MemoryRegion时需要提供MemoryRegionOps。映射内存的ops字段必不为空。
  3. ROM:只读内存,只读内存的读操作和RAM相同,禁止写操作,ROM内存的readonly字段必为true。
  4. ROM device:只读设备,读操作和RAM行为相同,只读设备的允许写操作,写操作和MMIO行为相同,会触发callback。
  5. IOMMU region:将对一段内存的访问转发到另一段内存上,这种类型的内存只用于模拟IOMMU的场景。
  6. container:容器,管理多个MR的MR,用于将多个MR组织成一个内存区域,比如一个pci配置空间,可以抽象成一个容器,它由普通内存(RAM)和映射内存(MMIO)组成。再比如整个虚机的内存地址区域,它被抽象成一个容器,包括了所有虚拟的内存区间。
  7. alias:alias MR指向另一段MR,可以将一段连续的内存空间分割成多个不连续的内存空间,alias MR可以指向其它类型的MR,也可以指向alias MR,但不能指向其本身,否则就回环了。alias MR的alias字段和alias_offset字段必不为空,alias字段指向一段完整的内存空间,alias_offset表示本MR在alias指向的MR的偏移

MR实例

  • 数据结构实例,下面的截图打印了4个MR,类型各不相同,分别是:
  1. container MR:组织管理多个MR,这里是两个,container MR的subregions字段指向其余3个MR组成的链表的头
  2. MMIO MR:内存映射MR,当虚机对这段内存有写操作时,会触发回调
  3. alias MR:指向另一段MR,它是另一段MR的一部分
  4. RAM MR:表示一段真实内存,它的ram字段必为true,并且关联一个RAMBlock
    在这里插入图片描述
    在这里插入图片描述
  • 各个数据结构关系,system MR是acpi MR和ram-blew-4g MR的容器,subregions_link将acpi MR和ram-blew-4g MR链接到一起。pc.ram MR则关联了可用的虚拟内存
    在这里插入图片描述

AddressSpace

数据结构

/**
 * AddressSpace: describes a mapping of addresses to #MemoryRegion objects
 */
struct AddressSpace {
	......
    MemoryRegion *root;				/* 关联的根MR,地址空间拥有了它,就拥有了整棵MR树的内存信息,结构体初始化时这一字段作为输入 */

    /* Accessed via RCU.  */
    struct FlatView *current_map;	/* Root MR对应的扁平化内存视图 */

    int ioeventfd_nb;
    struct MemoryRegionIoeventfd *ioeventfds;
    QTAILQ_HEAD(, MemoryListener) listeners;	/* 用于当地址空间发生变化时通知qemu其它模块或者内核 */
    QTAILQ_ENTRY(AddressSpace) address_spaces_link;
};
  • 从数据结构标注的解释看,地址空间将MR转换成更接近虚拟机侧的内存地址映射。它是一个中间层,起到连结MemoryRegion和FlatView的作用。对比来看,MemoryRegion描述的内存接近主机侧,AddressSpace作为向FlatView转换的中间层,更接近虚机侧。对于qemu模拟的cpu,如果是tcg,一个cpu可以拥有多个地址空间;如果是kvm,只支持一个地址空间(系统地址空间),在其下,可以管理多个子地址空间,比如IO地址空间或内存地址空间。qemu的cpu结构体的cpu_ases是存放地址空间的字段,如下:
/** 
struct CPUState {
	......
    CPUAddressSpace *cpu_ases;	/* 地址空间数组 */
    int num_ases;
    AddressSpace *as;			/* 指向第一个内存地址空间,as和cpu_ases[0]->as都存放了首个内存地址空间的指针 */
    MemoryRegion *memory;
	......
}

 * CPUAddressSpace: all the information a CPU needs about an AddressSpace
 * @cpu: the CPU whose AddressSpace this is
 * @as: the AddressSpace itself
 * @memory_dispatch: its dispatch pointer (cached, RCU protected)
 * @tcg_as_listener: listener for tracking changes to the AddressSpace
 */     
struct CPUAddressSpace {
    CPUState *cpu;
    AddressSpace *as;
    struct AddressSpaceDispatch *memory_dispatch;
    MemoryListener tcg_as_listener;
};

AdressSpace初始化

  • 地址空间的初始化有三个地方,1是静态全局链表,2是qemu准备cpu执行环境时,3是qemu初始化特定硬件类型时。下面分别介绍
  1. 全局链表初始化:qemu有链表将所有地址空间组织到一起,全局变量address_spaces指向这个链表的头部
static QTAILQ_HEAD(, AddressSpace) address_spaces = QTAILQ_HEAD_INITIALIZER(address_spaces);
  1. qemu准备cpu执行环境:将系统内存和IO内存的Root MR和地址空间都进行了初始化。Root MR作为地址空间初始化的输入
cpu_exec_init_all
memory_map_init
    system_memory = g_malloc(sizeof(*system_memory));	/* Root MemoryRegion */
    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");	/* 初始化系统IO空间并添加到全局链表中 */

初始化后的地址空间和根MR关系图如下,除了内存和IO地址空间外,还有cpu的地址空间
在这里插入图片描述
通过virsh qemu-monitor-command vm --hmp info mtree命令可以打印一个虚机的所有地址空间,内存地址空间如下:
在这里插入图片描述
IO地址空间如下:
在这里插入图片描述

  1. cpu初始化:每个cpu都有自己可访问的地址空间,因此在cpu对象中有一个cpu_ases数组用来保存地址空间,如果是kvm模拟的cpu,只允许拥有一个地址空间;如果是tcg模拟的cpu,允许有多个地址空间。
machine_run_board_init
pc_init1
pc_cpus_init
	......
		x86_cpu_realizefn
		if (tcg_enabled()) {
			cpu->cpu_as_mem = g_new(MemoryRegion, 1);
	        cpu->cpu_as_root = g_new(MemoryRegion, 1);

       	 	/* Outer container... */
        	memory_region_init(cpu->cpu_as_root, OBJECT(cpu), "memory", ~0ull);
        	memory_region_set_enabled(cpu->cpu_as_root, true);
        	
        	cs->num_ases = 2;
        	cpu_address_space_init(cs, 0, "cpu-memory", cs->memory);
        	cpu_address_space_init(cs, 1, "cpu-smm", cpu->cpu_as_root);
        	......

cpu地址空间初始化后拓扑如下,每个cpu的第一个地址空间都指向pc_memory_init中初始化的系统内存地址空间
在这里插入图片描述

AdressSpace Listener初始化

  • 地址空间还有一个关键数据结构listeners,它是一个链表头,链表的每个元素是一个MemoryListener成员。当qemu模拟的内存地址空间发生变化时,需要有机制通知到其它模块,对于kvm的模拟,内存地址空间变化后要通知内核,从而保证内核与用户态内存信息一致。对于tcg的模拟,虽然不通知到内核,但在内存地址信息变化时也有其它事情需要做,MemoryListener结构体就是为此而设计,每个地址空间都维护了这样一个结构体的链表,当内存信息变化时,会触发相关的回调。同时还有一个全局的链表维护所有注册的Listener结构体,这些结构体在链表内通过优先级排序
/** 
 * MemoryListener: callbacks structure for updates to the physical memory map
 *
 * Allows a component to adjust to changes in the guest-visible memory map.
 * Use with memory_listener_register() and memory_listener_unregister().
 */                               
struct MemoryListener {
    void (*begin)(MemoryListener *listener);					/* 1 */
    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,
                      int old, int new);
    void (*log_stop)(MemoryListener *listener, MemoryRegionSection *section,
                     int old, int new);
    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,
                        bool match_data, uint64_t data, EventNotifier *e);
    void (*eventfd_del)(MemoryListener *listener, MemoryRegionSection *section,
                        bool match_data, uint64_t data, EventNotifier *e);
 	unsigned priority;											/* 2 */
 	AddressSpace *address_space;
 	QTAILQ_ENTRY(MemoryListener) link;
 	QTAILQ_ENTRY(MemoryListener) link_as;
	......
};
1. 当内存地址空间有变化时,比如添加一个MR或者删除一个MR,整个地址空间都会变化,某些感兴趣的实体可能想要
让自己被通知到并调用提前注册的钩子函数,这些函数的原型就在这里定义。一个MemoryListener可以只实现其中部分
2. MemoryListener代表的是某个对地址空间感兴趣的实体,这些实体不只一个,需要被管理起来,有两个地方管理这
些实体,一是全局链表memory_listeners,它管理所有注册的Listener,结构体的link成员用作连接到这个链表。二是地
址空间,它管理对自己感兴趣的Listener,地址空间的listeners成员维护这个链表头,结构体的link_as成员用作链接到这个链表。成员的address_space指向这个所属的地址空间。同时所有listener有一个优先级,由priority表示,决定了在链表中的顺序。
  • tcg模拟的cpu地址空间初始化时,会注册这个Listener,它只实现了Listener结构体中的commit方法
cpu_address_space_init
    if (tcg_enabled()) {
        newas->tcg_as_listener.commit = tcg_commit;	/* 注册commit */
        memory_listener_register(&newas->tcg_as_listener, as);
    }
  • kvm模拟的情况下, 会注册以下方法
kvm_init
	    kvm_memory_listener_register(s, &s->memory_listener, &address_space_memory, 0)      /* 3 */            
			......
	    	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);
    		
    	memory_listener_register(&kvm_io_listener, &address_space_io);						/* 4 */
    	
    	static MemoryListener kvm_io_listener = {											/* 5 */
    		.eventfd_add = kvm_io_ioeventfd_add,
    		.eventfd_del = kvm_io_ioeventfd_del,
    		.priority = 10,			
   		}									
3. 系统内存空间的Listener注册
4. 系统IO空间的Listener注册
5. IO空间Listener的接口实现,这里实现ioeventfd的两个回调函数,两个函数在IO空间更新的时候会被调用

Memory Listener的注册分析

void memory_listener_register(MemoryListener *listener, AddressSpace *as)
{   
	......      
   	QTAILQ_FOREACH(other, &memory_listeners, link) {
   		if (listener->priority < other->priority) {
                break;       
     	}
  	}                    
  	QTAILQ_INSERT_BEFORE(other, listener, link);			/* 6 */
	......  
  	QTAILQ_FOREACH(other, &as->listeners, link_as) {
     	if (listener->priority < other->priority) {
         	break;
       	}
   	}
 	QTAILQ_INSERT_BEFORE(other, listener, link_as);			/* 7 */
 	
    listener_add_address_space(listener, as);				/* 8 */
}
6. 将Listener按优先级加入到全局链表memory_listerners中
7. 将Listener按优先级加入到地址空间的link_as成员中
8. 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章