1. 程式人生 > >Qemu-KVM虛擬機器初始化及建立過程原始碼簡要分析(二)

Qemu-KVM虛擬機器初始化及建立過程原始碼簡要分析(二)

    前面我們講了KVM核心層建立及初始化虛擬機器的一些工作過程,現在講一下Qemu層的流程以及與KVM核心層的配合過程。    

    Qemu層是從vl.c中的main()函式開始的,這裡通過在程式碼中新增一些註釋的方式來進行講解,中間省略了很多不重要或者我也沒有搞清楚的程式碼。   囧

int main(int argc, char **argv, char **envp)
{
    ......
    atexit(qemu_run_exit_notifiers);//註冊了Qemu的退出函式
    ......
    module_call_init(MODULE_INIT_QOM);//初始化Qemu的各個模組,具體見下面註釋
    
/*
    module_call_init實際上是設計了一個函式連結串列ModuleTypeList,引數作為一個Type,相關的函式註冊到這個函式連結串列上,
    然後內部通過呼叫e->init()函式完成所有Type相關的裝置的初始化。關於e->init()的具體內容後面再細說。
    Type總計有這些型別:
    typedef enum {
    MODULE_INIT_BLOCK,
    MODULE_INIT_MACHINE,
    MODULE_INIT_QAPI,
    MODULE_INIT_QOM,
    MODULE_INIT_MAX
    } module_init_type;
*/ 

    qemu_add_opts(&qemu_drive_opts);//將各種函式指標(也就是操作)集合新增到連結串列中 
    qemu_add_opts(&qemu_chardev_opts); 
    qemu_add_opts(&qemu_device_opts); 
    qemu_add_opts(&qemu_netdev_opts); 
    qemu_add_opts(&qemu_net_opts); 
    qemu_add_opts(&qemu_rtc_opts); 
    qemu_add_opts(&qemu_global_opts); 
    qemu_add_opts(&qemu_mon_opts); 
    qemu_add_opts(&qemu_trace_opts); 
    qemu_add_opts(&qemu_option_rom_opts); 
    qemu_add_opts(&qemu_machine_opts); 
    qemu_add_opts(&qemu_boot_opts); 
    qemu_add_opts(&qemu_sandbox_opts); 
    qemu_add_opts(&qemu_add_fd_opts); 
    qemu_add_opts(&qemu_object_opts); 
    qemu_add_opts(&qemu_tpmdev_opts); 
    qemu_add_opts(&qemu_realtime_opts);
     ...... 
    init_clocks();//時鐘初始化相關 
    rtc_clock = host_clock; 
    ...... 
    module_call_init(MODULE_INIT_MACHINE); 
    machine = find_default_machine();
    ...... 
    ...... 
    cpudef_init();//初始化CPU def相關
     ...... 
    if (log_mask) {//日誌相關的設定,KVM對外的日誌在這裡配置 
        int mask; 
        if (log_file) { 
            qemu_set_log_filename(log_file); 
        }
        mask = qemu_str_to_log_mask(log_mask); 
        if (!mask) { 
            qemu_print_log_usage(stdout); 
            exit(1); 
        }
        qemu_set_log(mask);
     } 
    ...... 
    configure_accelerator();//進行虛擬機器模擬器的配置,這裡重點注意,它內部呼叫了accel_list[i].init()函式 

/*
    for (i = 0; i < ARRAY_SIZE(accel_list); i++) {
        if (strcmp(accel_list[i].opt_name, buf) == 0) {
            if (!accel_list[i].available()) {
                printf("%s not supported for this target\n",accel_list[i].name);
                continue;
            }
            *(accel_list[i].allowed) = true;
            ret = accel_list[i].init();
            if (ret < 0) {
                init_failed = true;
                fprintf(stderr, "failed to initialize %s: %s\n",accel_list[i].name,strerror(-ret));
               *(accel_list[i].allowed) = false;        
            } else {            
                accel_initialised = true;       
            }       
            break;   
         }
     } 
    //accel_list定義如下,實際上在kvm平臺,我們就關注kvm_init即可。    
    static struct {
        const char *opt_name;  
        const char *name;  
        int (*available)(void);
        int (*init)(void);
        bool *allowed; 
    } accel_list[] = {
       { "tcg", "tcg", tcg_available, tcg_init, &tcg_allowed },
       { "xen", "Xen", xen_available, xen_init, &xen_allowed },
       { "kvm", "KVM", kvm_available, kvm_init, &kvm_allowed },
       { "qtest", "QTest", qtest_available, qtest_init, &qtest_allowed }, 
    };    
    //kvm_init函式內首先開啟用於使用者層以及核心層互動的字元裝置檔案/dev/kvm,然後通過kvm_ioctl()與核心進行互動,比如     KVM_GET_API_VERSION,KVM_CREATE_VM等命令,其中KVM_CREATE_VM命令建立虛擬機器並獲得虛擬機器控制代碼,後續kvm_arch_init()、 kvm_irqchip_create()等函式就可以通過kvm_vm_ioctl系統呼叫進行更進一步的一些配置。這些系統呼叫實際上是傳遞到核心層,由核心來完成相應的操作並返回到使用者層,核心層的相關函式很多就是前一篇文章註冊過的函式指標。
*/
    ......     
    ......     
    cpu_exec_init_all();//記錄CPU執行前的一些初始化工作   
    ......    
    ......     
    return 0;
}
    前面提到過e->init()函式,並沒有展開細說,現在來說一下這個函式。實際上e->init()函式是在machine_init(pc_machine_init)函式註冊時註冊到ModuleTypeList的ModuleEntry上的,module_call_init()針對X86架構時呼叫amchine_init(),隨即呼叫pc_machine_init()函式,程式碼如下:
