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的是一個統一的線性的地址空間。
附上兩張圖:

 2015-06-14 11:39:11屏幕截圖 

 2015-06-14 11:43:10屏幕截圖

這兩圖截自兩篇系列文章: 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函數。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM