關於Linux虛擬化技術KVM的科普 科普三(From OenHan)


http://oenhan.com/archives,包括《KVM源代碼分析1:基本工作原理》、《KVM源代碼分析2:虛擬機的創建與運行》、《KVM源代碼分析3:CPU虛擬化》、《KVM源代碼分析4:內存虛擬化》、《KVM源代碼分析5:IO虛擬化之PIO》,可以有個基本認識,以及CPU、內存、IO虛擬化(里面的一些圖居然沒有了,可以在轉載地址找到)。

這一系列文章按照基礎原理、使用以及CPU/Memory/IO虛擬化分析來進行的。


 

KVM源代碼分析1:基本工作原理

Linux Kernel在市場上的需求:虛擬化、存儲、網絡和驅動

作者給出的進行虛擬化開發准備工作:1.操作系統基礎知識;2.《深入Linux內核架構》、《深入理解Linux內核》;3.Intel的《系統虛擬化-原理與實現》。

關於Guest OS、QEMU、KVM、Host OS不同角色及其職責:

kvm_arch_map_oenhan

Guest OS是不經修改可以直接運行的一套系統,保證具體運行場景中的程序正常執行。而KVM的代碼則部署在Host上,Kernel對應的是KVM Driver,KVM Driver負責模擬虛擬機的CPU運行、內存管理、設備管理等等;Userspace對應的是QEMU,QEMU則模擬虛擬機的IO設備接口以及用戶態控制接口,QEMU通過KVM等fd進行ioctl控制KVM驅動的運行。

Guest有自己的用戶模式和內核模式,Guest是在Host中作為一個用戶態進程存在的,這個進程就是QEMU,QEMU本省就是一個虛擬化程序,它被KVM改造后,作為KVM的前端存在,用來進行創建進程或者IO交互等;而KCM Driver則是Linux內核模式,它提供KVM fs給QEMU調用,用來進行CPU虛擬化、內存虛擬化等。QEMU通過KVM提供的fd接口,通過ioctl系統調用創建和運行虛擬機。KVM Driver使得整個Linux成為一個虛擬機監控器,負責接收QEMU模擬效率很低的命令。

 

KVM的執行流程:

上圖是一個執行過程圖,首先啟動一個虛擬化管理軟件qemu,開始啟動一個虛擬機,通過ioctl等系統調用向內核中申請指定的資源,搭建好虛擬環境,啟動虛擬機內的OS,執行 VMLAUCH 指令,即進入了guest代碼執行過程。如果 Guest OS 發生外部中斷或者影子頁表缺頁之類的事件,暫停 Guest OS 的執行,退出QEMU即guest VM-exit,進行一些必要的處理,然后重新進入客戶模式,執行guest代碼;這個時候如果是io請求,則提交給用戶態下的qemu處理,qemu處理后再次通過IOCTL反饋給KVM驅動。

 

CPU虛擬化:
Guest和Host之間的切換

X86虛擬化技術Intel VT-x,提供了兩種工作環境,VMCS實現兩種環境之間的切換。VM Entry是虛擬機進入guest模式,VM Exit使虛擬機退出guest模式。

VMM調度guest執行時,qemu通過ioctl系統調用進入內核模式,在KVM Driver中獲得當前物理CPU的引用。之后將guest狀態從VMCS中讀出,並裝入物理CPU中。執行 VMLAUCH 指令使得物理處理器進入非根操作環境,運行guest OS代碼。

當 guest OS 執行一些特權指令或者外部事件時, 比如I/O訪問,對控制寄存器的操作,MSR的讀寫等, 都會導致物理CPU發生 VMExit, 停止運行 Guest OS,將 Guest OS保存到VMCS中, Host 狀態裝入物理處理器中, 處理器進入根操作環境,KVM取得控制權,通過讀取 VMCS 中 VM_EXIT_REASON 字段得到引起 VM Exit 的原因。 從而調用kvm_exit_handler 處理函數。 如果由於 I/O 獲得信號到達,則退出到userspace模式的 Qemu 處理。處理完畢后,重新進入guest模式運行虛擬 CPU。

Memory虛擬化:

OS對於物理內存主要有兩點認識:1.物理地址從0開始;2.內存地址是連續的。VMM接管了所有內存,但guest OS的對內存的使用就存在這兩點沖突了,除此之外,一個guest對內存的操作很有可能影響到另外一個guest乃至host的運行。VMM的內存虛擬化就要解決這些問題。

在OS代碼中,應用也是占用所有的邏輯地址,同時不影響其他應用的關鍵點在於有線性地址這個中間層;解決方法則是添加了一個中間層:guest物理地址空間;guest看到是從0開始的guest物理地址空間(類比從0開始的線性地址),而且是連續的,雖然有些地址沒有映射;同時guest物理地址映射到不同的host邏輯地址,如此保證了VM之間的安全性要求。

這樣MEM虛擬化就是GVA->GPA->HPA的尋址過程,傳統軟件方法有影子頁表,硬件虛擬化提供了EPT支持。
可能GVA->GPA->HVA->HPA更全面一點。
GVA: Guest Virtual Address
GPA: Guest Physical Address
HVA: Host Virtual Address
HPA: Host Physical Address

KVM源代碼分析2:虛擬機的創建與運行

在進行本章閱讀之前首先了解一下KVM、QEMU-KVM、libvirt之間的關系。

參看文檔:《KVM-Qemu-Libvirt三者之間的關系》和《KVM,QEMU,libvirt入門學習筆記》。

KVM是linux內核的模塊,它需要CPU的支持,采用硬件輔助虛擬化技術Intel-VT,AMD-V,內存的相關如Intel的EPT和AMD的RVI技術,Guest OS的CPU指令不用再經過Qemu轉譯,直接運行,大大提高了速度,KVM通過/dev/kvm暴露接口,用戶態程序可以通過ioctl函數來訪問這個接口。KVM內核模塊本身只能提供CPU和內存的虛擬化,所以它必須結合QEMU才能構成一個完成的虛擬化技術,這就是下面要說的qemu-kvm。

QEMU-KVM是基於Qemu將KVM整合進來,通過ioctl調用/dev/kvm接口,將有關CPU指令的部分交由內核模塊來做。kvm負責cpu虛擬化+內存虛擬化,實現了cpu和內存的虛擬化,但kvm不能模擬其他設備。qemu模擬IO設備(網卡,磁盤等),kvm加上qemu之后就能實現真正意義上服務器虛擬化。因為用到了上面兩個東西,所以稱之為qemu-kvm。Qemu模擬其他的硬件,如Network, Disk,同樣會影響這些設備的性能,於是又產生了pass through半虛擬化設備virtio_blk, virtio_net,提高設備性能。

