简要讲解内核中KVM源码实现

一、KVM内核模块组成概述

根据kvm的Makefile(arch/x86/kvm/Makefile)文件可知:该部分Makefile主要由三个模块生成:kvm.ko、kvm-intel.ko、kvm-amd.ko

KVM大致目录:


arch/x86/kvm/	(kvm主要文件目录,包含x86架构kvm所需头文件代码等)
	/mmu/
	/svm/
		sev.c
		svm.c
		svm.h
	/vmx/
	Kconfig
	Makefile
	cpuid.c
	cpuid.h
	debugfs.c
	hyperv.c
	hyperv.h
	i8259.c
	irq.c
	irq.h
	kvm_cache_regs.h
	kvm_emulate.h
	lapic.c
	lapic.h
	mmu.c
	mmu.h
	paging_tmpl.h
	segment_descriptor.h
	x86.c
	x86_emulate.c
	
include/asm-x86/(包含KVM的一些汇编语言操作所需的相关宏定义、全局变量等)
	kvm.h
	kvm_host.h
	kvm_para.h
	kvm_x86_emulate.h
	
include/linux/	(包含KVM的一些参数定义信息)
	kvm.h(include/uapi/linux/kvm.h)
	kvm_host.h
	kvm_para.h
	kvm_x86_emulate.h(现已没有)
	
include/kvm/
	arm_arch_timer.h
	arm_hypercalls.h
	arm_pmu.h
	arm_psci.h
	arm_vgic.h
	iodev.h
	
virt/kvm/		(KVM架构性质文件,独立于处理器,提供公用方法和数据)
	ioapic.h
	ioapic.c
	iodev.h(include/kvm/iodev.h)
	*kvm_main.c	(其中的kvm_init函数是kvm初始化入口)
image-20230802115126599 image-20230801230341296 image-20230801230256972 image-20230801225945811
# 二、KVM所提供的API

2.1 KVM API纵览

KVM的API通过/dev/kvm 设备进行访问。/dev/kvm 是一个字符型设备
在这里插入图片描述
所有对KVM的操作都是通过ioctl 发送的相应的控制字实现的。

KVM所提供的用户控件API从功能上划分,可以分成三种类型

API类型功能说明
system指令针对虚拟化系统的全局性参数设置和用于虚拟机创建等控制操作
VM指令针对具体的VM进行控制,如进行内存设置、创建VCPU等。注意:VM指令不是进程安全的
vcpu指令针对具体的vCPU进行参数设置(MRU寄存器读写、中断控制等)

通常,对于KVM API的操作是从打开/dev/kvm 设备文件开始的,通过使用系统调用open之后,会获得针对kvm subsystem的一个fd文件描述符。然后通过ioctl系统指针对该文件描述符进行进一步的操作。通过KVM_CREATE_VM指令将创建一个虚拟机并返回该虚拟机对应的fd文件描述符,然后根据该描述符来控制虚拟机的行为。通过KVM_CREATE_VCPU指令,将创建一个虚拟CPU并且返回该CPU对应的fd。

2.2 system ioctls调用

system ioctls系统调用用于控制KVM运行环境的参数,相关工作包括全局性的参数设置和虚拟机创建等工作,主要指令如下表。

指令字功能说明
KVM_GET_API_VERSION查询当前KVM API的版本
KVM_CREATE_VM创建KVM需虚拟机
KVM_GET_MSR_INDEX_LIST获得MSR索引列表
KVM_CHECK_EXTENSION检查扩展的支持情况
KVM_GET_VCPU_MMAP_SIZE运行虚拟机和用户态空间共享的一片内存区域的大小

通过KVM_CREATE_VM,KVM将返回一个文件描述符,该文件描述符指向内核空间中的新的虚拟机。全新创建的虚拟机没有VCPU,也没有内存,需要通过后续的ioctl指令进行配置。mmap()系统调用,则会直接返回该虚拟机对应的虚拟内存空间,并且内存的偏移量为0。如果KVM支持KVM_CAP_USER_MEMORY扩展特性,则应使用其他方法。

2.3 vm ioctl系统调用

vm ioctl系统调用实现了对虚拟机的控制。vm ioctl控制指令的参数大多需要从KVM_CREATE_VM中返回的fd文件描述符来进行操作,涉及的操作主要针对某一个虚拟机进行控制,如配置内存、配置VCPU等,主要指令如下表。

指令字功能说明
KVM_CREATE_VCPU为已经创建好的VM添加VCPU
KVM_GET_DIRTY_LOG返回经过上次操作之后,改变的内存页的位图
KVM_CREATE_IRQCHIP创建一个虚拟APIC,并且随后创建的VCPU都将连接到该APIC
KVM_IRQ_LINE对某虚拟的APIC发送中断信号
KVM_GET_IRQCHIP读取APIC的中断标志信息
KVM_SET_IRQCHIP写入APIC的中断标志结构
KVM_RUN根据kvm_run结构体的信息,启动VM虚拟机

KVM_RUN和KVM_CREATE_VCPU是vm ioctl系统调用的两个重要指令字。在通过KVM_CREATE_VCPU为VM虚拟机创建VCPU,并且获得VCPU对应的fd文件描述符之后,可以进行KVM_RUN启动虚拟机的操作。

KVM_RUN指令字虽然没有任何参数,但是在调用KVM_RUN启动了虚拟机之后,可以通过mmap()系统调用映射VCPU的fd所在的内存空间来获得kvm_run结构体的信息。该结构体位于内存偏移量0,结束位置在KVM_GET_VCPU_MMAP_SIZE指令所返回的大小之中。

kvm_run结构体的定义在include/linux/kvm.h中,通过读取该结构体可以了解KVM内部的运行状态,可以类比为计算机芯片中的寄存器组。其中主要的字段以及说明如下图所示。

字段名功能说明
request_interrupt_window往VCPU中发出一个中断插入请求,让VCPU做好相关的准备工作
ready_for_interrupt_injection响应request_interrupt_windows中的中断请求,当此位有效时,说明可以进行中断
if_flag中断标识,如果使用了APIC,则不起作用
hardware_exit_reason当VCPU因为各种不明原因退出时,该字段保存了失败的描述信息(硬件失效)
io该字段为一个结构体。当KVM产生硬件出错的原因是因为I/O输出时(KVM_EXIT_IO),该结构体将保存导出出错的I/O请求原因
mmio该字段为一个结构体。当KVM产生出错的原因是因为内存I/O映射导致的(KVM_EXIT_MMIO),该结构体中将保存导致出错的内存I/O映射请求数据