static void pc_machine_init(void)
{
    qemu_register_machine(&pc_i440fx_machine_v1_5);
    qemu_register_machine(&pc_i440fx_machine_v1_4);
    qemu_register_machine(&pc_machine_v1_3);
    qemu_register_machine(&pc_machine_v1_2);
    qemu_register_machine(&pc_machine_v1_1);
    qemu_register_machine(&pc_machine_v1_0);
    qemu_register_machine(&pc_machine_v0_15);
    qemu_register_machine(&pc_machine_v0_14);
    qemu_register_machine(&pc_machine_v0_13);
    qemu_register_machine(&pc_machine_v0_12);
    qemu_register_machine(&pc_machine_v0_11);
    qemu_register_machine(&pc_machine_v0_10);
    qemu_register_machine(&isapc_machine);
#ifdef CONFIG_XEN
    qemu_register_machine(&xenfv_machine);
#endif
}

machine_init(pc_machine_init);

    注意這裡註冊的第一個為pc_i440fx_machine_v1_5,這個結構體定義為下:
static QEMUMachine pc_i440fx_machine_v1_5 = {
    .name = "pc-i440fx-1.5",
    .alias = "pc",
    .desc = "Standard PC (i440FX + PIIX, 1996)",
    .init = pc_init_pci,
    .hot_add_cpu = pc_hot_add_cpu,
    .max_cpus = 255,
    .is_default = 1,
    DEFAULT_MACHINE_OPTIONS,
};

    .init=pc_init_pci, pc_init_pci()即為初始化時候呼叫的函式,一路跟下去,其實最終調到pc_init1()這個函式。再看pc_init1()這個函式,這裡面進行了記憶體(pc_memory_init)、cpu(pc_cpus_init)、中斷等等多種的初始化,這裡不細說,重點看cpu的初始化。
void pc_cpus_init(const char *cpu_model, DeviceState *icc_bridge)
{
    int i;
    X86CPU *cpu = NULL;
    Error *error = NULL;

    /* init CPUs */
    if (cpu_model == NULL) {
#ifdef TARGET_X86_64
        cpu_model = "qemu64";
#else
        cpu_model = "qemu32";
#endif
    }
    current_cpu_model = cpu_model;

    for (i = 0; i < smp_cpus; i++) {
        cpu = pc_new_cpu(cpu_model, x86_cpu_apic_id_from_index(i),//對每一個CPU進行初始化及建立
                         icc_bridge, &error);
        if (error) {
            fprintf(stderr, "%s\n", error_get_pretty(error));
            error_free(error);
            exit(1);
        }
    }

    /* map APIC MMIO area if CPU has APIC */
    if (cpu && cpu->env.apic_state) {
        /* XXX: what if the base changes? */
        sysbus_mmio_map_overlap(SYS_BUS_DEVICE(icc_bridge), 0,
                                APIC_DEFAULT_ADDRESS, 0x1000);
    }
}

    該函式內呼叫pc_new_cpu()函式對每一個CPU進行初始化,其後依次呼叫關係為:


    下面來看kvm_init_vcpu()函式