wKiom1WdDc2CEwy6AAGPf4VzQao172.jpg

libvirt是目前使用最為廣泛的對KVM虛擬機進行管理的工具和API。Libvirtd是一個daemon進程,可以被本地的virsh調用,也可以被遠程的virsh調用,Libvirtd調用qemu-kvm操作虛擬機。

wKioL1WdD72RRy8mAAIuDm6sVAY591.jpg

從上面分析可以知道:KVM是內核的一個模塊,提供CPU和Memory的虛擬化;QEMU-KVM是基於QEMU針對KVM修改后的工具,用於提供完整的KVM虛擬化環境;libvirt是用來管理虛擬化的通用庫,支持但不限於KVM。

 

關於QEMU KVM里面用到的幾個文件句柄:

KVMState.fd通過qemu_open("/dev/kvm", O_RDWR)獲取。

CPUState.kvm_fd通過 kvm_get_vcpu(s, kvm_arch_vcpu_id(cpu))獲取。

 

module_call_init

從vl.c的main開始,atexit注冊了qemu退出處理函數。module_call_init則開始初始化qemu的各個模塊,有:

typedef enum {
    MODULE_INIT_BLOCK,
    MODULE_INIT_OPTS,
    MODULE_INIT_QAPI,
    MODULE_INIT_QOM,
    MODULE_INIT_TRACE,
    MODULE_INIT_MAX
} module_init_type;

最開始初始化MODULE_INIT_TRACE,然后依次執行。module_call_init實際上是執行不同type函數鏈表ModuleTypeList上的ModuleEntry。

void module_call_init(module_init_type type)
{
    ModuleTypeList *l;
    ModuleEntry *e;

    l = find_type(type);

    QTAILQ_FOREACH(e, l, node) {
        e->init();
    }
}

實際上就是執行e->init,那么e->init是什么時候被賦值的呢?是通過register_module_init注冊到對應ModuleTypeList的。

調用關系如:block_init/opts_init/qaqi_init/type_init/trace_init->module_init->register_module_init。

下面可以看到初始化函數和module_init_type的一一對應關系。

#define block_init(function) module_init(function, MODULE_INIT_BLOCK)
#define opts_init(function) module_init(function, MODULE_INIT_OPTS)
#define qapi_init(function) module_init(function, MODULE_INIT_QAPI)
#define type_init(function) module_init(function, MODULE_INIT_QOM)
#define trace_init(function) module_init(function, MODULE_INIT_TRACE)

#define module_init(function, type)                                         \
static void __attribute__((constructor)) do_qemu_init_ ## function(void)    \
{                                                                           \
    register_module_init(function, type);                                   \
}

小知識:

修飾符__attribute__((constructor))導致module_init會在main()之前就被執行。所以所有的block_init/opts_init/qaqi_init/type_init/trace_init在main()之前已經被執行。同樣__attribute__((destructor))會在main()結束之后調用。

由於module_register_init已經先於main()執行,所有module_call_init可以遍歷各種類型的ModuleTypeList。

 

pc_init1

pc_init1是一個核心函數,那么他是怎么被調用的呢?

#define DEFINE_I440FX_MACHINE(suffix, name, compatfn, optionfn) \
    static void pc_init_##suffix(MachineState *machine) \
    { \
        void (*compat)(MachineState *m) = (compatfn); \
        if (compat) { \
            compat(machine); \
        } \
        pc_init1(machine, TYPE_I440FX_PCI_HOST_BRIDGE, \
                 TYPE_I440FX_PCI_DEVICE); \  所有此類型的Machine都會調用pc_init1。
        fprintf(stderr, "File: %s %s line=%d\n", __FILE__, __func__, __LINE__); \
    } \
    DEFINE_PC_MACHINE(suffix, name, pc_init_##suffix, optionfn)

那么pc_init_##suffix又是怎么被調用的呢?從下面代碼可以看出type_init會將pc_machine_init_##suffix注冊。最終mc->init會指向pc_machine_##suffix##_class_init,進而調用pc_init1。

#define DEFINE_PC_MACHINE(suffix, namestr, initfn, optsfn) \
    static void pc_machine_##suffix##_class_init(ObjectClass *oc, void *data) \
    { \
        MachineClass *mc = MACHINE_CLASS(oc); \
        optsfn(mc); \
    fprintf(stderr, "File: %s %s line=%d\n", __FILE__, __func__, __LINE__); \
        mc->init = initfn; \
    } \
    static const TypeInfo pc_machine_type_##suffix = { \
        .name       = namestr TYPE_MACHINE_SUFFIX, \
        .parent     = TYPE_PC_MACHINE, \
        .class_init = pc_machine_##suffix##_class_init, \
    }; \
    static void pc_machine_init_##suffix(void) \
    { \
        type_register(&pc_machine_type_##suffix); \
    fprintf(stderr, "File: %s %s line=%d\n", __FILE__, __func__, __LINE__); \
    } \
    type_init(pc_machine_init_##suffix)

由於type_init的特殊屬性(在main()之前已經被執行),所以在main中執行machine_class->init的時候函數已經就緒。

pc_init1分析如下,主要進行CPU、Memory、VGA、NIC、PCI等的初始化

static void pc_init1(MachineState *machine,
                     const char *host_type, const char *pci_type)
{

    pc_cpus_init(pcms);  初始化CPU

    if (kvm_enabled() && pcmc->kvmclock_enabled) {
        kvmclock_create();
    }

    if (pcmc->pci_enabled) {
        pci_memory = g_new(MemoryRegion, 1);
        memory_region_init(pci_memory, NULL, "pci", UINT64_MAX);
        rom_memory = pci_memory;
    } else {
        pci_memory = NULL;
        rom_memory = system_memory;
    }

    pc_guest_info_init(pcms);

    if (pcmc->smbios_defaults) {
        MachineClass *mc = MACHINE_GET_CLASS(machine);
        /* These values are guest ABI, do not change */
        smbios_set_defaults("QEMU", "Standard PC (i440FX + PIIX, 1996)",
                            mc->name, pcmc->smbios_legacy_mode,
                            pcmc->smbios_uuid_encoded,
                            SMBIOS_ENTRY_POINT_21);
    }

    /* allocate ram and load rom/bios */
    if (!xen_enabled()) {
        pc_memory_init(pcms, system_memory,
                       rom_memory, &ram_memory);
    } else if (machine->kernel_filename != NULL) {
        /* For xen HVM direct kernel boot, load linux here */
        xen_load_linux(pcms);
    }

    gsi_state = g_malloc0(sizeof(*gsi_state));
    if (kvm_ioapic_in_kernel()) {
        kvm_pc_setup_irq_routing(pcmc->pci_enabled);
        pcms->gsi = qemu_allocate_irqs(kvm_pc_gsi_handler, gsi_state,
                                       GSI_NUM_PINS);
    } else {
        pcms->gsi = qemu_allocate_irqs(gsi_handler, gsi_state, GSI_NUM_PINS);
    }

    if (pcmc->pci_enabled) {
        pci_bus =i440fx_init(host_type,
                              pci_type,
                              &i440fx_state, &piix3_devfn, &isa_bus, pcms->gsi,
                              system_memory, system_io, machine->ram_size,
                              pcms->below_4g_mem_size,
                              pcms->above_4g_mem_size,
                              pci_memory, ram_memory);
        pcms->bus = pci_bus;
    } else {
        pci_bus = NULL;
        i440fx_state = NULL;
        isa_bus = isa_bus_new(NULL, get_system_memory(), system_io,
                              &error_abort);
        no_hpet = 1;
    }
    isa_bus_irqs(isa_bus, pcms->gsi);

    pc_register_ferr_irq(pcms->gsi[13]);

    pc_vga_init(isa_bus, pcmc->pci_enabled ? pci_bus : NULL);

    assert(pcms->vmport != ON_OFF_AUTO__MAX);
    if (pcms->vmport == ON_OFF_AUTO_AUTO) {
        pcms->vmport = xen_enabled() ? ON_OFF_AUTO_OFF : ON_OFF_AUTO_ON;
    }

    /* init basic PC hardware */
    pc_basic_device_init(isa_bus, pcms->gsi, &rtc_state, true,
                         (pcms->vmport != ON_OFF_AUTO_ON), pcms->pit, 0x4);

    pc_nic_init(isa_bus, pci_bus);

    ide_drive_get(hd, ARRAY_SIZE(hd));

    pc_cmos_init(pcms, idebus[0], idebus[1], rtc_state);

    if (pcmc->pci_enabled && machine_usb(machine)) {
        pci_create_simple(pci_bus, piix3_devfn + 2, "piix3-usb-uhci");
    }

    if (pcmc->pci_enabled) {
        pc_pci_device_init(pci_bus);
    }

    if (pcms->acpi_nvdimm_state.is_enabled) {
        nvdimm_init_acpi_state(&pcms->acpi_nvdimm_state, system_io,
                               pcms->fw_cfg, OBJECT(pcms));
    }
}

pc_cpus_init

main
    ->machine_class->init
        ->pc_init1
            ->pc_cpus_init(i386/pc.c)
                ->cpu_class_by_name
                ->object_class_get_name
                ->pc_new_cpu
                    ->object_new
                        ->object_new_with_type
                            ->object_initialize_with_type
                                ->object_init_with_type
                                    ->ti->instance_init(x86_cpu_initfn)
                                    ->x86_cpu_realizefn
                                        ->qemu_init_vcpu
                                            ->qemu_kvm_start_vcpu
                                                ->qemu_kvm_cpu_thread_fn
                                                    ->kvm_init_vcpu
                                                        ->kvm_arch_init_vcpu

pc_cpus_init中循環對smp_cpus個數執行pc_new_cpu。pc_new_cpu進入到x86_cpu_initfn

qemu_init_vcpu用於創建CPU,根據條件創建KVM、HAX、TCG。DUMMY四種類型。

這里重點看看KVM類型的VCPU創建,qemu_kvm_start_vcpu:

static void qemu_kvm_start_vcpu(CPUState *cpu)
{
    char thread_name[VCPU_THREAD_NAME_SIZE];

    cpu->thread = g_malloc0(sizeof(QemuThread));
    cpu->halt_cond = g_malloc0(sizeof(QemuCond));
    qemu_cond_init(cpu->halt_cond);
    snprintf(thread_name, VCPU_THREAD_NAME_SIZE, "CPU %d/KVM",
             cpu->cpu_index);
    qemu_thread_create(cpu->thread, thread_name, qemu_kvm_cpu_thread_fn,
                       cpu, QEMU_THREAD_JOINABLE);  創建CPU的線程
    while (!cpu->created) {  如果cpu->reated為否,既沒有創建成功,則一直while(1)阻塞。說明多核vcpu創建是順序的。
        qemu_cond_wait(&qemu_cpu_cond, &qemu_global_mutex);
    }
}

qemu_kvm_cpu_thread_fn作為創建CPU的線程:

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);  初始化vcpu
    if (r < 0) {
        fprintf(stderr, "kvm_init_vcpu failed: %s\n", strerror(-r));
        exit(1);
    }

    qemu_kvm_init_cpu_signals(cpu);

    /* signal CPU creation */
    cpu->created = true;  和之前的while(!cpu->created)
    qemu_cond_signal(&qemu_cpu_cond);

    do {
        if (cpu_can_run(cpu)) {  進入CPU執行狀態
            r = kvm_cpu_exec(cpu);
            if (r == EXCP_DEBUG) {
                cpu_handle_guest_debug(cpu);
            }
        }
        qemu_kvm_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();
    return NULL;
}

kvm_init_vcpu通過kvm_vm_ioctl,KVM_CREATE_VCPU創建VCPU,用KVM_GET_VCPU_MMAP_SIZE獲取cpu->kvm_run對應的內存映射。kvm_arch_init_vcpu則填充對應的kvm_arch內容。

qemu_kvm_init_cpu_signals則是將中斷組合掩碼傳遞給kvm_set_signal_mask,最終給內核KVM_SET_SIGNAL_MASK。kvm_cpu_exec此時還在阻塞過程中,先掛起來,看內存的初始化。

 

在qemu_init_vcpu執行完成后,下面就是cpu_reset。

pc_memory_init

pc_memory_init是內存初始化函數,memory_region_init負責填充MemoryRegion結構體,重點在qemu_ram_alloc。

pc_memory_init
    ->memory_region_init_ram
        ->memory_region_init  (填充MemoryRegion結構體)
        ->qemu_ram_alloc  (返回RAMBlock結構體給mr->ram_block)
            ->qemu_ram_alloc_internal
                ->ram_block_add
                    ->find_ram_offset
                        ->phy_mem_alloc
(qemu_anon_ram_alloc)
                            ->qemu_ram_mmap
                                ->mmap
    ->vmstate_register_ram_global
        ->vmstate_register_ram
            ->qemu_ram_set_idstr
    ->memory_region_add_subregion_overlap

之前在qemu_kvm_cpu_thread_fn中的真正執行VCPU的kvm_cpu_exec有一個判斷條件cpu_can_run。

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

從cpu_can_run可知,必須cpu->stop和cpu->stopped || !runstate_is_running()都為false才具備往下執行的條件。

這個條件在main->vm_start中觸發,vm_start->resume_all_vcpus->cpu_resume:

void cpu_resume(CPUState *cpu)
{
    cpu->stop = false;
    cpu->stopped = false;
    qemu_cpu_kick(cpu);
}

 

mmap/madvise

參考資料:《Linux內存管理之mmap詳解

mmap

mmap將一個文件或者其它對象映射進內存。文件被映射到多個頁上,如果文件的大小不是所有頁的大小之和,最后一個頁不被使用的空間將會清零。munmap執行相反的操作,刪除特定地址區域的對象映射。

當使用mmap映射文件到進程后,就可以直接操作這段虛擬地址進行文件的讀寫等操作,不必再調用read,write等系統調用.但需注意,直接對該段內存寫時不會寫入超過當前文件大小的內容.

采用共享內存通信的一個顯而易見的好處是效率高,因為進程可以直接讀寫內存,而不需要任何數據的拷貝。對於像管道和消息隊列等通信方式,則需要在內核和用戶空間進行四次的數據拷貝,而共享內存則只拷貝兩次數據:一次從輸入文件到共享內存區,另一次從共享內存區到輸出文件。實際上,進程之間在共享內存時,並不總是讀寫少量數據后就解除映射,有新的通信時,再重新建立共享內存區域。而是保持共享區域,直到通信完畢為止,這樣,數據內容一直保存在共享內存中,並沒有寫回文件。共享內存中的內容往往是在解除映射時才寫回文件的。因此,采用共享內存的通信方式效率是非常高的。

基於文件的映射,在mmap和munmap執行過程的任何時刻,被映射文件的st_atime可能被更新。如果st_atime字段在前述的情況下沒有得到更新,首次對映射區的第一個頁索引時會更新該字段的值。用PROT_WRITE 和 MAP_SHARED標志建立起來的文件映射,其st_ctime 和 st_mtime在對映射區寫入之后,但在msync()通過MS_SYNC 和 MS_ASYNC兩個標志調用之前會被更新。

madvice

函數建議內核,在從 addr 指定的地址開始,長度等於 len 參數值的范圍內,該區域的用戶虛擬內存應遵循特定的使用模式。內核使用這些信息優化與指定范圍關聯的資源的處理和維護過程。如果使用 madvise() 函數的程序明確了解其內存訪問模式,則使用此函數可以提高系統性能。

#include <sys/types.h>
#include <sys/mman.h>
int madvise(caddr_t addr, size_t len, int advice);

madvise() 函數提供了以下標志,這些標志影響 lgroup 之間線程內存的分配方式:

MADV_NORMAN
此標志將指定范圍的內核預期訪問模式重置為缺省設置。
MADV_HUGEPAGE
在指定范圍內開啟透明大頁面(THP),THP是一個提取層,可自動創建、管理和使用超大頁面的大多數方面。超大頁面是2MB和1GB大小的內存塊。
MADV_NOHUGEPAGE
確保當前范圍的內存不會被當成大頁面分配。這兩種模式之后再內核配置了CONFIG_TRANSPARENT_HUGEPAGE之后才能生效。
MADV_DONTFORK
使指定范圍內的頁在fork之后不被子進程使用。
MADV_DOFORK
MADV_DONTFORK的反操作。
MADV_MERGEABLE
在制定范圍內使能KSM。KSM即Kernel Samepage Merging,如果頁面內容都是相同的,他們可以被合並,從而釋放內存。
MADV_UNMERGEABLE
去KSM功能,即使內容相同也保留各自,不合並。這兩種模式只有在內核配置了 CONFIG_KSM之后,才能生效。
 
mmap和madvise在使用中的區別

mmap的作用是將硬盤文件的內容映射到內存中,采用閉鏈哈希建立的索引文件非常適合利用mmap的方式進行內存映射,利用mmap返回的地址指針就是索引文件在內存中的首地址,這樣我們就可以放心大膽的訪問這些內容了。

      使用過mmap映射文件的同學會發現一個問題,search程序訪問對應的內存映射時,處理query的時間會有latecny會陡升,究其原因是因為mmap只是建立了一個邏輯地址,linux的內存分配測試都是采用延遲分配的形式,也就是只有你真正去訪問時采用分配物理內存頁,並與邏輯地址建立映射,這也就是我們常說的缺頁中斷。  

      缺頁中斷分為兩類,一種是內存缺頁中斷,這種的代表是malloc,利用malloc分配的內存只有在程序訪問到得時候,內存才會分配;另外就是硬盤缺頁中斷,這種中斷的代表就是mmap,利用mmap映射后的只是邏輯地址,當我們的程序訪問時,內核會將硬盤中的文件內容讀進物理內存頁中,這里我們就會明白為什么mmap之后,訪問內存中的數據延時會陡增。

      出現問題解決問題,上述情況出現的原因本質上是mmap映射文件之后,實際並沒有加載到內存中,要解決這個文件,需要我們進行索引的預加載,這里就會引出本文講到的另一函數madvise,這個函數會傳入一個地址指針,已經一個區間長度,madvise會向內核提供一個針對於於地址區間的I/O的建議,內核可能會采納這個建議,會做一些預讀的操作。例如MADV_SEQUENTIAL這個就表明順序預讀。

     如果感覺這樣還不給力,可以采用read操作,從mmap文件的首地址開始到最終位置,順序的讀取一遍,這樣可以完全保證mmap后的數據全部load到內存中。

kvm_init

type_init(kvm_type_init)->kvm_accel_type->kvm_accel_class_init->kvm_init依次完成注冊,然后在configure_accelerator的時候調用這些函數。

main->configure_accelerator->accel_init_machine->kvm_init是到kvm_init的調用路徑。

qemu_create_kvm_vm

static int kvm_init(MachineState *ms)
{

    s = KVM_STATE(ms->accelerator);  填充KVMState結構體。

    s->vmfd = -1;
    s->fd = qemu_open("/dev/kvm", O_RDWR);  打開/dev/kvm,獲取句柄。
    if (s->fd == -1) {
        fprintf(stderr, "Could not access KVM kernel module: %m\n");
        ret = -errno;
        goto err;
    }

    ret = kvm_ioctl(s, KVM_GET_API_VERSION, 0);  獲取KVM版本信息

    s->nr_slots = kvm_check_extension(s, KVM_CAP_NR_MEMSLOTS);  獲取最大內存插槽數

    /* If unspecified, use the default value */
    if (!s->nr_slots) {
        s->nr_slots = 32;
    }

    /* check the vcpu limits */
    soft_vcpus_limit = kvm_recommended_vcpus(s);
    hard_vcpus_limit = kvm_max_vcpus(s);

    do {
        ret = kvm_ioctl(s, KVM_CREATE_VM, type);  創建KVM虛擬機,獲取虛擬機句柄。
    } while (ret == -EINTR);

    if (ret < 0) {
        fprintf(stderr, "ioctl(KVM_CREATE_VM) failed: %d %s\n", -ret,
                strerror(-ret));


        goto err;
    }

    s->vmfd = ret;
    missing_cap = kvm_check_extension_list(s, kvm_required_capabilites); 一系列通過ioctl讀取s->fd參數。

    ret = kvm_arch_init(ms, s);  初始化KVMState
    if (ret < 0) {
        goto err;
    }

    if (machine_kernel_irqchip_allowed(ms)) { 
    kvm_irqchip_create(ms, s);  創建kcm中斷管理內容,通過kvm_vm_ioctl的KVM_CAP_IRQCHIP實現。
    }

    kvm_state = s;

    if (kvm_eventfds_allowed) {
        s->memory_listener.listener.eventfd_add = kvm_mem_ioeventfd_add;
        s->memory_listener.listener.eventfd_del = kvm_mem_ioeventfd_del;
    }
    s->memory_listener.listener.coalesced_mmio_add = kvm_coalesce_mmio_region;
    s->memory_listener.listener.coalesced_mmio_del = kvm_uncoalesce_mmio_region;

    kvm_memory_listener_register(s, &s->memory_listener,  注冊內存管理函數
                                 &address_space_memory, 0);
    memory_listener_register(&kvm_io_listener,
                             &address_space_io);

    s->many_ioeventfds = kvm_check_many_ioeventfds();

    cpu_interrupt_handler = kvm_handle_interrupt;

    return 0;
}

到kvm_init_vcpu用於創建CPU,並執行,調用路徑:

x86_cpu_realizefn
    ->qemu_init_vcpu
        ->qemu_kvm_start_vcpu
            ->qemu_kvm_cpu_thread_fn
                ->kvm_init_vcpu
                    ->kvm_get_vcpu
                        ->kvm_vm_ioctl(KVM_CREATE_VCPU)
                ->kvm_cpu_exec
                    ->kvm_vcpu_ioctl(KVM_RUN)

代碼如下:

int kvm_init_vcpu(CPUState *cpu)
{
    KVMState *s = kvm_state;
    long mmap_size;
    int ret;

    ret = kvm_get_vcpu(s, kvm_arch_vcpu_id(cpu));  創建VCPU句柄
    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);  獲取VCPU mmap大小,並且創建mmap映射給cpu->kvm_run。

    cpu->kvm_run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED,
                        cpu->kvm_fd, 0);

    ret = kvm_arch_init_vcpu(cpu);  架構相關的CPUState結構體初始化
err:
    return ret;
}

kvm_cpu_exec如下:

int kvm_cpu_exec(CPUState *cpu)
{
    struct kvm_run *run = cpu->kvm_run;
    int ret, run_ret;

    DPRINTF("kvm_cpu_exec()\n");

    if (kvm_arch_process_async_events(cpu)) {
        cpu->exit_request = 0;
        return EXCP_HLT;
    }

    qemu_mutex_unlock_iothread();

    do {
        MemTxAttrs attrs;

        if (cpu->kvm_vcpu_dirty) {
            kvm_arch_put_registers(cpu, KVM_PUT_RUNTIME_STATE);
            cpu->kvm_vcpu_dirty = false;
        }

        kvm_arch_pre_run(cpu, run);  RUN前准備
        if (cpu->exit_request) {
            DPRINTF("interrupt exit requested\n");
            /*
             * KVM requires us to reenter the kernel after IO exits to complete
             * instruction emulation. This self-signal will ensure that we
             * leave ASAP again.
             */
            qemu_cpu_kick_self();
        }

        run_ret = kvm_vcpu_ioctl(cpu, KVM_RUN, 0);  RUN

        attrs = kvm_arch_post_run(cpu, run);  RUN后收尾工作

        trace_kvm_run_exit(cpu->cpu_index, run->exit_reason);
        switch (run->exit_reason) {
…  各種退出原因處理 

        }
    } while (ret == 0);

    qemu_mutex_lock_iothread();

    if (ret < 0) {
        cpu_dump_state(cpu, stderr, fprintf, CPU_DUMP_CODE);
        vm_stop(RUN_STATE_INTERNAL_ERROR);
    }

    cpu->exit_request = 0;
    return ret;
}

 

KVM內存初始化

注冊Memory Listener過程:

kvm_init
    ->kvm_memory_listener_register
       ->.region_add = kvm_region_add
          .region_del = kvm_region_del

增加Memory Region:

kvm_region_add/kvm_region_del
    ->kvm_set_phys_mem
        ->kvm_set_user_memory_region
            ->kvm_vm_ioctl(KVM_SET_USER_MEMORY_REGION)

調用路徑:

memory_listener_register
    ->listener_add_address_space
        ->listener->log_start
        ->listener->region_add
        ->listener->commit

 

select_machine

QEMU的及其類型是通過select_machine獲得的,也可以通過-machine參數傳入。

查看支持的machine列表,可以通過qemu-system-x86_64 -machine help得到。

如果沒有指定,則使用默認的machine_class。

static MachineClass *select_machine(void)
{
    MachineClass *machine_class = find_default_machine();  初始化為默認的machine_class
    const char *optarg;
    QemuOpts *opts;
    Location loc;

    loc_push_none(&loc);

    opts = qemu_get_machine_opts();
    qemu_opts_loc_restore(opts);

    optarg = qemu_opt_get(opts, "type");
    if (optarg) {
        machine_class = machine_parse(optarg);  通過參數獲取的machine_class
    }

    loc_pop(&loc);
    return machine_class;
}

 

KVM源代碼分析3:CPU虛擬化

Intel VT-x介紹以及root和non-root切換

X86上KVM依賴的處理器虛擬化技術主要有Intel的VT-x和AMD的AMD-v。之所以CPU支持硬件虛擬化的原因是因為軟件虛擬化的效率太低。