2.4 vcpu ioctl系统调用

vcpu ioctl系统调用主要针对具体的每一个虚拟的vCPU进行配置,包括寄存器读/写、中断设置、内存配置、调试开关、时钟管理等功能,能够对KVM的虚拟机进行精确的运行时配置。

对于一个VM的CPU来说,寄存器控制是最重要的一个环节,vcpu ioctl在寄存器控制方面提供了丰富的指令字,如下表。
在这里插入图片描述
KVM在中断管理和事件管理中,也提供了丰富的指令字。在KVM运行期间,可以通过中断管理向vCPU插入中断,或者获得vCPU的一些事件(如热插拔等),该指令字如下表。
在这里插入图片描述
在这里插入图片描述
内存管理时虚拟机管理中一个重要的组成部分,KVM也提供了相应的API支持,该部分指令字如下图所示。
在这里插入图片描述
除了上面的vCPU管理、中断管理和内存管理之外,KVM最后还提供了其他方面的管理,如CPUID的配置、调试接口等,这部分接口的指令字如下表所示。
在这里插入图片描述
在这里插入图片描述

三、KVM内核模块重要的数据结构

3.1 KVM结构体

KVM结构体-代表 :在KVM系统架构中代表一个具体的虚拟机

KVM结构体-生产 :使用VM_CREATE_KVM指令字创建一个新的KVM虚拟机之后,就会创建一个心得KVM结构体对象

KVM结构体-包括 :vCPU、内存、APIC、IRQ、MMU、Event事件管理等信息。(这些信息主要在KVM虚拟机内部使用,用于跟踪虚拟机的状态)

KVM结构体-编译开关 :在定义KVM结构体的结构成员的过程中,集成了很多编译开关,这些开关对应了KVM体系中的不同功能点。在KVM中,连接了如下几个重要的结构体成员,它们对虚拟机的运行有重要的作用。

结构体成员作用
struct kvm_memslots *memslots;KVM虚拟机所分配到的内存slot,以数组形式存储这些slot地址信息。
strut kvm_vcpu *vcpus[KVM_MAX_VCPUS];KVM虚拟机中包含的vCPU结构体,一个虚拟CPU对应一个vCPU结构体。
struct kvm_io_bus *buses[KVM_NR_BUSES];KVM虚拟机中的I/O总线,一条总线对应一个kvm_io_bus结构体,如ISA总线、PCI总线。
struct kvm_vm_stat stat;KVM虚拟机中的页表、MMU等运行时状态信息。
struct kvm_arch arch;KVM的软件arch方面所需要的一些参数

3.2 kvm_vcpu结构体

kvm_vcpu结构体-产生 :用户通过KVM_CREATE_VCPU系统调用请求创建VCPU之后,KVM子模块将创建kvm_vcpu结构体并进行相应的初始化操作,并返回对应的vcpu_fd描述符。

kvm_vcpu结构体中的字段作用
int vcpu_id;对应的VCPU的ID。
struct kvm_run *run;VCPU的运行时参数,其中保存了寄存器信息、内存信息、虚拟机状态等各种动态信息。
struct kvm_vcpu_arch arch;存储有KVM虚拟机的运行时参数,如定时器、中断、内存槽等方面的信息。

PS,kvm_cpu中还包含了执行iomem所需要的数据结构,用于处理iomem方面的请求。

3.3 kvm_x86_ops结构体

kvm_x86_ops结构体-作用 :结构体中包含了针对具体的CPU架构进行虚拟化时的函数指针调用。

kvm_x86_ops结构体-位置 :定义在Linux内核文件的arch/x86/include/asm/kvm_host.h 中。

kvm_x86_ops结构体-包含操作 :CPU VMM状态硬件初始化;vCPU创建与管理、中断管理、寄存器管理、时钟管理。

//arch/x86/include/asm/kvm_host.h
struct kvm_x86_ops {
	int (*hardware_enable)(void);
	void (*hardware_disable)(void);
	void (*hardware_unsetup)(void);
	bool (*cpu_has_accelerated_tpr)(void);
	bool (*has_emulated_msr)(u32 index);
	void (*vcpu_after_set_cpuid)(struct kvm_vcpu *vcpu);

	unsigned int vm_size;
	int (*vm_init)(struct kvm *kvm);
	void (*vm_destroy)(struct kvm *kvm);

	/* Create, but do not attach this VCPU */
	int (*vcpu_create)(struct kvm_vcpu *vcpu);
	void (*vcpu_free)(struct kvm_vcpu *vcpu);
	void (*vcpu_reset)(struct kvm_vcpu *vcpu, bool init_event);

	void (*prepare_guest_switch)(struct kvm_vcpu *vcpu);
	void (*vcpu_load)(struct kvm_vcpu *vcpu, int cpu);
	void (*vcpu_put)(struct kvm_vcpu *vcpu);

	void (*update_exception_bitmap)(struct kvm_vcpu *vcpu);
	int (*get_msr)(struct kvm_vcpu *vcpu, struct msr_data *msr);
	int (*set_msr)(struct kvm_vcpu *vcpu, struct msr_data *msr);
	u64 (*get_segment_base)(struct kvm_vcpu *vcpu, int seg);
	void (*get_segment)(struct kvm_vcpu *vcpu,
			    struct kvm_segment *var, int seg);
	int (*get_cpl)(struct kvm_vcpu *vcpu);
	void (*set_segment)(struct kvm_vcpu *vcpu,
			    struct kvm_segment *var, int seg);
	void (*get_cs_db_l_bits)(struct kvm_vcpu *vcpu, int *db, int *l);
	void (*set_cr0)(struct kvm_vcpu *vcpu, unsigned long cr0);
	int (*set_cr4)(struct kvm_vcpu *vcpu, unsigned long cr4);
	int (*set_efer)(struct kvm_vcpu *vcpu, u64 efer);
	void (*get_idt)(struct kvm_vcpu *vcpu, struct desc_ptr *dt);
	void (*set_idt)(struct kvm_vcpu *vcpu, struct desc_ptr *dt);
	void (*get_gdt)(struct kvm_vcpu *vcpu, struct desc_ptr *dt);
	void (*set_gdt)(struct kvm_vcpu *vcpu, struct desc_ptr *dt);
	void (*sync_dirty_debug_regs)(struct kvm_vcpu *vcpu);
	void (*set_dr7)(struct kvm_vcpu *vcpu, unsigned long value);
	void (*cache_reg)(struct kvm_vcpu *vcpu, enum kvm_reg reg);
	unsigned long (*get_rflags)(struct kvm_vcpu *vcpu);
	void (*set_rflags)(struct kvm_vcpu *vcpu, unsigned long rflags);

	void (*tlb_flush_all)(struct kvm_vcpu *vcpu);
	void (*tlb_flush_current)(struct kvm_vcpu *vcpu);
	int  (*tlb_remote_flush)(struct kvm *kvm);
	int  (*tlb_remote_flush_with_range)(struct kvm *kvm,
			struct kvm_tlb_range *range);

	/*
	 * Flush any TLB entries associated with the given GVA.
	 * Does not need to flush GPA->HPA mappings.
	 * Can potentially get non-canonical addresses through INVLPGs, which
	 * the implementation may choose to ignore if appropriate.
	 */
	void (*tlb_flush_gva)(struct kvm_vcpu *vcpu, gva_t addr);

	/*
	 * Flush any TLB entries created by the guest.  Like tlb_flush_gva(),
	 * does not need to flush GPA->HPA mappings.
	 */
	void (*tlb_flush_guest)(struct kvm_vcpu *vcpu);

	enum exit_fastpath_completion (*run)(struct kvm_vcpu *vcpu);
	int (*handle_exit)(struct kvm_vcpu *vcpu,
		enum exit_fastpath_completion exit_fastpath);
	int (*skip_emulated_instruction)(struct kvm_vcpu *vcpu);
	void (*update_emulated_instruction)(struct kvm_vcpu *vcpu);
	void (*set_interrupt_shadow)(struct kvm_vcpu *vcpu, int mask);
	u32 (*get_interrupt_shadow)(struct kvm_vcpu *vcpu);
	void (*patch_hypercall)(struct kvm_vcpu *vcpu,
				unsigned char *hypercall_addr);
	void (*set_irq)(struct kvm_vcpu *vcpu);
	void (*set_nmi)(struct kvm_vcpu *vcpu);
	void (*queue_exception)(struct kvm_vcpu *vcpu);
	void (*cancel_injection)(struct kvm_vcpu *vcpu);
	int (*interrupt_allowed)(struct kvm_vcpu *vcpu, bool for_injection);
	int (*nmi_allowed)(struct kvm_vcpu *vcpu, bool for_injection);
	bool (*get_nmi_mask)(struct kvm_vcpu *vcpu);
	void (*set_nmi_mask)(struct kvm_vcpu *vcpu, bool masked);
	void (*enable_nmi_window)(struct kvm_vcpu *vcpu);
	void (*enable_irq_window)(struct kvm_vcpu *vcpu);
	void (*update_cr8_intercept)(struct kvm_vcpu *vcpu, int tpr, int irr);
	bool (*check_apicv_inhibit_reasons)(ulong bit);
	void (*pre_update_apicv_exec_ctrl)(struct kvm *kvm, bool activate);
	void (*refresh_apicv_exec_ctrl)(struct kvm_vcpu *vcpu);
	void (*hwapic_irr_update)(struct kvm_vcpu *vcpu, int max_irr);
	void (*hwapic_isr_update)(struct kvm_vcpu *vcpu, int isr);
	bool (*guest_apic_has_interrupt)(struct kvm_vcpu *vcpu);
	void (*load_eoi_exitmap)(struct kvm_vcpu *vcpu, u64 *eoi_exit_bitmap);
	void (*set_virtual_apic_mode)(struct kvm_vcpu *vcpu);
	void (*set_apic_access_page_addr)(struct kvm_vcpu *vcpu);
	int (*deliver_posted_interrupt)(struct kvm_vcpu *vcpu, int vector);
	int (*sync_pir_to_irr)(struct kvm_vcpu *vcpu);
	int (*set_tss_addr)(struct kvm *kvm, unsigned int addr);
	int (*set_identity_map_addr)(struct kvm *kvm, u64 ident_addr);
	u64 (*get_mt_mask)(struct kvm_vcpu *vcpu, gfn_t gfn, bool is_mmio);

	void (*load_mmu_pgd)(struct kvm_vcpu *vcpu, unsigned long pgd,
			     int pgd_level);

	bool (*has_wbinvd_exit)(void);

	/* Returns actual tsc_offset set in active VMCS */
	u64 (*write_l1_tsc_offset)(struct kvm_vcpu *vcpu, u64 offset);

	/*
	 * Retrieve somewhat arbitrary exit information.  Intended to be used
	 * only from within tracepoints to avoid VMREADs when tracing is off.
	 */
	void (*get_exit_info)(struct kvm_vcpu *vcpu, u64 *info1, u64 *info2,
			      u32 *exit_int_info, u32 *exit_int_info_err_code);

	int (*check_intercept)(struct kvm_vcpu *vcpu,
			       struct x86_instruction_info *info,
			       enum x86_intercept_stage stage,
			       struct x86_exception *exception);
	void (*handle_exit_irqoff)(struct kvm_vcpu *vcpu);

	void (*request_immediate_exit)(struct kvm_vcpu *vcpu);

	void (*sched_in)(struct kvm_vcpu *kvm, int cpu);

	/*
	 * Arch-specific dirty logging hooks. These hooks are only supposed to
	 * be valid if the specific arch has hardware-accelerated dirty logging
	 * mechanism. Currently only for PML on VMX.
	 *
	 *  - slot_enable_log_dirty:
	 *	called when enabling log dirty mode for the slot.
	 *  - slot_disable_log_dirty:
	 *	called when disabling log dirty mode for the slot.
	 *	also called when slot is created with log dirty disabled.
	 *  - flush_log_dirty:
	 *	called before reporting dirty_bitmap to userspace.
	 *  - enable_log_dirty_pt_masked:
	 *	called when reenabling log dirty for the GFNs in the mask after
	 *	corresponding bits are cleared in slot->dirty_bitmap.
	 */
	void (*slot_enable_log_dirty)(struct kvm *kvm,
				      struct kvm_memory_slot *slot);
	void (*slot_disable_log_dirty)(struct kvm *kvm,
				       struct kvm_memory_slot *slot);
	void (*flush_log_dirty)(struct kvm *kvm);
	void (*enable_log_dirty_pt_masked)(struct kvm *kvm,
					   struct kvm_memory_slot *slot,
					   gfn_t offset, unsigned long mask);