int kvm_init_vcpu(CPUState *cpu)
{
    KVMState *s = kvm_state;
    long mmap_size;
    int ret;

    DPRINTF("kvm_init_vcpu\n");

    ret = kvm_vm_ioctl(s, KVM_CREATE_VCPU, (void *)kvm_arch_vcpu_id(cpu));//通過ioctl呼叫向核心發起建立CPU請求,核心完成相關工作。
    if (ret < 0) {
        DPRINTF("kvm_create_vcpu failed\n");
        goto err;
    }

    cpu->kvm_fd = ret;
    cpu->kvm_state = s;
    cpu->kvm_vcpu_dirty = true;

    mmap_size = kvm_ioctl(s, KVM_GET_VCPU_MMAP_SIZE, 0);//通過ioctl呼叫獲取核心與使用者層共享的記憶體大小,這部分記憶體以記憶體對映的方式進行共享。
    if (mmap_size < 0) {
        ret = mmap_size;
        DPRINTF("KVM_GET_VCPU_MMAP_SIZE failed\n");
        goto err;
    }

    cpu->kvm_run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED,//獲取記憶體大小後,使用者層進行共享記憶體對映。
                        cpu->kvm_fd, 0);
    if (cpu->kvm_run == MAP_FAILED) {
        ret = -errno;
        DPRINTF("mmap'ing vcpu state failed\n");
        goto err;
    }

    if (s->coalesced_mmio && !s->coalesced_mmio_ring) {  //mmio相關的一部分共享記憶體設定
        s->coalesced_mmio_ring =
            (void *)cpu->kvm_run + s->coalesced_mmio * PAGE_SIZE;
    }

    ret = kvm_arch_init_vcpu(cpu);//相關初始化
    if (ret == 0) {
        qemu_register_reset(kvm_reset_vcpu, cpu);
        kvm_arch_reset_vcpu(cpu);
    }
err:
    return ret;
}

    回到上面一些列呼叫中的qemu_kvm_cpu_thread_fn()函式中,在呼叫了kvm_init_vcpu()函式完成cpu的初始化之後,又呼叫kvm_cpu_exec()函式執行cpu,也就是運行了整個虛擬機器。

    我們來看kvm_cpu_exec()這個函式

int kvm_cpu_exec(CPUArchState *env)
{
    .......
    do{	
        run_ret = kvm_vcpu_ioctl(cpu, KVM_RUN, 0);

       .......
        trace_kvm_run_exit(cpu->cpu_index, run->exit_reason);
        switch (run->exit_reason) {
        case KVM_EXIT_IO:
            ......
            break;
        case KVM_EXIT_MMIO:
            ......
            break;
        case KVM_EXIT_IRQ_WINDOW_OPEN:
            ......
            break;
        case KVM_EXIT_SHUTDOWN:
            ......
            break;
        case KVM_EXIT_UNKNOWN:
            ......
            break;
        case KVM_EXIT_INTERNAL_ERROR:
           ......
            break;
        default:
            ......
            break;
        }
    } while (ret == 0);

    .......
    return ret;
}
    首先一個kvm_vcpu_ioctl系統呼叫,向核心請求執行虛擬機器,然後核心執行虛擬機器,kvm_cpu_exec()內是有一個while迴圈,只要是沒有錯誤就會不斷執行不會終止,後面的switch語句實際上是接收核心傳來的退出原因,因為I/O等是需要Qemu即使用者層來完成的,這樣虛擬機器執行時核心層遇到I/O等就需要退出到Qemu層並記錄退出原因,Qemu根據退出原因執行相關操作,完成後再次執行ioctl操作轉到核心層繼續執行虛擬機器。關於異常退出的具體流程下篇文章再詳細講解。

    關於記憶體初始化相關工作,在之前提到過的kvm初始化主函式kvm_init()函式裡,依次呼叫:

    memory_listener_register(&kvm_memory_listener, &address_space_memory);

    listener_add_address_space(listener, as);

    listener->region_add(listener, &section);

    .region_add = kvm_region_add,

    kvm_set_phys_mem(section, true);

    err = kvm_set_user_memory_region(s, mem);

    return kvm_vm_ioctl(s, KVM_SET_USER_MEMORY_REGION, &mem);

    這些函式依次呼叫,基本完成記憶體初始化過程,這裡最後的ioctl呼叫是設定影子頁表資訊以及設定頁面訪問許可權等。

    最終,在核心與使用者層的配合下完成整個虛擬機器的建立和初始化工作,並執行虛擬機器。