转载: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",
                          65536);
    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,
                           PcGuestInfo *guest_info)
{
    ...
    ram = g_malloc(sizeof(*ram));
    memory_region_allocate_system_memory(ram, NULL, "pc.ram",
                                         machine->ram_size);
    *ram_memory = ram;
    ram_below_4g = g_malloc(sizeof(*ram_below_4g));
    memory_region_init_alias(ram_below_4g, NULL, "ram-below-4g", ram,
                             0, below_4g_mem_size);
    //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,
                                    ram_above_4g);
        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 */
        if (new_addr == r->addr)
            continue;
 
        /* now do the real mapping */
        if (r->addr != PCI_BAR_UNMAPPED) {
            trace_pci_update_mappings_del(d, pci_bus_num(d->bus),
                                          PCI_FUNC(d->devfn),
                                          PCI_SLOT(d->devfn),
                                          i, r->addr, r->size);
            memory_region_del_subregion(r->address_space, r->memory);
        }
        r->addr = new_addr;
        if (r->addr != PCI_BAR_UNMAPPED) {
            trace_pci_update_mappings_add(d, pci_bus_num(d->bus),
                                          PCI_FUNC(d->devfn),
                                          PCI_SLOT(d->devfn),
                                          i, r->addr, r->size);
            /*r->address_space的赋值在pci_register_bar里完成*/
            memory_region_add_subregion_overlap(r->address_space,
                                                r->addr, r->memory, 1);
        }
    }
    ...
}
 
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又是在哪里定义的?

static void pc_init1()
{
    ...
    MemoryRegion *system_io = get_system_io();
    ...
    if (pci_enabled) {
        pci_memory = g_new(MemoryRegion, 1);
        memory_region_init(pci_memory, NULL, "pci", UINT64_MAX);
        rom_memory = pci_memory;
    }
    ...
    if (pci_enabled) {
        pci_bus = i440fx_init(&i440fx_state, &piix3_devfn, &isa_bus, gsi,
                              system_memory, system_io, machine->ram_size,
                              below_4g_mem_size,
                              above_4g_mem_size,
                              pci_memory, ram_memory);
    }
    ...
}
 
PCIBus *i440fx_init(PCII440FXState **pi440fx_state,
                    int *piix3_devfn,
                    ISABus **isa_bus, qemu_irq *pic,
                    MemoryRegion *address_space_mem,
                    MemoryRegion *address_space_io,
                    ram_addr_t ram_size,
                    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,
                           f->pci_address_space);
    ...
}
 
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,
                         const char *name,
                         MemoryRegion *address_space_mem,
                         MemoryRegion *address_space_io,
                         uint8_t devfn_min)
{
    ...
    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_address_space, -1);
}

从上面的代码片段可以看出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);
    } else {
        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();
    ...
    一些io 处理的事情,可能会更新地址空间
}

可以看到整个线程除了进入kvm没有加锁,其他时候都会加锁。也就是说vcpu线程里处理io事件的时候是会持有这把锁的。

再看看这把锁在qemu里的应用:

在os_host_main_loop_wait里有这把锁的存在:

static int os_host_main_loop_wait(int64_t timeout)
{
    ...
     if (timeout) {
        spin_counter = 0;
        qemu_mutex_unlock_iothread();
    } else {
        spin_counter++;
    }
 
    ret = qemu_poll_ns((GPollFD *)gpollfds->data, gpollfds->len, timeout);
 
    if (timeout) {
        qemu_mutex_lock_iothread();
    }
    ...
}

可以看出,除了poll的时候释放了锁,其他时候会占有锁。而os_host_main_loop_wait这个函数是主线程里循环等待事件的函数节点,

main_loop ()
{
     do {
        ...
        last_io = main_loop_wait(nonblocking);
        ...
    } while (!main_loop_should_exit());
}
 