	/* pmu operations of sub-arch */
	const struct kvm_pmu_ops *pmu_ops;
	const struct kvm_x86_nested_ops *nested_ops;

	/*
	 * Architecture specific hooks for vCPU blocking due to
	 * HLT instruction.
	 * Returns for .pre_block():
	 *    - 0 means continue to block the vCPU.
	 *    - 1 means we cannot block the vCPU since some event
	 *        happens during this period, such as, 'ON' bit in
	 *        posted-interrupts descriptor is set.
	 */
	int (*pre_block)(struct kvm_vcpu *vcpu);
	void (*post_block)(struct kvm_vcpu *vcpu);

	void (*vcpu_blocking)(struct kvm_vcpu *vcpu);
	void (*vcpu_unblocking)(struct kvm_vcpu *vcpu);

	int (*update_pi_irte)(struct kvm *kvm, unsigned int host_irq,
			      uint32_t guest_irq, bool set);
	void (*apicv_post_state_restore)(struct kvm_vcpu *vcpu);
	bool (*dy_apicv_has_pending_interrupt)(struct kvm_vcpu *vcpu);

	int (*set_hv_timer)(struct kvm_vcpu *vcpu, u64 guest_deadline_tsc,
			    bool *expired);
	void (*cancel_hv_timer)(struct kvm_vcpu *vcpu);

	void (*setup_mce)(struct kvm_vcpu *vcpu);

	int (*smi_allowed)(struct kvm_vcpu *vcpu, bool for_injection);
	int (*pre_enter_smm)(struct kvm_vcpu *vcpu, char *smstate);
	int (*pre_leave_smm)(struct kvm_vcpu *vcpu, const char *smstate);
	void (*enable_smi_window)(struct kvm_vcpu *vcpu);

	int (*mem_enc_op)(struct kvm *kvm, void __user *argp);
	int (*mem_enc_reg_region)(struct kvm *kvm, struct kvm_enc_region *argp);
	int (*mem_enc_unreg_region)(struct kvm *kvm, struct kvm_enc_region *argp);

	int (*get_msr_feature)(struct kvm_msr_entry *entry);

	bool (*can_emulate_instruction)(struct kvm_vcpu *vcpu, void *insn, int insn_len);

	bool (*apic_init_signal_blocked)(struct kvm_vcpu *vcpu);
	int (*enable_direct_tlbflush)(struct kvm_vcpu *vcpu);

	void (*migrate_timers)(struct kvm_vcpu *vcpu);
	void (*msr_filter_changed)(struct kvm_vcpu *vcpu);
};

kvm_x86_ops结构体中所有的成员都是函数指针。在KVM的初始化过程和后续的运行过程中,KVM子系统的代码将通过调用该结构体的函数进行实际的硬件操作。

kvm_x86_ops结构体通过静态初始化。针对amd架构的初始化代码在svm.c中,针对intel架构的初始化代码在vmx.c中。amd架构的kvm_x86_ops结构体部分代码如下。

//arch/x86/kvm/svm/svm.c
static struct kvm_x86_ops svm_x86_ops __initdata = {
    .hardware_unsetup = svm_hardware_teardown,
	.hardware_enable = svm_hardware_enable,
	.hardware_disable = svm_hardware_disable,
	.cpu_has_accelerated_tpr = svm_cpu_has_accelerated_tpr,
	.has_emulated_msr = svm_has_emulated_msr,

	.vcpu_create = svm_create_vcpu,
	.vcpu_free = svm_free_vcpu,
	.vcpu_reset = svm_vcpu_reset,

	.vm_size = sizeof(struct kvm_svm),
	.vm_init = svm_vm_init,
	.vm_destroy = svm_vm_destroy,

	.prepare_guest_switch = svm_prepare_guest_switch,
	.vcpu_load = svm_vcpu_load,
	.vcpu_put = svm_vcpu_put,
	.vcpu_blocking = svm_vcpu_blocking,
	.vcpu_unblocking = svm_vcpu_unblocking,

	.update_exception_bitmap = update_exception_bitmap,
	.get_msr_feature = svm_get_msr_feature,
	.get_msr = svm_get_msr,
	.set_msr = svm_set_msr,
//……有100多行代码忽略
};
    
static struct kvm_x86_init_ops svm_init_ops __initdata = {
	.cpu_has_kvm_support = has_svm,
	.disabled_by_bios = is_disabled,
	.hardware_setup = svm_hardware_setup,
	.check_processor_compatibility = svm_check_processor_compat,

	.runtime_ops = &svm_x86_ops,
};

kvm_x86_ops结构体是在KVM架构的初始化过程中注册并导出成为全局变量,以方便KVM的各个子模块能够方便地调用。

在 arch/x86/kvm/x86.c 中,定义了名为kvm_x86_ops的静态变量,通过export_symbol 宏在全局范围内导出。在kvm_init 的初始化过程中,通过调用kvm_arch_init函数给kvm_x86_ops赋值,代码如下。其中,ops就是通过svm.c 调用kvm_init函数时传入的kvm_x86_ops 结构体。

//arch/x86/kvm/x86.c
struct kvm_x86_ops kvm_x86_ops __read_mostly;
EXPORT_SYMBOL_GPL(kvm_x86_ops);

3.4 KVM API中重要的结构体

KVM和用户态程序进行交互的过程中,主要通过/dev/kvm 设备文件进行通信。/dev/kvm 是一个字符型设备,通过符合Linux标准的一系列结构进行支撑,主要是kvm_chardev_ops、kvm_vm_fops、kvm_vcpu_fops ,分别对应字符型设备、VM文件描述符和VCPU文件描述符的三种操作。

kvm_chardev_ops——字符型设备

kvm_chardev_ops 的定义在 virt/kvm/kvm_main.c 中,代码如下:

//virt/kvm/kvm_main.c
static struct file_operations kvm_chardev_ops = {
	.unlocked_ioctl = kvm_dev_ioctl,
	.llseek		= noop_llseek,
	KVM_COMPAT(kvm_dev_ioctl),
};

kvm_chardev_ops 为一个标准的file_operations 结构体,但是只包含了ioctl函数,read、open、write等常见的系统调用均采用默认实现。因此,就只能在用户态通过ioctl函数进行操作。

kvm_vm_fops——VM的fd文件描述符

