qemu 对虚机的地址空间管理

转载: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函数。

 

时间: 2024-08-02 14:19:56

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

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

上一节我们通过 virt-manager 在本地主机上创建并管理 KVM 虚机.其实 virt-manager 也可以管理其他宿主机上的虚机.只需要简单的将宿主机添加进来 填入宿主机的相关信息,确定即可. 接下来,我们就可以像管理本地虚机一样去管理远程宿主机上的虚机了. 这里其实有一个要配置的地方. 因为 KVM(准确说是 Libvirt)默认不接受远程管理,需要按下面的内容配置被管理宿主机中的两个文件 /etc/default/libvirt-bin start_libvirtd="yes&q

【上云狂欢节】6元虚机+9元建站+免费套餐,将普惠进行到底!

上云狂欢节来袭!豪华建站大礼包,高稳定性高安全性高性价比,史上最低价,速来围观! 将普惠进行到底!全民云计算,云服务器30/月 共享虚拟主机普惠版,特价6元/年 学生专享 | 云翼计划1折特惠,9.9/月 9款企业免费套餐,最长6个月免费使用 除了特惠大礼包,还为大家准备了建站全套攻略.在建站这条路上,相信大家都有很多疑惑也踩了不少坑.如何选择服务器与虚拟主机?如何提高网站浏览体验?如何实现各种插件的功能?诸如此类,小编就此整理了很多老司机的经验帖,希望可帮大家少走些弯路.  关于建站 网站作为

azure-我有很多虚机,不知道哪些里面跑了sql怎么办?

问题描述 我有很多虚机,不知道哪些里面跑了sql怎么办? 我抓了磁盘的原始镜像 有些都是后来自己做的,没办法 解决方案 查看机器是否有对应的sql服务进程. 解决方案二: 下载一些网络扫描程序,可以自动扫描主机开了什么端口和服务. 解决方案三: sql管理器里面修改端口 解决方案四: 你好 SQL Server 安装的时候默认端口是1433, 所以如果安装时选择的是默认端口的话 使用telnet 命令检查一下1433端口是否打开即可.telnet用法 测试端口号'telnet'不是内部或外部命令

azure里哪个api是取角色列表的?(虚机列表)

问题描述 azure里哪个api是取角色列表的?(虚机列表) 请问一下 !! azure里哪个api是取角色列表的?(虚机列表) 谢啦!! 解决方案 Hi, 你可以通过下列步骤获得虚拟机的列表: 1) 列出所有的云服务,详细请参考文档:https://msdn.microsoft.com/library/azure/ee460781.aspx?f=255&MSPPError=-2147217396 2)遍历每一个云服务,使用Get Deployment rest api,详细请参考文档:http

从虚机产品的升级换代剖析独立IP金冠主机的发展趋势

中介交易 SEO诊断 淘宝客 云主机 技术大厅 在谈虚机产品的升级换代之前我们来先了解下何为传统的虚拟主机,虚拟主机又是怎么样的一种产品.所谓虚拟主机(Virtual Host Virtual Server)是使用特殊的软硬件技术,把一台计算机主机分成一台台'虚拟'的主机,每一台虚拟主机都具有独立的域名和IP地址(或共享的IP地址),具有完整的Internet服务器功能.在同一台硬件.同一个操作系统上,运行着为多个用户打开的不同的服务器程序,互不干扰;而各个用户拥有自己的一部分系统资源(IP地址

个人网站必读:主机虚机篇,你关注的细节

中介交易 SEO诊断 淘宝客 云主机 技术大厅 站长的生活,犹如主妇的生活,柴米油盐酱醋茶 站长是:域名 主机,流量 排名  搜索,统计,赚钱 可见主机是不可或缺的一部分,无论你是使用虚拟主机,还是使用独立服务器,本文集结多年的主机管理经验,写出来一些小点.希望在你看完的5分钟内,对你有用. 对于虚拟主机,如何挑选一个好的虚拟主机商,是很多人关注的首选,一看价格,目前万网 新网 中国频道的的价格虽然比较高,但是品质不错,适合初做网站的朋友,流量在2000ip一天以下,找到代理价格购买,还是很不错

“买域名,送虚机” 靠谱儿吗?

中介交易 http://www.aliyun.com/zixun/aggregation/6858.html">SEO诊断 淘宝客 云主机 技术大厅 乍一看,小编还以为搞反了,买虚机送域名这事儿绝对靠谱,可新网反过来这么一弄,相信大家跟小编一样,心中升起无数个问号.有附加条件吗?数据安全吗?有售后吗?这免费送到底值不值得"收"?活动具体又是什么规则呢?下面就让我们来仔细研究一下. 搭送的虚机,能用吗? 不管什么东西,一提到免费,大家心里就得打鼓--有这好事儿?的确,以往

请问azure上linux虚机的root密码是什么?

问题描述 请问azure上linux虚机的root密码是什么? 请问azure上linux虚机的root密码是什么?有专家知道么?还有一个附加的小问题,PSping 工具哪里可以下载到呢 解决方案 root密码在创建虚拟机的时候由客户指定.如果忘记,即便是微软也没法知道. 解决方案二: psping下载地址 http://www.7edown.com/soft/down/soft_44294.html 解决方案三: 也可以下载这个完整的套装,里面有psping http://www.pc6.co

azure-Azure下已开WS2012虚机一台,如何平滑升级到WS2012R2

问题描述 Azure下已开WS2012虚机一台,如何平滑升级到WS2012R2 之前开了一台虚机WS2012的,一直在用.现在希望升级到WS2012R2,有什么升级办法吗?还是必须要新开一台WS012R2虚机,然后把数据和设置的迁过来呢?那之前那个公网IP怎么办? 解决方案 公网ip别说迁移了,就是关机再开都没了.2012升级2012R2只能重新开一台迁移数据了. 解决方案二: 之前的公网IP 是创建虚拟机的时候 自动分配的吧?入股哦是自动分配的话虚拟机重启后是会变掉的哦. 建议还是重新创建一台