處理器虛擬化的本質是分時共享,主要體現在狀態恢復和資源隔離,實際上每個VM對於VMM看就是一個task么,之前Intel處理器在虛擬化上沒有提供默認的硬件支持,傳統 x86 處理器有4個特權級,Linux使用了0,3級別,0即內核,3即用戶態,(更多參考CPU的運行環、特權級與保護)而在虛擬化架構上,虛擬機監控器的運行級別需要內核態特權級,而CPU特權級被傳統OS占用,所以Intel設計了VT-x,提出了VMX模式,即VMX root operation 和 VMX non-root operation虛擬機監控器運行在VMX root operation虛擬機運行在VMX non-root operation。每個模式下都有相對應的0~3特權級。

Host運行在VMX root operation模式下,包括0內核和3用戶態。

Guest運行在VMX non-root operation模式下,也包括0內核和3用戶態。

那么為什么需要root和non-root兩種模式呢?歸根結底還是Guest和Host之間對資源權限不一致,Guest的部分敏感指令需要被屏蔽。

在傳統x86的系統中,CPU有不同的特權級,是為了划分不同的權限指令,某些指令只能由系統軟件操作,稱為特權指令,這些指令只能在最高特權級上才能正確執行,反之則會觸發異常,處理器會陷入到最高特權級,由系統軟件處理。還有一種需要操作特權資源(如訪問中斷寄存器)的指令,稱為敏感指令。OS運行在特權級上,屏蔽掉用戶態直接執行的特權指令,達到控制所有的硬件資源目的;而在虛擬化環境中,VMM控制所有所有硬件資源,VM中的OS只能占用一部分資源,OS執行的很多特權指令是不能真正對硬件生效的,所以原特權級下有了root模式,OS指令不需要修改就可以正常執行在特權級上,但這個特權級的所有敏感指令都會傳遞到root模式處理,這樣達到了VMM的目的。

 

下面圖將KVM應用分成三部分:VMX中non-root模式,即Guest OS;VMX中root模式下0特權級,即Host Kernel,對應Kernel KVM驅動;VMX中root模式下3特權級,即Host Userspace,對應QEMU軟件

root和non-root模式的切換稱為VM Exit和VM Entry。

VM Exit:non-root模式下,敏感指令引發的陷入,CPU從non-root切換到root模式。指令執行從Guest切換到Host。

VM Entry:root模式下,調用VMLAUCH/VMRESUME命令發起,從root切換到non-root模式。

VMCS寄存器

VMCS保存虛擬機相關的CPU狀態,每個vcpu都有一個VMCS(內存的),每個物理CPU都有VMCS對應的寄存器(物理的)。當CPU發生VM Entry時,CPU則從vcpu指定的內存中讀取VMCS加載到物理CPU上執行;當VM Exit時,CPU則將當前的CPU狀態保存到vcpu狀態指定的內存中,以備下次VMRESUME。

VNLAUCH指Vm的第一次VM Entry,VMRESUME則是VMLAUCH之后后續的Vm Entry。

VM Entry/VM Exit

VM-Entry是從根模式切換到非根模式,即Host切換到Guest上,這個狀態由VMM發起,發起之前先保存VMM中的關鍵寄存器內容到VMCS中,然后進入到VM-Entry,VM-Entry附帶參數主要有3個:1.guest是否處於64bit模式,2.MSR VM-Entry控制,3.注入事件。1應該只在VMLAUCH有意義,3更多是在VMRESUME,而VMM發起VM-Entry更多是因為3,2主要用來每次更新MSR。

VM-Exit是CPU從非根模式切換到根模式,從Guest(VM)切換到Host(VMM)的操作,VM-Exit觸發的原因就很多了,執行敏感指令,發生中斷,模擬特權資源等。

運行在非根模式下的敏感指令一般分為3個方面:

1.行為沒有變化的,也就是說該指令能夠正確執行。

2.行為有變化的,直接產生VM-Exit。

3.行為有變化的,但是是否產生VM-Exit受到VM-Execution控制域控制。

主要說一下"受到VM-Execution控制域控制"的敏感指令,這個就是針對性的硬件優化了,一般是1.產生VM-Exit;2.不產生VM-Exit,同時調用優化函數完成功能。典型的有“RDTSC指令”。除了大部分是優化性能的,還有一小部分是直接VM-Exit執行指令結果是異常的,或者說在虛擬化場景下是不適用的,典型的就是TSC offset了。

VM-Exit發生時退出的相關信息,如退出原因、觸發中斷等,這些內容保存在VM-Exit信息域中。

KVM_CREATE_VM、KVM_CREATE_VCPU、KVM_RUN

vmx_init
    ->kvm_init
        ->kvm_dev(/dev/kvm)
            ->kvm_dev_ioctl(所有基於/dev/kvm的ioctl處理)
                ->KVM_CREATE_VM(kvm_dev_ioctl_create_vm,創建VM)
                    ->kvm_create_vm
                    ->kvm_vm_fops(kvm-vm)
==============================KVM子系統VM分界==============================
                        ->kvm_vm_ioctl(VM的ioctl處理)
                            ->KVM_CREATE_VCPU
(kvm_vm_ioctl_create_vcpu,創建vcpu)
                                ->kvm_arch_vcpu_create
                                ->create_vcpu_fd
                                    ->kvm_vcpu_fops(kvm-vcpu操作函數集)
==============================VM和vcpu分界================================
                                        ->kvm_vcpu_ioctl
                                            ->
KVM_RUN(運行vcpu)
                                                ->
kvm_arch_vcpu_ioctl_run
                                                    ->
vcpu_run

kvm_create_vm創建struct kvm結構體,對應一個VM虛擬機;kvm_arch_vcpu_create創建struct kvm_vcpu結構體,對應VM虛擬機的一個虛擬CPU。

下面就來看看struct kvm:

struct kvm {
    spinlock_t mmu_lock;
    struct mutex slots_lock;
    struct mm_struct *mm; /* userspace tied to this vm */
    struct kvm_memslots *memslots[KVM_ADDRESS_SPACE_NUM];  QEMU模擬的內存條模型
    struct srcu_struct srcu;
    struct srcu_struct irq_srcu;
    struct kvm_vcpu *vcpus[KVM_MAX_VCPUS];  模擬CPU

    int created_vcpus;
    int last_boosted_vcpu;
    struct list_head vm_list;  Host上VM管理鏈表
    struct mutex lock;
    struct kvm_io_bus *buses[KVM_NR_BUSES];

