QEMU中KVM的初始化調用路徑

近日剛拜讀了一部分QEMU的源碼,其中用到了蠻多神奇的trick和飛來飛去的回調函數、對象鏈表、註冊方法、使用宏實現的C語言的OO設計,感觸頗多。 這裏先姑且記錄一下QEMU的KVM的內存初始化和CPU初始化的調用路徑和註冊路徑,姑且作一個備忘,也但願給被源碼繞暈的朋友們一條鏈子。bash

vcpu的初始化函數註冊

accel/kvm/kvm_all中的最後一行 type_init(kvm_type_init) kvm_type_init type_register_static(&kvm_accel_type) kvm_accel_type.class_init=kvm_accel_class_init kvm_accel_class_init中設置ac->init_machine=kvm_initkvm_init中執行初始化。ide

其中,type_init(kvm_type_init)只是把kvm_type_init插入給了init_type_list[MODULE_INIT_QOM]鏈表中,並把type_register_static(&kvm_accel)做爲該成員的e->init而已。函數

而這個初始化函數,則是在module_call_init(MODULE_INIT_QOM)中調用,此時會把這個type_info,即kvm_accel_type得相關信息,生成一個TypeImpl得類型,插入到全局類型哈希表type_table中。ui

kvm_init初始化函數的執行

而咱們註冊的ac->init_machine什麼時候被調用呢? 他就是在vl.c的main函數中,在configure_accelerator()函數中被調用,首先,在尋找加速器accel的過程當中,會根據傳入的參數,調用accel_find()函數,在全局的type_table中找到相應的KVM類,而後把它轉換成AccelClass返回到configure_accelerator函數中,緊接着調用accel_init_machine函數,在這個函數中,則會調用acc->init_machine(ms)函數,因爲以前註冊了ac->init_machine=kvm_init,因此此時實際調用的就是kvm_init函數,從而完成KVM的初始化。this

vpu的初始化以及每一個vcpu主線程的執行流程

target/i386/cpu.c中設置了x86_cpu_type_info.class_init=x86_cpu_common_class_init,並調用x86_cpu_register_types將其在初始化時註冊進MODULE_INIT_QOM類型中 x86_cpu_common_class_init中調用device_class_set_parent_realize(dc, x86_cpu_realizefn, &xcc->parent_realize) x86_cpu_realizefn()調用 qemu_init_vpcu(在cpus.c中)調用 qemu_kvm_start_vcpu,在這裏面,調用了qemu_thread_create(cpu->thread, thread_name, qemu_kvm_cpu_thread_fn, cpu, QEMU_THREAD_JOINABLE)增長了qemu_kvm_cpu_thread_fn做爲線程工做函數:spa

static void *qemu_kvm_cpu_thread_fn(void *arg) {
    CPUState *cpu = arg;
    int r;

    rcu_register_thread();

    qemu_mutex_lock_iothread();
    qemu_thread_get_self(cpu->thread);
    cpu->thread_id = qemu_get_thread_id();
    cpu->can_do_io = 1;
    current_cpu = cpu;

    r = kvm_init_vcpu(cpu);
    if (r < 0) {
        error_report("kvm_init_vcpu failed: %s", strerror(-r));
        exit(1);
    }

    kvm_init_cpu_signals(cpu);

    /* signal CPU creation */
    cpu->created = true;
    qemu_cond_signal(&qemu_cpu_cond);

    do {
        if (cpu_can_run(cpu)) {
            r = kvm_cpu_exec(cpu);
            if (r == EXCP_DEBUG) {
                cpu_handle_guest_debug(cpu);
            }
        }
        qemu_wait_io_event(cpu);
    } while (!cpu->unplug || cpu_can_run(cpu));

    qemu_kvm_destroy_vcpu(cpu);
    cpu->created = false;
    qemu_cond_signal(&qemu_cpu_cond);
    qemu_mutex_unlock_iothread();
    rcu_unregister_thread();
    return NULL;
}
複製代碼

在這裏,調用了kvm_init_vcpu(cpu)進行初始化,其實主要也就是用ioctl建立vpu,並給vcpu進行一些屬性的初始化。線程

內存初始化的執行

hw/i386/pc_piix.c中的pc_init1函數中,會首先初始化ram_size 首先對max_ram_below_4g這個元素進行了初始化,默認初始化成了0xe0000000,即3.5G的默認值,而且是初始化了above_4g_mem_size。 在pc_memory_init中,則是調用了memory_region_allocate_system_memory(位於numa.c:510),此處則是調用了allocate_system_memory_nonnuma, 在這其中則調用了memory_region_init_ram_from_file函數,該函數的實現以下圖所示:debug

void memory_region_init_ram_from_file(MemoryRegion *mr,
                                      struct Object *owner,
                                      const char *name,
                                      uint64_t size,
                                      uint64_t align,
                                      uint32_t ram_flags,
                                      const char *path,
                                      Error **errp)
{
    Error *err = NULL;
    memory_region_init(mr, owner, name, size);
    mr->ram = true;
    mr->terminates = true;
    mr->destructor = memory_region_destructor_ram;
    mr->align = align;
    mr->ram_block = qemu_ram_alloc_from_file(size, mr, ram_flags, path, &err);
    mr->dirty_log_mask = tcg_enabled() ? (1 << DIRTY_MEMORY_CODE) : 0;
    if (err) {
        mr->size = int128_zero();
        object_unparent(OBJECT(mr));
        error_propagate(errp, err);
    }
}
複製代碼

顯然,初始化後,使用了qemu_ram_alloc_from_file函數分配了ram_block,那麼再往深處走一下,其實這個函數就是比較簡單的調用了qemu_ram_alloc_from_fd,而該函數的實現則:設計

RAMBlock *qemu_ram_alloc_from_fd(ram_addr_t size, MemoryRegion *mr,
                                 uint32_t ram_flags, int fd,
                                 Error **errp)
{
    RAMBlock *new_block;
    Error *local_err = NULL;
    int64_t file_size;

    /* Just support these ram flags by now. */
    assert((ram_flags & ~(RAM_SHARED | RAM_PMEM)) == 0);

    if (xen_enabled()) {
        error_setg(errp, "-mem-path not supported with Xen");
        return NULL;
    }

    if (kvm_enabled() && !kvm_has_sync_mmu()) {
        error_setg(errp,
                   "host lacks kvm mmu notifiers, -mem-path unsupported");
        return NULL;
    }

    if (phys_mem_alloc != qemu_anon_ram_alloc) {
        /*
         * file_ram_alloc() needs to allocate just like
         * phys_mem_alloc, but we haven't bothered to provide * a hook there. */ error_setg(errp, "-mem-path not supported with this accelerator"); return NULL; } size = HOST_PAGE_ALIGN(size); file_size = get_file_size(fd); if (file_size > 0 && file_size < size) { error_setg(errp, "backing store %s size 0x%" PRIx64 " does not match 'size' option 0x" RAM_ADDR_FMT, mem_path, file_size, size); return NULL; } new_block = g_malloc0(sizeof(*new_block)); new_block->mr = mr; new_block->used_length = size; new_block->max_length = size; new_block->flags = ram_flags; new_block->host = file_ram_alloc(new_block, size, fd, !file_size, errp); if (!new_block->host) { g_free(new_block); return NULL; } ram_block_add(new_block, &local_err, ram_flags & RAM_SHARED); if (local_err) { g_free(new_block); error_propagate(errp, local_err); return NULL; } return new_block; } 複製代碼

儘管該函數的實現頗長,但咱們須要關係的大概也就兩句話:file_ram_allocram_block_add,前一句是實打實地用qemu_ram_mmap影射了一段內存並返回,然後一句則是把這段內存使用RAMBlock的形式保存,並插入全局的ram_list.blocks中。code