前文所讲:“通过KVM_CREATE_VM之后可以获得一个fd文件描述符,其代表了该VM”。提到的这个fd在KVM子模块内部操作实际上对应着kvm_vm_fops 结构体。

//virt/kvm/kvm_main.c
static struct file_operations kvm_vm_fops = {
	.release        = kvm_vm_release,
	.unlocked_ioctl = kvm_vm_ioctl,
	.llseek		= noop_llseek,
	KVM_COMPAT(kvm_vm_compat_ioctl),
};

针对VM的文件操作中,提供了ioctl这个操作函数。

而在更早的版本中,还额外提供一个mmap操作函数。
在这里插入图片描述

mmap对应着GUEST OS的物理地址,可以直接对GUEST OS的地址空间进行读/写,ioctl则用于发送KVM的控制字。

kvm_vcpu_fops——VCPU的文件描述符

针对KVM的fd,通过KVM_CREATE_VCPU指令字可以创建KVM的VCPU,并且获得该vcpu_fd。该vcpu_fd的操作主要包含在kvm_vcpu_fops中。

//virt/kvm/kvm_main.c
static struct file_operations kvm_vcpu_fops = {
	.release        = kvm_vcpu_release,
	.unlocked_ioctl = kvm_vcpu_ioctl,
	.mmap           = kvm_vcpu_mmap,
	.llseek		= noop_llseek,
	KVM_COMPAT(kvm_vcpu_compat_ioctl),
};

在ioctl中,通过发送ioctl,即可对VCPU进行控制。通过mmap可以访问kvm_run结构体,在这个结构体中保存了VCPU运行和控制的信息,并且可以对其运行参数进行设置。

四、KVM内核模块重要流程分析

4.1 初始化流程

KVM模块分为三个主要模块:kvm.ko、kvm-intel.ko和kvm-amd.ko,这三个模块在初始化阶段的流程如图所示。
在这里插入图片描述
KVM模块可以编译进内核中,也可以作为内核模块在Linux系统启动完成之后加载。

Linux的子模块入口通常通过module_init宏进行定义,由内核进行调用。KVM的初始化流程如图所示。
在这里插入图片描述
参考上图,KVM的初始化步骤分为以下三步。

  1. 在平台相关的KVM模块中通过module_init宏正式进入KVM的初始化阶段,并且执行相关的硬件初始化准备。
  2. 进入kvm_main.c 中的kvm_init函数进行正式的初始化工作,期间进行了一系列子操作。
    • 通过kvm_arch_init函数初始化KVM内部的一些数据结构:注册全局变量kvm_x86_ops、初始化MMU等数据结构、初始化Timer定时器架构。
    • 分配KVM内部操作所需要的内存空间。
    • 调用kvm_x86_ops的hardware_setup函数进行具体的硬件体系架构的初始化工作。
    • 注册sysfs和devfs等API接口信息
    • 最后初始化debugfs的调试信息
  3. 进行后续的硬件初始化准备操作。

4.2 虚拟机的创建

基于KVM的虚拟机创建分为虚拟机创建和虚拟CPU创建两个步骤。VM对应的文件描述符为vm_fd,vCPU对应的文件描述符为vcpu_fd。

打开/dev/kvm 文件并且获得文件描述符fd 后,通过ioctl指令写入 KVM_CREATE_VM ,即可创建一个 VM 虚拟机。KVM的该部分代码实现在kvm_dev的 file_operation结构体中,对应的代码在kvm_main.c 中调用kvm_dev_ioctl_create_vm 函数实现,其代码如下:

//virt/kvm/kvm_main.c
static long kvm_dev_ioctl(struct file *filp, unsigned int ioctl, unsigned long arg)
{
	long r = -EINVAL;

	switch (ioctl) {
	case KVM_GET_API_VERSION:
		if (arg)
			goto out;
		r = KVM_API_VERSION;
		break;
	case KVM_CREATE_VM:		/* KVM_CREATE_VM */
		r = kvm_dev_ioctl_create_vm(arg);
		break;
	case KVM_CHECK_EXTENSION:
		r = kvm_vm_ioctl_check_extension_generic(NULL, arg);
		break;
	case KVM_GET_VCPU_MMAP_SIZE:
		if (arg)
			goto out;
		r = PAGE_SIZE;     /* struct kvm_run */
#ifdef CONFIG_X86
		r += PAGE_SIZE;    /* pio data page */
#endif
#ifdef CONFIG_KVM_MMIO
		r += PAGE_SIZE;    /* coalesced mmio ring page */
#endif
		break;
	case KVM_TRACE_ENABLE:
	case KVM_TRACE_PAUSE:
	case KVM_TRACE_DISABLE:
		r = -EOPNOTSUPP;
		break;
	default:
		return kvm_arch_dev_ioctl(filp, ioctl, arg);
	}
out:
	return r;
}

/* …………………………………省略多行分割线…………………………… */

static int kvm_dev_ioctl_create_vm(unsigned long type)
{
	int r;
	struct kvm *kvm;
	struct file *file;

	kvm = kvm_create_vm(type);
	if (IS_ERR(kvm))
		return PTR_ERR(kvm);
    
	if (kvm_create_vm_debugfs(kvm, r) < 0) {
		put_unused_fd(r);
		fput(file);
		return -ENOMEM;
	}
	kvm_uevent_notify_change(KVM_EVENT_CREATE_VM, kvm);

	fd_install(r, file);
	return r;

put_kvm:
	kvm_put_kvm(kvm);
	return r;
}

/* …………………………………省略多行分割线…………………………… */

static struct kvm *kvm_create_vm(unsigned long type)
{
	struct kvm *kvm = kvm_arch_alloc_vm();
	int r = -ENOMEM;
	int i;

	if (!kvm)
		return ERR_PTR(-ENOMEM);

	spin_lock_init(&kvm->mmu_lock);
	mmgrab(current->mm);
	kvm->mm = current->mm;
	kvm_eventfd_init(kvm);
    /* …………省略多行………… */
out_err:
#if defined(CONFIG_MMU_NOTIFIER) && defined(KVM_ARCH_WANT_MMU_NOTIFIER)
	if (kvm->mmu_notifier.ops)
		mmu_notifier_unregister(&kvm->mmu_notifier, current->mm);
#endif
	/* …………省略多行………… */
out_err_no_srcu:
	kvm_arch_free_vm(kvm);
	mmdrop(current->mm);
	return ERR_PTR(r);
}