    struct kvm_vm_stat stat;
    struct kvm_arch arch;  Host上的arch參數
    atomic_t users_count;

}

kvm_create_vm中,主要有兩個函數kvm_arch_init_vm初始化kvm結構體的arch成員,hardware_enable_all針對每個CPU執行hardware_enable_nolock。

在hardware_enable_nolock中先把cpus_hardware_enabled置位,進入到kvm_arch_hardware_enable中,有hardware_enable和TSC初始化規則,主要看hardware_enable,crash_enable_local_vmclear清理位圖,判斷MSR_IA32_FEATURE_CONTROL寄存器是否滿足虛擬環境,不滿足則將條件寫入到寄存器內,CR4將X86_CR4_VMXE置位,另外還有kvm_cpu_vmxon打開VMX操作模式,外層包了vmm_exclusive的判斷,它是kvm_intel.ko的外置參數,默認唯一,可以讓用戶強制不使用VMM硬件支持。

kvm_vm_ioctl_create_vcpu調用kvm_arch_vcpu_create(輸入kvm和)、kvm_arch_vcpu_setup、create_vcpu_fd。

kvm_arch_vcpu_create輸入kvm和待創建的vcpu的id,調用kvm_x86_ops來創建vcpu。kvm_x86_ops指向vmx_x86_ops,所以是調用vmx_create_vcpu來創建的。vmx是X86硬件虛擬化層,從代碼看,qemu用戶態是一層,kernel 中KVM通用代碼是一層,類似kvm_x86_ops是一層,針對各個不同硬件架構,而vcpu_vmx則是具體架構的虛擬化方案一層。首先是kvm_vcpu_init初始化,主要是填充結構體,可以注意的是vcpu->run分派了一頁內存,下面有kvm_arch_vcpu_init負責填充x86 CPU結構體kvm_vcpu_arch。
kvm_arch_vcpu_init初始化了x86在虛擬化底層的實現函數,首先是pv和emulate_ctxt,這些不支持VMX下的模擬虛擬化,尤其是vcpu->arch.emulate_ctxt.ops = &emulate_ops,emulate_ops初始化虛擬化模擬的對象函數。這里面還涉及到MMU、IRQ、PMU等一系列初始化動作。

kvm_arch_vcpu_setup為空略過,create_vcpu_fd為proc創建控制fd,讓qemu使用。

這一大塊細節有待研究,現摘錄於此。

 

 

KVM源代碼分析4:內存虛擬化

從vl.c的main到pc_init1函數,在這里區分了above_4g_mem_size和below_4g_mem_size及高低端內存,然后開始初始化內存,即pc_memory_init。pc_memory_init調用memory_region_init_ram分配內存,進而調用qemu_ram_alloc至qemu_ram_alloc_internal。

if (machine->ram_size >= lowmem) {
    pcms->above_4g_mem_size = machine->ram_size - lowmem;
    pcms->below_4g_mem_size = lowmem;
} else {
    pcms->above_4g_mem_size = 0;
    pcms->below_4g_mem_size = machine->ram_size;
}

QEMU對內存條的模擬是通過RAMBlock和ram_list管理的,RAMBlock就是每次申請的內存池,ran_list則是RAMBlock的鏈表。

struct RAMBlock {
    struct rcu_head rcu;
    struct MemoryRegion *mr;
    uint8_t *host;
    ram_addr_t offset;
    ram_addr_t used_length;
    ram_addr_t max_length;
    void (*resized)(const char*, uint64_t length, void *host);
    uint32_t flags;
    /* Protected by iothread lock.  */
    char idstr[256];
    /* RCU-enabled, writes protected by the ramlist lock */
    QLIST_ENTRY(RAMBlock) next;  RAMList blocks上的節點。
    QLIST_HEAD(, RAMBlockNotifier) ramblock_notifiers;
    int fd;
    size_t page_size;
};

typedef struct RAMList {
    QemuMutex mutex;
    RAMBlock *mru_block;
    /* RCU-enabled, writes protected by the ramlist lock. */
    QLIST_HEAD(, RAMBlock) blocks; RAMBlock鏈表,遍歷ram_list.blocks就可以查看所有的RAMBlock。
    DirtyMemoryBlocks *dirty_memory[DIRTY_MEMORY_NUM];
    uint32_t version;
    QLIST_HEAD(, RAMBlockNotifier) ramblock_notifiers;
} RAMList;
extern RAMList ram_list;

qemu_ram_alloc_from_ptr、qemu_ram_alloc、qemu_ram_alloc_resizeable三個函數都調用qemu_ram_alloc_internal。只是參數不同而已。

RAMBlock *qemu_ram_alloc(ram_addr_t size, MemoryRegion *mr, Error **errp)

RAMBlock *qemu_ram_alloc_from_ptr(ram_addr_t size, void *host, MemoryRegion *mr, Error **errp)
RAMBlock *qemu_ram_alloc_resizeable(ram_addr_t size, ram_addr_t maxsz, void (*resized)(const char*,
                                                     uint64_t length, void *host), MemoryRegion *mr, Error **errp)

三個函數返回值都是RAMBlock,qemu_ram_alloc最簡單,只指定ram大小和MemoryRegion;qemu_ram_alloc多一個參數,指定host;qemu_ram_alloc_resizeable相對於qemu_ram_alloc制定了resize函數和ram最大值。

static
RAMBlock *qemu_ram_alloc_internal(ram_addr_t size, ram_addr_t max_size,
                                  void (*resized)(const char*,
                                                  uint64_t length,
                                                  void *host),
                                  void *host, bool resizeable,
                                  MemoryRegion *mr, Error **errp)
{
    RAMBlock *new_block;
    Error *local_err = NULL;

    size = HOST_PAGE_ALIGN(size);
    max_size = HOST_PAGE_ALIGN(max_size);
    new_block = g_malloc0(sizeof(*new_block));
    new_block->mr = mr;
    new_block->resized = resized;
    new_block->used_length = size;
    new_block->max_length = max_size;
    assert(max_size >= size);
    new_block->fd = -1;
    new_block->page_size = getpagesize();
    new_block->host = host;
    if (host) {
        new_block->flags |= RAM_PREALLOC;
    }
    if (resizeable) {
        new_block->flags |= RAM_RESIZEABLE;
    }
    ram_block_add(new_block, &local_err);
    if (local_err) {
        g_free(new_block);
        error_propagate(errp, local_err);
        return NULL;
    }
    return new_block;
}

