转载: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
static void memory_map_init(void) |
system_memory = g_malloc(sizeof(*system_memory)); |
memory_region_init(system_memory, NULL, "system", UINT64_MAX); |
//每个address space 都有个root memory region |
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", |
address_space_init(&address_space_io, system_io, "I/O"); |
memory_listener_register(&core_memory_listener, &address_space_memory); |
address_space_memory其实就是虚机的线性地址空间(设备的mmio分布在这个地址空间),address_space_io是虚机的io地址空间(设备的io port就分布在这个地址空间里)。
不管是DRAM还是设备的资源都要通过memory region添加到address space里。
DRAM的memory region
DRAM的memory_region初始化在pc_memory_init里可以看到:
FWCfgState *pc_memory_init(MachineState *machine, |
MemoryRegion *system_memory, |
ram_addr_t below_4g_mem_size, |
ram_addr_t above_4g_mem_size, |
MemoryRegion *rom_memory, |
MemoryRegion **ram_memory, |
ram = g_malloc(sizeof(*ram)); |
memory_region_allocate_system_memory(ram, NULL, "pc.ram", |
ram_below_4g = g_malloc(sizeof(*ram_below_4g)); |
memory_region_init_alias(ram_below_4g, NULL, "ram-below-4g", ram, |
//ram-below-4g到4G之间的地址主要是留给PCI设备的mmio地址使用 |
memory_region_add_subregion(system_memory, 0, ram_below_4g); |
e820_add_entry(0, below_4g_mem_size, E820_RAM); |
if (above_4g_mem_size > 0) { |
ram_above_4g = g_malloc(sizeof(*ram_above_4g)); |
memory_region_init_alias(ram_above_4g, NULL, "ram-above-4g", ram, |
below_4g_mem_size, above_4g_mem_size); |
memory_region_add_subregion(system_memory, 0x100000000ULL, |
e820_add_entry(0x100000000ULL, above_4g_mem_size, E820_RAM); |
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函数:
static void pci_update_mappings(PCIDevice *d) |
for(i = 0; i < PCI_NUM_REGIONS; i++) { r = &d->io_regions[i]; |
new_addr = pci_bar_address(d, i, r->type, r->size); |
/* This bar isn't changed */ |
/* now do the real mapping */ |
if (r->addr != PCI_BAR_UNMAPPED) { |
trace_pci_update_mappings_del(d, pci_bus_num(d->bus), |
memory_region_del_subregion(r->address_space, r->memory); |
if (r->addr != PCI_BAR_UNMAPPED) { |
trace_pci_update_mappings_add(d, pci_bus_num(d->bus), |
/*r->address_space的赋值在pci_register_bar里完成*/ |
memory_region_add_subregion_overlap(r->address_space, |
void pci_register_bar(PCIDevice *pci_dev, int region_num, |
uint8_t type, MemoryRegion *memory) |
pci_dev->io_regions[region_num].address_space |
= type & PCI_BASE_ADDRESS_SPACE_IO |
? pci_dev->bus->address_space_io |
: pci_dev->bus->address_space_mem; |
pci bus 的address_space_io和address_space_mem又是在哪里定义的?
MemoryRegion *system_io = get_system_io(); |
pci_memory = g_new(MemoryRegion, 1); |
memory_region_init(pci_memory, NULL, "pci", UINT64_MAX); |
pci_bus = i440fx_init(&i440fx_state, &piix3_devfn, &isa_bus, gsi, |
system_memory, system_io, machine->ram_size, |
PCIBus *i440fx_init(PCII440FXState **pi440fx_state, |
ISABus **isa_bus, qemu_irq *pic, |
MemoryRegion *address_space_mem, |
MemoryRegion *address_space_io, |
ram_addr_t below_4g_mem_size, |
ram_addr_t above_4g_mem_size, |
MemoryRegion *pci_address_space, |
MemoryRegion *ram_memory) |
b = pci_bus_new(dev, NULL, pci_address_space, |
address_space_io, 0, TYPE_PCI_BUS); |
/* setup pci memory mapping */ |
pc_pci_as_mapping_init(OBJECT(f), f->system_memory, |
PCIBus *pci_bus_new(DeviceState *parent, const char *name, |
MemoryRegion *address_space_mem, |
MemoryRegion *address_space_io, |
uint8_t devfn_min, const char *typename) |
pci_bus_init(bus, parent, name, address_space_mem, |
address_space_io, devfn_min); |
static void pci_bus_init(PCIBus *bus, DeviceState *parent, |
MemoryRegion *address_space_mem, |
MemoryRegion *address_space_io, |
bus->address_space_mem = address_space_mem; |
bus->address_space_io = address_space_io; |
void pc_pci_as_mapping_init(Object *owner, MemoryRegion *system_memory, |
MemoryRegion *pci_address_space) |
/* Set to lower priority than RAM */ |
memory_region_add_subregion_overlap(system_memory, 0x0, |
从上面的代码片段可以看出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这个函数,从下面的代码可以看到这个函数其实就是锁住了一把全局锁。
void qemu_mutex_lock_iothread(void) |
atomic_inc(&iothread_requesting_mutex); |
if (!tcg_enabled() || !first_cpu || !first_cpu->thread) { |
qemu_mutex_lock(&qemu_global_mutex); |
atomic_dec(&iothread_requesting_mutex); |
if (qemu_mutex_trylock(&qemu_global_mutex)) { |
qemu_cpu_kick_thread(first_cpu); |
qemu_mutex_lock(&qemu_global_mutex); |
atomic_dec(&iothread_requesting_mutex); |
qemu_cond_broadcast(&qemu_io_proceeded_cond); |
这个函数在vcpu线程里使用:
int kvm_cpu_exec(CPUState *) |
qemu_mutex_unlock_iothread(); |
run_ret = kvm_vcpu_ioctl(cpu, KVM_RUN, 0); |
qemu_mutex_lock_iothread(); |
可以看到整个线程除了进入kvm没有加锁,其他时候都会加锁。也就是说vcpu线程里处理io事件的时候是会持有这把锁的。
再看看这把锁在qemu里的应用:
在os_host_main_loop_wait里有这把锁的存在:
static int os_host_main_loop_wait(int64_t timeout) |
qemu_mutex_unlock_iothread(); |
ret = qemu_poll_ns((GPollFD *)gpollfds->data, gpollfds->len, timeout); |
qemu_mutex_lock_iothread(); |
可以看出,除了poll的时候释放了锁,其他时候会占有锁。而os_host_main_loop_wait这个函数是主线程里循环等待事件的函数节点,
last_io = main_loop_wait(nonblocking); |
} while (!main_loop_should_exit()); |
ret = os_host_main_loop_wait(timeout_ns); |
qemu_iohandler_poll(gpollfds, ret); |
qemu_clock_run_all_timers(); |
所以主线程里每次处理io事件的时候也会获取这把锁,这时候就可以解释memory region的更新函数里为什么没有看见锁了,因此实际上用的是这一把全局锁。
memory_region_transaction_begin和memory_region_transaction_commit
在每个更新memory region的函数里都能看到这两个函数对,这两个函数对干什么呢?
void memory_region_transaction_begin(void) |
qemu_flush_coalesced_mmio_buffer(); |
++memory_region_transaction_depth; |
void memory_region_transaction_commit(void) |
--memory_region_transaction_depth; |
if (!memory_region_transaction_depth) { |
函数对的关键其实是memory_region_transaction_depth的计数,也就是说这两个函数对允许递归调用,在一个函数对内部可以再调用多个函数对,只要函数数量是配对的,那么只有等到最外层memory_region_transaction_commit才会开始地址空间的更新。为什么需要这样做呢,这是因为每次更新地址空间的花销是比较大的,如果把多个memory region的更新操作放在一起执行,那么最终只会产生一次地址空间的更新,这是很划算的。
在ich9.c里找到了这样的一个例子:
void ich9_pm_iospace_update(ICH9LPCPMRegs *pm, uint32_t pm_io_base) |
ICH9_DEBUG("to 0x%x\n", pm_io_base); |
assert((pm_io_base & ICH9_PMIO_MASK) == 0); |
pm->pm_io_base = pm_io_base; |
memory_region_transaction_begin(); |
memory_region_set_enabled(&pm->io, pm->pm_io_base != 0); |
memory_region_set_address(&pm->io, pm->pm_io_base); |
memory_region_transaction_commit(); |
memory_listener
地址空间里有个比较重要的数据结构是memory listner,这个数据结构里可以存放一些回调函数,顾名思义,回调函数被调用的时机就是地址空间发生变动的时候。譬如在memory_region_transaction_commit里可以看到对begin和commit的调用,而在address_space_update_topology_pass里可以看到对region_add,region_del,region_nop的调用。
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); |
void (*log_stop)(MemoryListener *listener, MemoryRegionSection *section); |
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); |
void (*coalesced_mmio_add)(MemoryListener *listener, MemoryRegionSection *section, |
hwaddr addr, hwaddr len); |
void (*coalesced_mmio_del)(MemoryListener *listener, MemoryRegionSection *section, |
hwaddr addr, hwaddr len); |
/* Lower = earlier (during add), later (during del) */ |
AddressSpace *address_space_filter; |
QTAILQ_ENTRY(MemoryListener) link; |
比较重要的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函数。
- KVM 介绍(6):Nova 通过 libvirt 管理 QEMU/KVM 虚机 [Nova Libvirt QEMU/KVM Domain]
学习 KVM 的系列文章: (1)介绍和安装 (2)CPU 和 内存虚拟化 (3)I/O QEMU 全虚拟化和准虚拟化(Para-virtulizaiton) (4)I/O PCI/PCIe设备直接分 ...
- KVM(六)Nova 通过 libvirt 管理 QEMU/KVM 虚机
1. Libvirt 在 OpenStack 架构中的位置 在 Nova Compute 节点上运行的 nova-compute 服务调用 Hypervisor API 去管理运行在该 Hypervi ...
- KVM 介绍(8):使用 libvirt 迁移 QEMU/KVM 虚机和 Nova 虚机 [Nova Libvirt QEMU/KVM Live Migration]
学习 KVM 的系列文章: (1)介绍和安装 (2)CPU 和 内存虚拟化 (3)I/O QEMU 全虚拟化和准虚拟化(Para-virtulizaiton) (4)I/O PCI/PCIe设备直接分 ...
- KVM(八)使用 libvirt 迁移 QEMU/KVM 虚机和 Nova 虚机
1. QEMU/KVM 迁移的概念 迁移(migration)包括系统整体的迁移和某个工作负载的迁移.系统整理迁移,是将系统上所有软件包括操作系统完全复制到另一个物理机硬件机器上.虚拟化环境中的迁移, ...
- 远程管理 KVM 虚机 - 每天5分钟玩转 OpenStack(5)
上一节我们通过 virt-manager 在本地主机上创建并管理 KVM 虚机.其实 virt-manager 也可以管理其他宿主机上的虚机.只需要简单的将宿主机添加进来 填入宿主机的相关信息,确定即 ...
- 远程管理 KVM 虚机
上一节我们通过 virt-manager 在本地主机上创建并管理 KVM 虚机.其实 virt-manager 也可以管理其他宿主机上的虚机.只需要简单的将宿主机添加进来 填入宿主机的相关信息,确定即 ...
- 启动第一个 KVM 虚机 - 每天5分钟玩转 OpenStack(4)
本节演示如何使用 virt-manager 启动 KVM 虚机. 首先通过命令 virt-manager 启动图形界面 # virt-manager 点上面的图标创建虚机 给虚机命名为 kvm1,这里 ...
- 启动第一个 KVM 虚机
本节演示如何使用 virt-manager 启动 KVM 虚机. 首先通过命令 virt-manager 启动图形界面 1 # virt-manager 点上面的图标创建虚机 给虚机命名为 kvm1, ...
- O005、远程管理 KVM 虚机
参考https://www.cnblogs.com/CloudMan6/p/5256018.html 上一节我们通过 virt-manager 在本地主机上创建并管理 KVM 虚机,其实 virt ...
随机推荐
- Cesium高度解析
var viewer = new Cesium.Viewer('cesiumContainer', { shadows : true }); //为true时,球体会有高程遮挡效果(在没有地形时候也会 ...
- 写文件的工具类,输出有格式的文件(txt、json/csv)
import java.io.BufferedWriter; import java.io.File; import java.io.FileOutputStream; import java.io. ...
- [ICPC 北京 2017 J题]HihoCoder 1636 Pangu and Stones
#1636 : Pangu and Stones 时间限制:1000ms 单点时限:1000ms 内存限制:256MB 描述 In Chinese mythology, Pangu is the fi ...
- Maven setting.xml 文件配置
全局配置: ${M2_HOME}/conf/settings.xml (配置环境变量 新建 M2_HOME 安装目录到版本名那里(D:\apache-maven-3.0.2) 编辑path 环 ...
- Exception in thread "main" org.apache.hadoop.security.AccessControlException: Permission denied: user=lenovo, access=WRITE, inode="/user/hadoop/spark/people_savemode_test/_temporary/0":hadoop:supergro
保存文件时权限被拒绝 曾经踩过的坑: 保存结果到hdfs上没有写的权限 通过修改权限将文件写入到指定的目录下 * * * $HADOOP_HOME/bin/hdfs dfs -chmod 777 /u ...
- js设计模式(七)---模板方法模式
模板方法模式 模板方法模式是一种只需要继承就可以实现的非常简单的模式. 模板方法模式是由两部分组成,第一部分是抽象父类,第二部分是具体实现的子类, 主要适用在同级的子类具有相同的行为放在父类中实现,而 ...
- Python学习之旅(一)
Python的简介 Python是一种面向对象的.动态的脚本语言,可用来设计网页和开发后台功能.其创始人Guido van Rossum于1989年圣诞节期间创造了这门语言. (图片来自百度) Pyt ...
- Gym 101873K - You Are Fired - [贪心水题]
题目链接:http://codeforces.com/gym/101873/problem/K 题意: 现在给出 $n(1 \le n \le 1e4)$ 个员工,最多可以裁员 $k$ 人,名字为 $ ...
- Mycat了解下
首先说下,因为本身不怎么推荐中间件,所以我对这东西也只是了解,业内mycat用的最好的应该顺风算一个,但是他们是做过二次开发的,咱菜鸡比不了,据说最近出来一个叫cetus的还不错,有空可以关注下 Ⅰ. ...
- Java开发想尝试大数据和数据挖掘,如何规划学习?
大数据火了几年了,但是今年好像进入了全民大数据时代,本着对科学的钻(zhun)研(bei)精(tiao)神(cao),我在17年年初开始自学大数据,后经过系统全面学习,于这个月跳槽到现任公司. 现在已 ...