kvm_dev_ioctl_create_vm函数通过调用kvm_create函数对KVM结构体进行创建。[KVM 结构体文件](#3.1 KVM结构体)如前文所述,保存了虚拟机运行的上下文及其他相关状态,在使用之前,需要进行一定的初始化工作。

在x86体系架构中,KVM结构体的初始化任务在kvm_arch_create_vm函数中进行, 进行了分配内存、初始化设备列表、设置中断管理和初始化tsc 的 spi_lock 的功能。在完成只有,将执行硬件初始化工作,该部分硬件初始化工作通过调用on_each_cpu 宏,将在每个物理CPU上执行同样的操作。

该操作主要是尝试将所有的CPU切换入 vitualize 模式,并且设置好时钟等信息,这个过程通过 kvm_arch_hardware_enable 函数完成。该函数代码(arch/x86/kvm/x86.c)如下,主要执行了两个工作:整理CPU的时钟信息;调用kvm_x86_ops 的硬件相关函数进行具体操作。

//arch/x86/kvm/x86.c
int kvm_arch_hardware_enable(void)
{
	/**/struct kvm *kvm;
	/**/struct kvm_vcpu *vcpu;
	/**/int i;
	int ret;
	u64 local_tsc;
	u64 max_tsc = 0;
	bool stable, backwards_tsc = false;

	/**/kvm_user_return_msr_cpu_online();
	/**/ret = kvm_x86_ops.hardware_enable();
	/**/if (ret != 0)
		/**/return ret;

	local_tsc = rdtsc();
	stable = !kvm_check_tsc_unstable();
	/**/list_for_each_entry(kvm, &vm_list, vm_list) {
		/**/kvm_for_each_vcpu(i, vcpu, kvm) {
			/**/if (!stable && vcpu->cpu == smp_processor_id())
				/**/kvm_make_request(KVM_REQ_CLOCK_UPDATE, vcpu);
			if (stable && vcpu->arch.last_host_tsc > local_tsc) {
				backwards_tsc = true;
				if (vcpu->arch.last_host_tsc > max_tsc)
					max_tsc = vcpu->arch.last_host_tsc;
			}
		}
	}

	/*
	 * 省略多行注释
	 */
	if (backwards_tsc) {
		u64 delta_cyc = max_tsc - local_tsc;
		list_for_each_entry(kvm, &vm_list, vm_list) {
			kvm->arch.backwards_tsc_observed = true;
			kvm_for_each_vcpu(i, vcpu, kvm) {
				vcpu->arch.tsc_offset_adjustment += delta_cyc;
				vcpu->arch.last_host_tsc = local_tsc;
				kvm_make_request(KVM_REQ_MASTERCLOCK_UPDATE, vcpu);
			}

			/*
			 * 省略多行注释
			 */
			kvm->arch.last_tsc_nsec = 0;
			kvm->arch.last_tsc_write = 0;
		}

	}
	return 0;
}

接下来,将初始化KVM的memslot结构体、Bus总线结构体信息、scru读/写锁信息、eventfd事件通知信息、mmu内存管理结构体信息。

然后,调用anon_inode_getfd 函数。该函数设置了对所有KVM的操作都将给予kvm_vm这个共享文件进行,该共享文件的操作封装在kvm_vm_fops结构体中,对VM的操作实际上就是对此文件的操作。因此,对其ioctl调用的是kvm_vm_fops中成员函数。

//virt/kvm/kvm_main.c
/*
 * Allocates an inode for the vcpu.
 */
static int create_vcpu_fd(struct kvm_vcpu *vcpu)
{
	char name[8 + 1 + ITOA_MAX_LEN + 1];

	snprintf(name, sizeof(name), "kvm-vcpu:%d", vcpu->vcpu_id);
	return anon_inode_getfd(name, &kvm_vcpu_fops, vcpu, O_RDWR | O_CLOEXEC);
}

通过 anon_inode_getfd 获得的fd文件描述符,就是供用户态的vm_fd,用户态将通过该fd 进行进一步的虚拟机操作,首先要做的事情是初始化vCPU。

4.3 vCPU的创建

创建vCPU实际上就是创建vCPU的描述符,在KVM中,vCPU对应的数据结构体为kvm_vcpu。创建vCPU的描述符,简单来说就是分配相应大小的内存,并且进行相应的初始化工作。

在物理 CPU 上电之后,需要进一步初始化才可以使用。在这个过程中,硬件会自动将CPU 初始化成特定的状态。kvm_vcpu 的初始化也是一个类似的过程,将kvm_vcpu 的各个数据结构体设置成可用的状态,通常包括如下内容:

  • 分配VCPU标识,设置cpu_id 属于哪个KVM 虚拟机,并且分配对该VCPU 的唯一标识符。
  • 初始化虚拟寄存器组。
  • 初始化kvm_vcpu 的状态信息,设置kvm_vcpu 在被调度前需要配置的必要表示。
  • 初始化额外的信息,并且配置apic 等虚拟化组件。

一下是KVM中的vCPU的创建过程。 在获得了fd_vm 之后,通过ioctl 调用KVM_CREATE_VCPU 指令,可以对该fd_vm 对应的虚拟机创建vCPU ,其入口函数地址在 kvm_vm_ioctl 函数中,通过switch 之后,程序流程将选择进入kvm_vm_ioctl_create_vcpu 函数进行处理,其代码如下。

//virt/kvm/kvm_main.c
static long kvm_vm_ioctl(struct file *filp,
			   unsigned int ioctl, unsigned long arg)
{
	struct kvm *kvm = filp->private_data;
	void __user *argp = (void __user *)arg;
	int r;

	if (kvm->mm != current->mm)
		return -EIO;
	switch (ioctl) {
	case KVM_CREATE_VCPU:
		r = kvm_vm_ioctl_create_vcpu(kvm, arg);
		break;
	case KVM_ENABLE_CAP: { }
    /* …………省略多行………… */
	default:
		r = kvm_arch_vm_ioctl(filp, ioctl, arg);
	}
out:
	return r;
}


/*
 * Creates some virtual cpus.  Good luck creating more than one.
 */
static int kvm_vm_ioctl_create_vcpu(struct kvm *kvm, u32 id)
{
	int r;
	struct kvm_vcpu *vcpu;
	struct page *page;

	if (id >= KVM_MAX_VCPU_ID)
		return -EINVAL;

	mutex_lock(&kvm->lock);
	if (kvm->created_vcpus == KVM_MAX_VCPUS) {
		mutex_unlock(&kvm->lock);
		return -EINVAL;
	}

	kvm->created_vcpus++;
	mutex_unlock(&kvm->lock);

	r = kvm_arch_vcpu_precreate(kvm, id);
	if (r)
		goto vcpu_decrement;

	vcpu = kmem_cache_zalloc(kvm_vcpu_cache, GFP_KERNEL);
	if (!vcpu) {
		r = -ENOMEM;
		goto vcpu_decrement;
	}

	BUILD_BUG_ON(sizeof(struct kvm_run) > PAGE_SIZE);
	page = alloc_page(GFP_KERNEL | __GFP_ZERO);
	if (!page) {
		r = -ENOMEM;
		goto vcpu_free;
	}
	vcpu->run = page_address(page);

	kvm_vcpu_init(vcpu, kvm, id);

	r = kvm_arch_vcpu_create(vcpu);
	if (r)
		goto vcpu_free_run_page;

	mutex_lock(&kvm->lock);
	if (kvm_get_vcpu_by_id(kvm, id)) {
		r = -EEXIST;
		goto unlock_vcpu_destroy;
	}

	vcpu->vcpu_idx = atomic_read(&kvm->online_vcpus);
	BUG_ON(kvm->vcpus[vcpu->vcpu_idx]);

	/* Now it's all set up, let userspace reach it */
	kvm_get_kvm(kvm);
	r = create_vcpu_fd(vcpu);
	if (r < 0) {
		kvm_put_kvm_no_destroy(kvm);
		goto unlock_vcpu_destroy;
	}

	kvm->vcpus[vcpu->vcpu_idx] = vcpu;

	/*
	 * Pairs with smp_rmb() in kvm_get_vcpu.  Write kvm->vcpus
	 * before kvm->online_vcpu's incremented value.
	 */
	smp_wmb();
	atomic_inc(&kvm->online_vcpus);

	mutex_unlock(&kvm->lock);
	kvm_arch_vcpu_postcreate(vcpu);
	kvm_create_vcpu_debugfs(vcpu);
	return r;

unlock_vcpu_destroy:
	mutex_unlock(&kvm->lock);
	kvm_arch_vcpu_destroy(vcpu);
vcpu_free_run_page:
	free_page((unsigned long)vcpu->run);
vcpu_free:
	kmem_cache_free(kvm_vcpu_cache, vcpu);
vcpu_decrement:
	mutex_lock(&kvm->lock);
	kvm->created_vcpus--;
	mutex_unlock(&kvm->lock);
	return r;
}

首先,在66行,调用kvm_arch_vcpu_create 函数创建一个kvm_vcpu 结构体。该创建内容与架构相关,因此直接调用kvm_x86_ops 中的create_vcpu 方法执行。在AMD和Intel平台上虽然代码不同,但是实现思路都是类似的:先指定CPUID之后,接着初始化MSR 和 VMCS 等寄存器,最后完成I/O和内存部分寄存器的初始化,为被初次调用运行做好准备。

其次,在上面一段的过程中,会有对kvm_vcpu 中国的数据结构进行初始化的过程,这其中肯定会调用 kvm_x86_ops 中的put_vcpu 函数,实现将vCPU 的参数信息加载入 CPU 中,并执行MMU初始化和CPU复位操作。

在70~74行中,进行了一个检测。如果当前的vCPU创建出来已经加入了某一个已有的KVM主机(if (kvm_get_vcpu_by_id(kvm, id)) ),则将销毁刚才创建的实例。

在76行中,atomic_read(&kvm->online_vcpus) 会获得当前vCPU的数量,但是这里不需要检测是否达到 KVM_MAX_VCPU。

然后,在79~94行,会创建当前vCPU对应的文件描述符 vcpu_fd ,并且将kvm_cpu 添加入KVM 的vCPU数组中。这里特别使用 atom_read 和 atom_inc 宏,这两个宏能够保证在进行KVM 虚拟机的vCPU 添加时按照给定的顺序,不会因为执行中途的中断、进程切换等方式导致添加到不正确的kvm_vcpu数组中。

最后,释放掉所有的内核锁,完成本次vCPU的创建工作。

//arch/x86/kvm/svm/svm.c
static struct kvm_x86_ops svm_x86_ops __initdata = {
	/* …………省略多行………… */
	.vcpu_create = svm_create_vcpu,
	.vcpu_free = svm_free_vcpu,
	.vcpu_reset = svm_vcpu_reset,
    
    .vm_size = sizeof(struct kvm_svm),
	.vm_init = svm_vm_init,
	.vm_destroy = svm_vm_destroy,

	.prepare_guest_switch = svm_prepare_guest_switch,
	.vcpu_load = svm_vcpu_load,
	.vcpu_put = svm_vcpu_put,
	.vcpu_blocking = svm_vcpu_blocking,
	.vcpu_unblocking = svm_vcpu_unblocking,
    /* …………省略多行………… */
};
    
static int svm_create_vcpu(struct kvm_vcpu *vcpu)
{
	struct vcpu_svm *svm;
	struct page *vmcb_page;
	int err;

	BUILD_BUG_ON(offsetof(struct vcpu_svm, vcpu) != 0);
	svm = to_svm(vcpu);

	err = -ENOMEM;
	vmcb_page = alloc_page(GFP_KERNEL_ACCOUNT | __GFP_ZERO);
	if (!vmcb_page)
		goto out;

	err = avic_init_vcpu(svm);
	if (err)
		goto error_free_vmcb_page;

	/* We initialize this flag to true to make sure that the is_running
	 * bit would be set the first time the vcpu is loaded.
	 */
	if (irqchip_in_kernel(vcpu->kvm) && kvm_apicv_activated(vcpu->kvm))
		svm->avic_is_running = true;

	svm->msrpm = svm_vcpu_alloc_msrpm();
	if (!svm->msrpm) {
		err = -ENOMEM;
		goto error_free_vmcb_page;
	}

	svm_vcpu_init_msrpm(vcpu, svm->msrpm);

	svm->vmcb = page_address(vmcb_page);
	svm->vmcb_pa = __sme_set(page_to_pfn(vmcb_page) << PAGE_SHIFT);
	svm->asid_generation = 0;
	init_vmcb(svm);

	svm_init_osvw(vcpu);
	vcpu->arch.microcode_version = 0x01000065;

	return 0;

error_free_vmcb_page:
	__free_page(vmcb_page);
out:
	return err;
}

4.4 vCPU的运行

在创建完VM和vCPU并且完成了初始化工作之后,就可以通过调度程序调度执行。在当前,KVM的调用是从ioctl的KVM_RUN指令字开始的。

PS:在Linux中,KVM VM作为一个系统线程运行,因此KVM VM的调度实际上也就是Linux的调度程序,在当前,KVM的调用是从ioctl的KVM_RUN指令字开始的。

KVM_RUN指令字针对fd_vcpu 描述符操作,当vCPU准备完成之后,即可通过该指令让虚拟机运行起来。

VM 运行的主要任务则是进行上下文切换。上下文切换的内容较多,通常包括通用寄存器、浮点寄存器、段寄存器、控制寄存器、MSR 等,在KVM中,还包括APIC状态、TLB 等。

通常,进行上下文切换的过程可以归纳如下:

  1. KVM 保存自己的上下文。
  2. KVM 通过使用将 kvm_vcpu 结构体中的相关上下文加载到物理 CPU 中。
  3. KVM 执行kvm_x86_ops 中的 run_vcpu 函数,调用具体的平台相关指令,进入虚拟机运行环境中。

由此可见,上下文切换次数过于频繁会带来不小的性能开销。
在这里插入图片描述
执行vCPU 的请求首先发送到kvm_vcpu_ioctl 函数中,然后加载 vCPU参数,调用kvm_arch_vcpu_ioctl_run 函数进入具体的vCPU 运行环境。

  1. 通过调用 sigprocmask 函数,保证在vCPU的初始化过程中,不会因为来自其他线程的信号干扰而中断。
  2. 将 vCPU 的状态切换为 KVM_MP_STATE_UNINITIALIZED
  3. 配置APIC 和 mmio的中断信息。
  4. 对要进入的虚拟机进行一些关键指令的测试,在测试中主要针对内存读/写情况进行测试。
  5. 将vCPU中保存的上下文信息(寄存器状态等)写入指定的位置。
  6. 接下来才开始实质性的工作,调用 _vcpu_run 函数进行后续处理。
//arch/x86/kvm/x86.c
static int vcpu_run(struct kvm_vcpu *vcpu)
{
	int r;
	struct kvm *kvm = vcpu->kvm;

	vcpu->srcu_idx = srcu_read_lock(&kvm->srcu);
	vcpu->arch.l1tf_flush_l1d = true;

	for (;;) {
		if (kvm_vcpu_running(vcpu)) {
			r = vcpu_enter_guest(vcpu);
		} else {
			r = vcpu_block(kvm, vcpu);
		}

		if (r <= 0)
			break;

		kvm_clear_request(KVM_REQ_PENDING_TIMER, vcpu);
		if (kvm_cpu_has_pending_timer(vcpu))
			kvm_inject_pending_timer_irqs(vcpu);

		if (dm_request_for_irq_injection(vcpu) &&
			kvm_vcpu_ready_for_interrupt_injection(vcpu)) {
			r = 0;
			vcpu->run->exit_reason = KVM_EXIT_IRQ_WINDOW_OPEN;
			++vcpu->stat.request_irq_exits;
			break;
		}

		if (__xfer_to_guest_mode_work_pending()) {
			srcu_read_unlock(&kvm->srcu, vcpu->srcu_idx);
			r = xfer_to_guest_mode_handle_work(vcpu);
			if (r)
				return r;
			vcpu->srcu_idx = srcu_read_lock(&kvm->srcu);
		}
	}

	srcu_read_unlock(&kvm->srcu, vcpu->srcu_idx);

	return r;
}
  1. 旧版本,5.10版本中已经没有,可能是在准备前已经实现了)在5232~5240行中,将虚拟APIC 和vCPU 的状态重置。这个操作通过调用 kvm_lapi_reset 和 kvm_arch_vcpu_reset 函数实现。在这里插入图片描述

  2. 在11~15行,正常情况下,kvm_vcpu处于RUNNING的运行状态,然后使得其执行vcpu_enter_guest函数,即物理 CPU 进入了 GUEST 状态并且执行完毕后,才会执行下一步操作。

  3. 在17~22行中,将检查执行本次的一些结果。如果CPU 当前有挂起的定时器或者是其他中断,则会保存该中断现场。

  4. 旧版本,5.10版本中已经没有,可能是不需要了)在5286~5289行中,如果对当前执行的vCPU 需要调度,会引用Linux的进程调度子程序进行一次任务调度。在这里插入图片描述