ram_block_add首先通過find_ram_offset獲取分配給當前RAMBlock的offset。然后在沒有指定host情況下,xen_enabled時則通過xen_ram_alloc分配內存;其它通過phys_mem_alloc分配內存。在結尾通過qemu_madvise來設置內存的使用建議。

其中涉及到一個重要的結構體RAMBlock,QEMU模擬了普通內存分布模型,內存的線性也是分塊被使用的,每個塊被稱為RAMBlock,由ram_list統領。

RAMBlock.offset是區塊的線性地址,及相對於開始的偏移位,RAMBlock.max_length則是區塊的大小。

find_ram_offset則是在線性區間內找到沒有使用的一段區間,可以完全容納新申請的RAMBlock.max_length大小,代碼就是進行了所有區塊的遍歷,找到滿足新申請max_length的最小區間,把RAMBlock安插進去即可,返回的offset即是新分配區間的開始地址。

RAMBlock.host是RAMBlock對應的地址,由phys_mem_alloc分配真正的物理內存,由mmap使用RAMBlock.mr.align也對齊進行內存映射。

后面就是對RAMBlock進行插入等處理。

至此memory_region_init_ram已經將qemu內存模型和實際的物理內存初始化了。

vmstate_register_ram_global這個函數則是負責將前面提到的ramlist中的ramblock和memory region的初始地址對應一下,將mr->name填充到ramblock的idstr里面,就是讓二者有確定的對應關系,如此mr就有了物理內存使用。

qemu_memory_module

 

static ram_addr_t find_ram_offset(ram_addr_t size)
{
    RAMBlock *block, *next_block;
    ram_addr_t offset = RAM_ADDR_MAX, mingap = RAM_ADDR_MAX;

    assert(size != 0); /* it would hand out same offset multiple times */

    if (QLIST_EMPTY_RCU(&ram_list.blocks)) { 判斷ram_list.blocks是否為空鏈表。
        return 0;
    }

    QLIST_FOREACH_RCU(block, &ram_list.blocks, next) {

        ram_addr_t end, next = RAM_ADDR_MAX;

        end = block->offset + block->max_length; 遍歷ram_list.blocks鏈表,獲取當前RAMBlock的尾地址。

        QLIST_FOREACH_RCU(next_block, &ram_list.blocks, next) {
            if (next_block->offset >= end) { 再次遍歷ram_list.blocks鏈表,選取end之后的RAMBlock進行比較。
                next = MIN(next, next_block->offset);
            }
        }
        if (next - end >= size && next - end < mingap) { 確保所選的空間滿足size大小,並且在滿足size條件下最小。
            offset = end;
            mingap = next - end;
        }
    }

    if (offset == RAM_ADDR_MAX) { 此種情況存在溢出危險。
        fprintf(stderr, "Failed to find gap of requested size: %" PRIu64 "\n",
                (uint64_t)size);
        abort();
    }

    return offset;
}

phys_mem_alloc默認指向qemu_anon_ram_alloc:

static void *(*phys_mem_alloc)(size_t size, uint64_t *align) =
                               qemu_anon_ram_alloc;
qemu_anon_ram_alloc
  -->qemu_ram_mmap
    -->mmap

進入ram_block_add看看詳細:

static void ram_block_add(RAMBlock *new_block, Error **errp)
{
    RAMBlock *block;
    RAMBlock *last_block = NULL;
    ram_addr_t old_ram_size, new_ram_size;
    Error *err = NULL;

    old_ram_size = last_ram_offset() >> TARGET_PAGE_BITS;

    qemu_mutex_lock_ramlist(); 給ram_list加鎖。
    new_block->offset = find_ram_offset(new_block->max_length); 在ram_list找到可用的RAMBlock。

    if (!new_block->host) {
        if (xen_enabled()) { 針對xen分配內存。
            xen_ram_alloc(new_block->offset, new_block->max_length,
                          new_block->mr, &err);
            if (err) {
                error_propagate(errp, err);
                qemu_mutex_unlock_ramlist();
                return;
            }
        } else { 調用mmap分配內存。
            new_block->host = phys_mem_alloc(new_block->max_length,
                                             &new_block->mr->align);
            if (!new_block->host) {
                error_setg_errno(errp, errno,
                                 "cannot set up guest memory '%s'",
                                 memory_region_name(new_block->mr));
                qemu_mutex_unlock_ramlist();
                return;
            }
            memory_try_enable_merging(new_block->host, new_block->max_length); 通過madvise設置內存區間的屬性。
        }
    }

    new_ram_size = MAX(old_ram_size,
              (new_block->offset + new_block->max_length) >> TARGET_PAGE_BITS);
    if (new_ram_size > old_ram_size) {
        migration_bitmap_extend(old_ram_size, new_ram_size);
        dirty_memory_extend(old_ram_size, new_ram_size);
    }
    /* Keep the list sorted from biggest to smallest block.  Unlike QTAILQ,
     * QLIST (which has an RCU-friendly variant) does not have insertion at
     * tail, so save the last element in last_block.
     */
    QLIST_FOREACH_RCU(block, &ram_list.blocks, next) {
        last_block = block;
        if (block->max_length < new_block->max_length) {
            break;
        }
    }
    if (block) { 將新建RAMBlock new_block插入到RAMLIst上。
        QLIST_INSERT_BEFORE_RCU(block, new_block, next);
    } else if (last_block) {
        QLIST_INSERT_AFTER_RCU(last_block, new_block, next);
    } else { /* list is empty */
        QLIST_INSERT_HEAD_RCU(&ram_list.blocks, new_block, next);
    }
    ram_list.mru_block = NULL;

    /* Write list before version */
    smp_wmb(); 此處加寫內存屏障,確保在ram_list.version變動之前new_block已經插入。
    ram_list.version++;
    qemu_mutex_unlock_ramlist(); 給ram_list解鎖。

    cpu_physical_memory_set_dirty_range(new_block->offset,
                                        new_block->used_length,
                                        DIRTY_CLIENTS_ALL);

    if (new_block->host) {
        qemu_ram_setup_dump(new_block->host, new_block->max_length);
        qemu_madvise(new_block->host, new_block->max_length, QEMU_MADV_HUGEPAGE); 設置內存塊的屬性,參照前面配置。
        /* MADV_DONTFORK is also needed by KVM in absence of synchronous MMU */
        qemu_madvise(new_block->host, new_block->max_length, QEMU_MADV_DONTFORK);
        ram_block_notify_add(new_block->host, new_block->max_length);
    }
}

 

KVM源代碼分析5:IO虛擬化之PIO


免責聲明!

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



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