main_loop_wait()
{
    ...
    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的调用。

struct MemoryListener {
    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) */
    unsigned priority;
    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函数。

qemu 对虚机的地址空间管理的更多相关文章

  1. 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设备直接分 ...

  2. KVM(六)Nova 通过 libvirt 管理 QEMU/KVM 虚机

    1. Libvirt 在 OpenStack 架构中的位置 在 Nova Compute 节点上运行的 nova-compute 服务调用 Hypervisor API 去管理运行在该 Hypervi ...

  3. 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设备直接分 ...

  4. KVM(八)使用 libvirt 迁移 QEMU/KVM 虚机和 Nova 虚机

    1. QEMU/KVM 迁移的概念 迁移(migration)包括系统整体的迁移和某个工作负载的迁移.系统整理迁移,是将系统上所有软件包括操作系统完全复制到另一个物理机硬件机器上.虚拟化环境中的迁移, ...

  5. 远程管理 KVM 虚机 - 每天5分钟玩转 OpenStack(5)

    上一节我们通过 virt-manager 在本地主机上创建并管理 KVM 虚机.其实 virt-manager 也可以管理其他宿主机上的虚机.只需要简单的将宿主机添加进来 填入宿主机的相关信息,确定即 ...

  6. 远程管理 KVM 虚机

    上一节我们通过 virt-manager 在本地主机上创建并管理 KVM 虚机.其实 virt-manager 也可以管理其他宿主机上的虚机.只需要简单的将宿主机添加进来 填入宿主机的相关信息,确定即 ...

  7. 启动第一个 KVM 虚机 - 每天5分钟玩转 OpenStack(4)

    本节演示如何使用 virt-manager 启动 KVM 虚机. 首先通过命令 virt-manager 启动图形界面 # virt-manager 点上面的图标创建虚机 给虚机命名为 kvm1,这里 ...

  8. 启动第一个 KVM 虚机

    本节演示如何使用 virt-manager 启动 KVM 虚机. 首先通过命令 virt-manager 启动图形界面 1 # virt-manager 点上面的图标创建虚机 给虚机命名为 kvm1, ...

  9. O005、远程管理 KVM 虚机

    参考https://www.cnblogs.com/CloudMan6/p/5256018.html   上一节我们通过 virt-manager 在本地主机上创建并管理 KVM 虚机,其实 virt ...

随机推荐

  1. [转帖]Loading Data into HAWQ

    Loading Data into HAWQ Leave a reply Loading data into the database is required to start using it bu ...

  2. store.state

    https://blog.csdn.net/qq_38658567/article/details/82847758

  3. 公开的免费WebService接口分享

    天气预报Web服务,数据来源于中国气象局 Endpoint  Disco  WSDL IP地址来源搜索 WEB 服务(是目前最完整的IP地址数据) Endpoint  Disco  WSDL 随机英文 ...

  4. Kafka 2.1.0压缩算法性能测试

    Apache Kafka 2.1.0正式支持ZStandard —— ZStandard是Facebook开源的压缩算法,旨在提供超高的压缩比(compression ratio),具体细节参见htt ...

  5. Javascript 变量、函数的声明

    javascript变量 全局变量和局部变量    按照变量的作用域来区分,和大多数编程语言类似,javascript变量也分为全局变量和局部变量.全局变量的作用域是整个js文件,而局部变量的作用域是 ...

  6. 【腾讯云的1001种玩法】 Laravel 整合万向优图图片管理能力,打造高效图片处理服务

    版权声明:本文由白宦成原创文章,转载请注明出处: 文章原文链接:https://www.qcloud.com/community/article/574549001488234358 来源:腾云阁 h ...

  7. QT上位机

    程序是用QT写的,通过COM口进行数据的读取. 源码地址:https://github.com/kunkunlin/QT-SWJ

  8. java课程课后作业04之动手动脑

    一.多层的异常捕获-1 先贴出代码: public class CatchWho { public static void main(String[] args) { try { try { thro ...

  9. Oracle数据库实例的启动及关闭

    要启动和关闭数据库,必须要以具有Oracle 管理员权限的用户登陆,通常也就是以具有SYSDBA权限的用户登陆.一般我们常用SYS用户以SYSDBA连接来启动和关闭数据库.下面介绍Oracle数据库几 ...

  10. python文档-基本API命令翻译及使用方法!

    1.使用tkinter.Tk() 生成主窗口(window=tkinter.Tk()): window.title('标题名')         修改框体的名字,也可在创建时使用className参数 ...