转载:http://huchh.com/2015/06/22/qemu-%E5%AF%B9%E8%99%9A%E6%9C%BA%E7%9A%84%E7%BA%BF%E6%80%A7%E5%9C%B0%E5%9D%80%E7%A9%BA%E9%97%B4%E7%AE%A1%E7%90%86/
cpu有两个地址空间:io 地址空间和内存地址空间。io地址空间是给设备用的,平时说设备占有哪些端口,指的就是io地址空间里的地址。内存地址空间相对比较复杂,这个地址空间被DRAM,设备和Flash rom等使用,最终呈现给cpu的是一个线性地址空间。
附:平时编程说的物理地址指的是内存地址空间的地址,不要误认为这个地址一定是物理内存,譬如3G以上的物理地址很可能对应的是某个PCI设备。
什么是线性地址空间,鉴于不同的地方对这个名词有不同的解释,先在文章的开头申明一下,本文说的线性地址空间指的是从cpu的角度看到的一段连续的可以访问的地址空间,其中包括了真正的物理内存RAM,PCI地址空间,还有一些设备的ROM占据的地址空间,这些地址空间互相重叠最后呈现给cpu的是一个统一的线性的地址空间。
附上两张图:
这两图截自两篇系列文章:
System Address Map Initialization in x86/x64 Architecture Part 1: PCI-Based Systems
System Address Map Initialization in x86/x64 Architecture Part 2: PCI Express-Based Systems
这两篇文章详细解释了pci和pcie设备在系统地址里的映射,对于理解线性地址空间和pci设备有很好的帮助,强烈建议仔细阅读。
qemu维护地址空间
qemu负责模拟虚机的外设,因此虚机的线性地址空间主要由qemu进行管理,也就是确定线性地址空间中哪段地址属于哪个设备或者DRAM或者其他的什么。通过qemu的monitor可以查看运行中的虚机的地址空间,如果用libvirt启动的话,可以这样查看:
virsh qemu-monitor-command –hmpinfo mtree
注: qemu源码里有一篇文档介绍了qemu的虚机内存管理 Docs/memory.txt
address space 和 memory region
在qemu里有几个重要的数据结构来维护虚机的线性地址空间: AddressSpace, MemoryRegion, FlatView, MemoryListener等。
在memory_map_init 中可以看到对两个最重要的address space的初始化: address_space_memory 和 address_space_io
address_space_memory其实就是虚机的线性地址空间(设备的mmio分布在这个地址空间),address_space_io是虚机的io地址空间(设备的io port就分布在这个地址空间里)。
不管是DRAM还是设备的资源都要通过memory region添加到address space里。
DRAM的memory region
DRAM的memory_region初始化在pc_memory_init里可以看到:
legacy devices的地址一般是固定的,在设备初始化的时候就可以通过memory_region_add_subregion加入到地址空间的确切位置。
pci设备的memory region
PCI设备的资源在地址空间中的偏移是动态不确定的,一般PCI设备需要的memory region对应的就是bar,一开始初始化memory region,然后用pci_register_bar注册bar。那么到底在什么地方将bar对应的memory region添加到address space里呢?
看一下pci_update_mappings函数:
从上面的代码片段可以看出pci bus的address_space_io就是address_space_io的root memory region,而address_space_mem是新建的一个属于pci设备的总的memory region,在pc_pci_as_mapping_init里将pci_address_space以-1的优先级加入到system_memory里,将pci设备的地址空间和线性地址空间进行统一。
而每个pci设备在pci_update_mappings里将他们的bar作为sub memory region加入到其附属的pci总线的address_space_io或者address_space_mem里,其实就是添加到统一的io地址空间或者内存地址空间(线性地址空间)。
回顾一下pci_update_mappings,它是在pci_default_write_config里被调用的,而大部分pci设备写config space的时候都会调用到pci_default_write_config,也就是说虚机的fireware或者OS确定了bar的基地址后,更新config space,然后bar就会正式添加到io地址空间或者线性地址空间,在此之前,qemu里的pci设备只是定义了bar,相当于准备好了硬件,但是还不能在地址空间里看到pci设备的bar。
有关地址空间分布的api内部有一些细节挺绕的,当初也花了一些时间来理解,这里记录一些认为比较关键的函数点,权充日后按图索骥之用,并不会详细地展开每个函数。
锁的存在
memory_region_add_subregion这样的函数会更新memory region内部的数据结构,可以从代码上看明显没有锁的存在,难道这个函数确保不会被并发访问吗? 当然不是,在主线程和vcpu线程都可能会更新设备的memory region,因此这类函数一定存在并发使用的可能。那么同步措施到底在哪里做的呢?
关键在qemu_mutex_lock_iothread这个函数,从下面的代码可以看到这个函数其实就是锁住了一把全局锁。
所以主线程里每次处理io事件的时候也会获取这把锁,这时候就可以解释memory region的更新函数里为什么没有看见锁了,因此实际上用的是这一把全局锁。
memory_region_transaction_begin和memory_region_transaction_commit
在每个更新memory region的函数里都能看到这两个函数对,这两个函数对干什么呢?
函数对的关键其实是memory_region_transaction_depth的计数,也就是说这两个函数对允许递归调用,在一个函数对内部可以再调用多个函数对,只要函数数量是配对的,那么只有等到最外层memory_region_transaction_commit才会开始地址空间的更新。为什么需要这样做呢,这是因为每次更新地址空间的花销是比较大的,如果把多个memory region的更新操作放在一起执行,那么最终只会产生一次地址空间的更新,这是很划算的。
在ich9.c里找到了这样的一个例子:
比较重要的memory_listner有kvm_memory_listener,kvm_io_listener,dispatch_listener。kvm相关的两个listner比较明显,用意就是在qemu的地址空间发生变动的时候通过回调函数通知到kvm。
dispatch_listener的初始化在address_space_init_dispatch,它在每个地址空间里都存在,用意是在地址空间发生变动的时候,通过内部的数据结构记录这种变化,以此得知地址空间里每一段地址应该属于哪个memory region,这样当虚机有io操作需要在qemu里完成的时候,也就是vcpu线程从kvm返回需要处理io或者mmio的时候都需要通过对应的地址空间的dispatch_listner找到io操作的目标。具体可以看address_space_rw里的address_space_translate函数。