轉載: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_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, |
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); |
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), |
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); |
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) |
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); |
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函數。