這樣系統的RAMBlock就初始化完畢了,那麼如何把分配出來的內存分配給KVM呢?這裏就主要是memory_region_add_subregion作的事情了。 memory_region_add_subregion首先講傳入的subregioncontainer成員設爲mr,而後就調用memory_region_update_container_subregions函數。 memory_region_update_container_subregions則把subregion插入到其container,也就是爸爸的subregions_link鏈表中去,緊接着就調用memory_region_transaction_commit函數,對全部address_spaces_link調用address_space_set_flatview,尋找掛載在AddressSpace上的全部listener,執行address_space_update_topology_pass,兩個FlatView逐條的FlatRange進行對比,之後一個FlatView爲準,若是前面FlatView的FlatRange和後面的不同,則對前面的FlatView的這條FlatRange進行處理。 具體邏輯以下:

static void address_space_update_topology_pass(AddressSpace *as,
                                               const FlatView *old_view,
                                               const FlatView *new_view,
                                               bool adding)
{
    unsigned iold, inew;
    FlatRange *frold, *frnew;

    /* Generate a symmetric difference of the old and new memory maps.
     * Kill ranges in the old map, and instantiate ranges in the new map.
     */
    iold = inew = 0;
    while (iold < old_view->nr || inew < new_view->nr) {
        if (iold < old_view->nr) {
            frold = &old_view->ranges[iold];
        } else {
            frold = NULL;
        }
        if (inew < new_view->nr) {
            frnew = &new_view->ranges[inew];
        } else {
            frnew = NULL;
        }

        if (frold
            && (!frnew
                || int128_lt(frold->addr.start, frnew->addr.start)
                || (int128_eq(frold->addr.start, frnew->addr.start)
                    && !flatrange_equal(frold, frnew)))) {
            /* In old but not in new, or in both but attributes changed. */

            if (!adding) {
                flat_range_coalesced_io_del(frold, as);
                MEMORY_LISTENER_UPDATE_REGION(frold, as, Reverse, region_del);
            }

            ++iold;
        } else if (frold && frnew && flatrange_equal(frold, frnew)) {
            /* In both and unchanged (except logging may have changed) */

            if (adding) {
                MEMORY_LISTENER_UPDATE_REGION(frnew, as, Forward, region_nop);
                if (frnew->dirty_log_mask & ~frold->dirty_log_mask) {
                    MEMORY_LISTENER_UPDATE_REGION(frnew, as, Forward, log_start,
                                                  frold->dirty_log_mask,
                                                  frnew->dirty_log_mask);
                }
                if (frold->dirty_log_mask & ~frnew->dirty_log_mask) {
                    MEMORY_LISTENER_UPDATE_REGION(frnew, as, Reverse, log_stop,
                                                  frold->dirty_log_mask,
                                                  frnew->dirty_log_mask);
                }
            }

            ++iold;
            ++inew;
        } else {
            /* In new */

            if (adding) {
                MEMORY_LISTENER_UPDATE_REGION(frnew, as, Forward, region_add);
                flat_range_coalesced_io_add(frnew, as);
            }

            ++inew;
        }
    }
}
複製代碼

能夠看出,通過一系列判斷以後,調用了MEMORY_LISTENER_UPDATE_REGION宏,該宏則根據第四個參數索引應該執行的回調方法,這裏若是傳入的是region_add,則調用AddressSpace::region_add方法,對於kvm而言,則是調用了kvm_region_add方法。 而該回調則是在初始化時調用kvm_init時,在初始化結束以後調用了kvm_memory_listener_register將kvm的一系列函數註冊到address_space_memory中的。 話說回來,kvm_region_add又作了什麼呢?最核心的其實就是用kvm_vm_ioctl(s, KVM_SET_USER_MEMORY_REGION, &mem)將這段內存扔給KVM啦。

相關文章
相關標籤/搜索