zoukankan      html  css  js  c++  java
  • 关于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 ", __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 ", __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 ", __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 ", 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 ");
            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 ", -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 ");
            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() ");

        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 ");
                /*
                 * 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 " ",
                    (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

  • 相关阅读:
    信息量
    MVC4的实战:排球计分(一)(综述)
    排球计分规则3.17
    观后感-----怎样成为一个高手
    本学期最后一个博客
    第五组作业
    个人作业
    第五组作业
    个人作业
    一周的总结
  • 原文地址:https://www.cnblogs.com/arnoldlu/p/6510641.html
Copyright © 2011-2022 走看看