五、参考链接

非常详细的文档(尚未细看)《KVM虚拟化源代码详解》
字符设备和块设备详解及区别
字符设备总结
Linux Kernel Makefiles编译标志
后续可能会用到的内容
KVM Debugfs接口
KVM API docs

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
liosam是一个开的软件项目,是一个用于iOS平台的音乐播放器。以下是对liosam码的简要讲解。 liosam的码结构清晰,主要分为以下几个模块:音乐播放核心模块、界面显示模块、网络模块和工具模块。 音乐播放核心模块是liosam的核心功能,它负责管理音乐的播放、暂停和切换等操作。这个模块使用了AVFoundation框架,通过AVAudioPlayer来控制音乐的播放。同时,它还提供了一些接口用于获取音乐的时间长度、当前播放时间以及判断音乐是否在播放等功能。 界面显示模块负责展示音乐播放器的界面,包括歌曲封面、歌曲名字和进度条等。这个模块使用了UIKit框架,通过UIImageView和UILabel来展示歌曲的封面和名称,并通过UISlider来实现进度条的显示。同时,它还提供了一些交互功能,如点击按钮播放/暂停音乐,拖动进度条切换音乐进度等。 网络模块负责加载音乐数据,包括从网络上下载音乐文件、解析音乐文件等。这个模块使用了NSURLSession框架,通过发送HTTP请求来获取音乐文件,并通过解析音乐文件的元数据来获取歌曲相关信息。 工具模块是一些辅助功能的集合,它包括一些常用的工具类或方法,如时间转换、文件管理等。这个模块提供了一些方便的方法,使得其他模块可以更方便地进行开发。 总体来说,liosam的实现了一个简单的音乐播放器的功能,并提供了一些扩展接口,方便使用者进行二次开发。通过深入研究和理解liosam的码,我们可以更好地理解iOS音乐播放器的原理和开发方法,并能在此基础上进行自己的项目开发。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值