基于armv8的kvm实现分析(三)kvm初始化流程

news2025/1/20 16:29:15

本文基于以下软硬件假定:

架构:AARCH64

内核版本:5.14.0-rc5

1 kvm概述

  kvm是基于linux内核实现的一种type 2虚拟化方案,它作为内核的一个模块负责虚拟化环境初始化,虚拟机和虚拟cpu模拟,以及IO捕获与转发等功能。在kvm中虚拟机和虚拟cpu分别通过host os的进程和线程实现,并且由host os的调度器对其进行调度。

  由于除了像中断控制器之类的关键设备之外,kvm并不会执行设备模拟工作,因此它通常需要与qemu结合使用。由qemu执行实际的IO设备模拟,以及虚拟机创建和参数配置功能。为此,kvm需要通过ioctl向用户态导出一组虚拟机管理相关的接口,它们之间的关系如下图:

2 初始化总体流程

  kvm初始化的主要目的是为虚拟机的创建和运行提供必要的软硬件环境,其总体流程图如下:

从上图可看出,kvm的初始化流程比较清晰,其主要包含以下几部分:
(1)架构相关的初始化流程
(2)为电源管理接口注册回调函数,以处理kvm在电源管理流程中的行为
(3)为kvm注册字符设备以为用户态提供ioctl接口
(4)其它一些辅助接口

由于架构相关的初始化流程比较复杂,我们将在后面单独用一章进行讨论,因此下面将分别介绍其它的一些流程

2.1 电源管理回调注册

  由于在cpu热插拔和系统休眠唤醒流程中需要执行cpu的offline和online状态转换,因此对于需要控制cpu的相关模块,在这一流程中需要正确管理本模块的cpu状态设置。在电源管理流程中,相关模块可以向电源管理模块注册回调,当对应的电源管理事件发生时,该回调函数将会被调用。

  其中cpuhp_setup_state_nocalls用于注册cpu热插拔时的回调,register_syscore_op用于注册系统休眠唤醒时的回调,而register_reboot_notifier用于注册系统重启时的通知。它们最终都由kvm_arch_hardware_enable和kvm_arch_hardware_disable实现,用于在cpu下线时关闭hypervisor,并在cpu上线时重新初始化hypervisor。

2.2 ioctl接口注册

  从在上一章图中可看到kvm一共为用户态提供了三组ioctl接口:kvm ioctl、vm ioctl和vcpu ioctl。它们分别为用于控制kvm全局、特定虚拟机以及特定vcpu相关的操作。

  其中kvm全局ioctl通过misc_register()接口以字符设备的方式注册,而vm ioctl和vcpu ioctl则通过anon_inode_getfd()接口以匿名inode方式注册。

  Linux中一般的文件都包含一个inode和与若干个其关联的dentry,其中dentry用于表示其在文件系统中的路径。若用户态希望操作该文件时,可通过打开dentry对应的文件名,并获取一个fd。

  但是有些文件操作希望将fd与inode直接关联起来,其文件名不在文件系统中被显示,这就是匿名inode。如vm的匿名inode在下图所示的虚拟机创建流程中建立:

而vcpu的匿名inode同样在vcpu创建流程中建立,其流程如下:

2.3 其它辅助接口

(1)kvm_irqfd_init():为eventfd创建一个全局的工作队列,它用于在虚拟机被关闭时,关闭所有与其相关的irqfd,并等待该操作完成

(2)kmem_cache_create_usercopy()和kvm_async_pf_init()用于创建特定的slab

(3)kvm_init_debug()用于为kvm创建debugfs相关接口

(4)kvm_vfio_ops_init()用于为vfio注册设备回调函数

3 架构相关初始化

  Armv8的虚拟化方案具有两种实现方式nvhe和vhe,在vhe实现中host os和hypervisor都运行在EL2中,此时host os与hypervisor共用所有的EL2寄存器,且host可以直接通过函数调用方式调用hypervisor的接口。因此对于运行在vhe模式下的kvm模块,其初始化流程比较简单,主要包括一些host context的初始化,以及虚拟gic和timer初始化等流程。

  相对而言,vnhe由于host os和hypervisor运行在如下图所示的不同异常等级下,因此其初始化流程更加复杂。如host os需要通过异常的方式进入hypervisor,因此需要在EL2下为hypervisor设置独立的异常处理函数。同时,由于hypervisor的代码运行于el2,还需要为其在该异常等级下映射代码段、数据段等程序的内存地址

3.1 aarch64架构初始化总体流程

  下图为kvm在aarch64下的架构相关初始化流程:

图中黄色部分为vhe和nvhe共同包含的流程,而灰色部分的流程只有nvhe才需要执行。
(1)is_kernel_in_hyp_mode:由于hypervisor运行在EL2,因此该函数通过判断当前是否在EL2中执行,以确定是否处于hypervisor模式。实际上nvhe的kvm实现包含两部分,位于EL1 host中的kvm驱动和位于EL2中的hypervisor,因此对于nvhe实现当前实际还在host驱动中执行,因此其会返回false。而vhe由于host本身就在EL2中执行,故其会返回true

(2)check_kvm_target_cpu:该函数会通过smp_call_function_single()调用分别在每个cpu上执行,用于读取cpu的型号并判断其是否为合法的值。其中smp_call_function_single()函数用于smp核之间的核间通信,其原理为通过向特定cpu发送ipi中断,以使其执行参数给定的回调函数

(3)init_common_resources:该函数读取内存属性寄存器ID_AA64MMFR0_EL1的值,并根据其支持的页大小判断当前系统配置的page size是否能被硬件支持,同时它也会从该寄存器中解析出硬件支持的最大物理地址长度

(4)kvm_arm_init_sve:sve是arm用于支持向量计算的可变长度单指令多数据指令,其特点是向量寄存器长度是可变的。该函数即被用于设置vcpu支持的最大sve向量长度

(5)init_hyp_mode函数将在后面重点介绍

(6)kvm_init_vector_slots:由于cpu具有多级流水线和分支预测功能,在分支预测时可能会将该分支对应的数据提前加载到cache中。一旦分支预测失败,则由于另一分支的数据未被加载到cache,因此其访问速度和预测成功时相比会慢的多。
  因此攻击者可能利用这种执行速度的差异来判断分支预测是否成功,更糟糕的是失败分支的数据依然还位于cache中。因此,攻击者最终可能有机会从cache中获取该数据,从而造成数据泄露。这就是安全界鼎鼎大名的spectre漏洞,其本质是利用侧信道方式攻击cache中的数据。
  vector slots中不同slot中的vector就是根据硬件能力,用于防止不同等级spectre漏洞的向量表集合。当然,其如何预防的具体原理我也没有研究过

(7)init _subsystems:该函数也将在后面重点介绍

(8)finalize_hyp_mode:该函数将与init_hyp_mode一起介绍

(免费订阅,永久学习)学习地址: Dpdk/网络协议栈/vpp/OvS/DDos/NFV/虚拟化/高性能专家-学习视频教程-腾讯课堂

更多DPDK相关学习资料有需要的可以自行报名学习,免费订阅,永久学习,或点击这里加qun免费
领取,关注我持续更新哦! !   

3.2 nvhe特有的初始化流程

  由于armv8异常等级越高具有的权限也越大,因此低异常等级只能通过像smc或hvc之类的异常,通过EL异常处理函数提供的服务。故nvhe若需要支持EL2下的hypervisor,则host os必须要先从EL2启动,并完成EL2异常处理函数设置等基本初始化流程后,才能跳转到EL1中运行。该流程位于下图所示的内核启动阶段(arch/arm64/kernel/head.S):

其代码如下:

SYM_FUNC_START(init_kernel_el)
	mrs	x0, CurrentEL
	cmp	x0, #CurrentEL_EL2                             (1)
	b.eq	init_el2                                       (2)
	…
SYM_INNER_LABEL(init_el2, SYM_L_LOCAL)
	mov_q	x0, HCR_HOST_NVHE_FLAGS
	msr	hcr_el2, x0                                    (3)
	isb

	init_el2_state                                         (4)

	adr_l	x0, __hyp_stub_vectors                       
	msr	vbar_el2, x0                                   (5)
	isb

	mrs	x0, hcr_el2
	and	x0, x0, #HCR_E2H                               
	cbz	x0, 1f                                         (6)

	mov_q	x0, INIT_SCTLR_EL1_MMU_OFF
	msr_s	SYS_SCTLR_EL12, x0

	mov	x0, #INIT_PSTATE_EL2
	msr	spsr_el1, x0
	adr	x0, __cpu_stick_to_vhe
	msr	elr_el1, x0                                    (7)
	eret

1:
	mov_q	x0, INIT_SCTLR_EL1_MMU_OFF
	msr	sctlr_el1, x0

	msr	elr_el2, lr                                        
	mov	w0, #BOOT_CPU_MODE_EL2
	eret                                                   (8)

__cpu_stick_to_vhe:
	mov	x0, #HVC_VHE_RESTART
	hvc	#0
	mov	x0, #BOOT_CPU_MODE_EL2
	ret
SYM_FUNC_END(init_kernel_el)

(1)获取启动时的EL,并判断内核是否从EL2开始启动

(2)若从EL2启动,则调用init_el2初始化el2相关的寄存器

(3)初始化hcr_el2寄存器的值

(4)初始化el2的系统寄存器,如sctlr_el2等

(5)设置el2的初始异常向量表__hyp_stub_vectors

(6)通过hcr_el2.e2h判断是否支持vhe,若支持vhe则跳转到标号1处

(7)对于nvhe模式,el2系统寄存器初始化和stub异常处理函数设置完成。host os可以切换到el1执行,此后若需要进入el2,则只需调用hvc指令进入__hyp_stub_vectors异常处理流程

(8)对于vhe模式,host os继续保持在el2下运行

接下来我们继续看init_hyp_mode函数的实现:

3.2.1 kvm_mmu_init

  我们回忆一下上一节的el2初始化流程只是设置了一些系统寄存器,而并没有为其内存创建页表和开启mmu,因此在hypervisor初始化流程中需要完成该流程。

  本函数的主要目的就是为hypervisor分配页表pgd,并且基于该pgd为hypervisor的identity段建立identity映射。如果看过先前内核启动分析博文的同学,可能还记得在内核初始化时会将开启mmu附近的代码放在一个叫做identity的段中,在建立页表时该段将会映射到与物理地址相同的虚拟地址上,从而保证mmu使能时能平滑切换。

  hypervisor映射也类似,其也包含一个mmu切换相关的identity段,并且也需要建立虚拟地址与物理地址相等的映射关系。以下为代码的实际流程:

int kvm_mmu_init(u32 *hyp_va_bits)
{
	…
	hyp_idmap_start = __pa_symbol(__hyp_idmap_text_start);
	hyp_idmap_start = ALIGN_DOWN(hyp_idmap_start, PAGE_SIZE);
	hyp_idmap_end = __pa_symbol(__hyp_idmap_text_end);                      (1)
	hyp_idmap_end = ALIGN(hyp_idmap_end, PAGE_SIZE);

	hyp_idmap_vector = __pa_symbol(__kvm_hyp_init);

	…
	hyp_pgtable = kzalloc(sizeof(*hyp_pgtable), GFP_KERNEL);
	…
	err = kvm_pgtable_hyp_init(hyp_pgtable, *hyp_va_bits, &kvm_hyp_mm_ops); (2)
	if (err)
		goto out_free_pgtable;

	err = kvm_map_idmap_text();                                             (3)
	…
}

(1)获取identity段的物理地址

(2)初始化hypervisor的pgd

(3)为identity段建立页表

我们再看下其页表建立实现流程

static int kvm_map_idmap_text(void)
{
	…
int err = __create_hyp_mappings(hyp_idmap_start, size, hyp_idmap_start,
			PAGE_HYP_EXEC);
	…
}

从上面的参数可看出其物理地址和虚拟地址的起始值都为hyp_idmap_start

3.2.2 hypervisor的栈和percpu内存分配

该流程比较简单,主要是为所有的vcpu分别分配对应的内存,其流程如下:

for_each_possible_cpu(cpu) {
		unsigned long stack_page;
		stack_page = __get_free_page(GFP_KERNEL);                                 (1)
		…
		per_cpu(kvm_arm_hyp_stack_page, cpu) = stack_page;                        (2)
	}

	for_each_possible_cpu(cpu) {
		…
		page = alloc_pages(GFP_KERNEL, nvhe_percpu_order());      
		…
		page_addr = page_address(page);                                            (3)
		memcpy(page_addr, CHOOSE_NVHE_SYM(__per_cpu_start), nvhe_percpu_size());   (4)
		kvm_arm_hyp_percpu_base[cpu] = (unsigned long)page_addr;                   (5)
	}

(1)为该vcpu的栈分配内存

(2)将其保存到percpu的全局变量中

(3)为该vcpu分配percpu内存并获取其地址

(4)将代码段中的percpu数据拷贝到该地址处

(5)将该地址保存到全局变量中

3.2.3 其它内存的映射

  由于armv8在支持vhe之前,el2下只有一个页表基地址寄存器ttbr0_el2,而ttbr0_el2支持的地址范围为0x0000 0000 0000 0000 - 0x000f ffff ffff ffff。但是hypervisor代码是与内核链接在一起的,我们知道内核链接脚本中定义的虚拟地址位于0xfff0 0000 0000 0000 – 0xffff ffff ffff ffff之间。因此其在映射时需要对内核链接脚本中定义的虚拟地址做一些调整,使其位于ttbr0_el2支持的范围之内。它是在内存映射接口create_hyp_mappings()中通过kern_hyp_va()宏实现的。

最后我们再看一下映射的接口的实现:

static int __create_hyp_mappings(unsigned long start, unsigned long size,
				 unsigned long phys, enum kvm_pgtable_prot prot)
{
	…
	if (!kvm_host_owns_hyp_mappings()) {                                     (1)
		return kvm_call_hyp_nvhe(__pkvm_create_mappings,
					 start, size, phys, prot);               (2)
	}
	mutex_lock(&kvm_hyp_pgd_mutex);
	err = kvm_pgtable_hyp_map(hyp_pgtable, start, size, phys, prot);         (3)
	mutex_unlock(&kvm_hyp_pgd_mutex);
	…
}

  从该流程中可看出根据kvm_host_owns_hyp_mappings 值的不同,hypervisor有两种映射方式:kvm_call_hyp_nvhe和kvm_pgtable_hyp_map。

  原因为在kvm初始化时,hypervisor只能处理__hyp_stub_vectors已经定义的服务,而该函数只实现了一些基本的功能,因此el2自身并不能完成建立页表的能力。此时就要通过step 3的方式由host os帮其先建好页表,然后将该页表的pgd基地址传给el2,并由el2将其设置到ttbr0_el2中。

  当hypervisor初始化完成后,其异常向量表将会被替换为最终运行时的vectors。该vector本身实现了页表创建相关的服务,故在此时可以通过step 2的方式由hypervisor自身完成页表创建功能。其中kvm_call_hyp_nvhe函数的功能即是通过hvc指令陷入el2中

3.2.4 cpu_prepare_hyp_mode

  该函数为每个cpu设置其一些属性相关系统寄存器context的初始值,如tpidr_el2、mair_el2等。并且将前面建立页表的pgd基地址和为vcpu分配的栈指针保存到vcpu的context中。其基本流程如下:

static void cpu_prepare_hyp_mode(int cpu)
{
	struct kvm_nvhe_init_params *params = per_cpu_ptr_nvhe_sym(kvm_init_params, cpu);
	unsigned long tcr;

	params->tpidr_el2 = (unsigned long)kasan_reset_tag(per_cpu_ptr_nvhe_sym(__per_cpu_start, cpu)) -
			    (unsigned long)kvm_ksym_ref(CHOOSE_NVHE_SYM(__per_cpu_start));

	params->mair_el2 = read_sysreg(mair_el1);

	tcr = (read_sysreg(tcr_el1) & TCR_EL2_MASK) | TCR_EL2_RES1;
	tcr &= ~TCR_T0SZ_MASK;
	tcr |= (idmap_t0sz & GENMASK(TCR_TxSZ_WIDTH - 1, 0)) << TCR_T0SZ_OFFSET;
	params->tcr_el2 = tcr;

	params->stack_hyp_va = kern_hyp_va(per_cpu(kvm_arm_hyp_stack_page, cpu) + PAGE_SIZE);
	params->pgd_pa = kvm_mmu_get_httbr();
	if (is_protected_kvm_enabled())
		params->hcr_el2 = HCR_HOST_NVHE_PROTECTED_FLAGS;
	else
		params->hcr_el2 = HCR_HOST_NVHE_FLAGS;
	params->vttbr = params->vtcr = 0;

	kvm_flush_dcache_to_poc(params, sizeof(*params));
}

3.2.5 kvm_hyp_init_protection

该函数的实现如下:

static int kvm_hyp_init_protection(u32 hyp_va_bits)
{
	void *addr = phys_to_virt(hyp_mem_base);
	int ret;

	kvm_nvhe_sym(id_aa64mmfr0_el1_sys_val) = read_sanitised_ftr_reg(SYS_ID_AA64MMFR0_EL1);
	kvm_nvhe_sym(id_aa64mmfr1_el1_sys_val) = read_sanitised_ftr_reg(SYS_ID_AA64MMFR1_EL1);   (1)

	ret = create_hyp_mappings(addr, addr + hyp_mem_size, PAGE_HYP);                          (2)
	if (ret)
		return ret;

	ret = do_pkvm_init(hyp_va_bits);                                                          (3)
	if (ret)
		return ret;

	free_hyp_pgds();                                                                          (4)

	return 0;
}

(1)读取并保存ID_AA64MMFR1_EL0和ID_AA64MMFR1_EL1寄存器的值

(2)为hypervisor的保留内存创建el2页表,该内存由arch/arm64/kvm/hyp/reserved_mem.c中的 kvm_hyp_reserve函数定义

(3)该函数用于执行实际的hypervisor初始化工作,它主要包含以下部分:
  (a)调用hvc异常,将异常处理函数从__hyp_stub_vectors切换为idmap时的处理函数__kvm_hyp_init。该异常处理函数只被用于hypervisor初始化流程
  (b)通过hvc异常调用__kvm_hyp_init函数,以初始化hypervisor。它主要包括初始化sp、hcr_el2等系统寄存器,设置前面创建的页表并使能mmu。最后将异常处理函数切换到最终工作时使用的版本__kvm_hyp_host_vector
  (c)由于el2的初始化页表是使用内核内存建立的,而实际上step 2已经为el2保留了页表相关的内存。因此__pkvm_init会使用该内存作为页表重新为el2建立页表,并将其页表切换到新的位置

以下为其代码流程图:

(4)由于上一步已经使用新的内存为hypervisor重新建立的页表,故初始化时建立的页表不再需要,因此可释放该内存

3.3 subsystem初始化流程

subsystem初始化流程图如下:

  它主要包括对cpu、vgic和timer的初始化流程。由于前面只初始化了当前运行cpu的hypervisor,而对于smp系统实际每个cpu都需要为其执行初始化流程。因此在本函数中通过向所有cpu发送ipu中断以执行_kvm_arch_hardware_enable操作,其主要目的包括为el2设置正确的异常里程序,以及初始化vgic的list register值。其中list register是一组gic用于向vcpu注入虚拟中断的寄存器

  vgic初始化流程主要是将vgic设备注册到kvm中,并为其实现一组回调函数。这组回调函数可被用于操作vgic相关组件,如vgic_v3_set_attr可用于设置vgic中虚拟distributor、redistributor等的寄存器,而vgic_v3_get_attr可用于读取相关寄存器的值

  timer初始化流程主要是为每个cpu注册vtimer和ptimer的ppi中断,并在ppi中断处理函数中将该中断以虚拟中断方式注入给vcpu。该流程相对比较简单,故不再赘述

4 小结

本文主要介绍了kvm初始化相关流程,其主要包含以下内容:
(1)kvm所有架构共同流程的初始化

(2)armv8架构中nvhe和vhe共同流程的初始化

(3)armv8架构nvhe特有流程的初始化

在初始化流程完成后,用户态就可以通过kvm导出的ioctl接口执行虚拟机创建,vcpu创建及运行等操作了

原文链接:https://zhuanlan.zhihu.com/p/530130205

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/26467.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

发布适用于 .NET 7 的 .NET MAUI

我们在六个月前向您介绍了 .NET 多平台应用程序 UI (MAUI)&#xff0c;现在我们很高兴地宣布 .NET MAUI 在我们的下一个主要版本 .NET 7 中普遍可用。在此短的时间范围内&#xff0c;我们在 .NET MAUI 中的主要工作是解决您的主要反馈报告、改进 CollectionView 的性能&#xf…

P8842 [传智杯 #4 初赛] 小卡与质数2 垃圾筛

题目&#xff1a; 思路&#xff1a; 首先排除比较每一个比X小的数去看结果&#xff0c;因为一定会tle 然后考虑去和每一个比X小的数去看结果&#xff0c;去判定是否比它小&#xff0c;看起来是优秀了一些&#xff0c;但是 n以内的质数比例大约是1ln(n)\frac{1}{ln(n)}ln(n)1​…

MCE | 线粒体和能量代谢的关系

线粒体是细胞生命活动的能量工厂&#xff0c;是几乎所有真核生物都存在的一种细胞器。它的主要功能是进行氧化磷酸化 (OXPHOS) 合成 ATP&#xff0c;是糖类、脂肪和氨基酸等物质的最终氧化释放能量的场所。自带“内核”和“核心技术”的线粒体&#xff1a;线粒体内有一套独立的…

JAVA中使用最广泛的本地缓存?Ehcache的自信从何而来3 —— 解析Ehcache的各种集群方案,本地缓存如何变身分布式缓存

大家好&#xff0c;又见面了。 上一篇文章中&#xff0c;我们知晓了如何在项目中通过不同的方式来集成Ehcache并在业务逻辑中进行使用。作为JAVA本地缓存框架综合实力天花板级别的Ehcache&#xff0c;除了在本地缓存方面具有强悍的实力外&#xff0c;还具有一个其它对手所不具…

Metabase学习教程:提问-6

搜索表格和问题 了解如何使用筛选器和自定义表达式在SQL查询和简单问题中进行搜索。在表格中查找单词或短语现在比以往任何时候都容易。 添加过滤器给你的问题可以让你的问题搜索文本轻而易举。您可以使用示例数据库包含在每个Metabase安装中。 在问题中搜索 我们将从单击浏…

linux笔记(8):东山哪吒D1H移植lvgl(HDMI输出)

文章目录1. 下载&#xff0c;修改&#xff0c;编译源码1.1下载源码1.1.1新建一个lvgl目录&#xff0c;在该目录下下载源码1.1.2 在lvgl目录下再建一个myspace/lvgl_demo目录&#xff0c;把参与编译的文件拷贝到本目录1.2 修改源码1.3编译源码2.拷贝到东山哪吒开发板运行3.已移植…

BUUCTF Web 极客大挑战 2019 EasySQL

BUUCTF Web 极客大挑战 2019 EasySQL 文章目录BUUCTF Web 极客大挑战 2019 EasySQL1&#xff0c;输入万能密码&#xff1a;2&#xff0c;输入万能账号首先有点常识&#xff1a;正常SQL语句这样子写&#xff1a;select * from user where username XXX and password XXX&#x…

MySQL函数

函数的理解 什么是函数 函数在计算机语言的使用中贯穿始终&#xff0c;函数的作用是什么呢&#xff1f;它可以把我们经常使用的代码封装起来&#xff0c;需要的时候直接调用即可。这样既 提高了代码效率 &#xff0c;又 提高了可维护性 。在 SQL 中我们也可以使用函数对检索出…

prompt(1) to win -xss学习

网址 https://prompt.ml/level 0 (闭合) function escape(input) {// warm up// script should be executed without user interactionreturn <input type"text" value" input ">; } 闭合前面的双引号 "><img src1 onerrorprompt(1)…

EasyPoi——导出导入表格数据工具

文章目录文档概念环境搭建&#xff08;maven&#xff09;工具类主要使用注解Excel 主要用的注解实战实体类&#xff08;部分&#xff09;导出Controller层导出结果导入service层controller层文档 官方文档 概念 Easypoi主打的功能就是容易&#xff0c;让一个没见接触过poi的…

java每日一练 (1)

java每日一练 &#xff08;1&#xff09; 单选部分 1.在 Java 中&#xff0c;存放字符串常量的对象属于&#xff08; &#xff09;类对象。   A Character B String C StringBuffer D Vector 答案 &#xff1a; B 复习文章 &#xff1a; String类 — 上篇_ 2.图示 &#xff…

C#在Pdf画统计图表之【雷达图】(以五边形为例)

前言 工具&#xff1a;PdfSharpCore或Pdfsharp 知识 &#xff1a;基本的三角函数 思路 &#xff1a; 画坐标&#xff0c;确定点-------->画多个半径渐变的正多边形------->根据数据确定雷达图的数值位置 StepOne:画一个正五边形 先画一个辅助坐标轴&#xff0c; 再通过…

40多行实现一个非常简单的shell

目录简单的shell的实现附代码简单的shell的实现 目的&#xff1a;主要就是为了加深对shell的底层原理的理解 可以通过接口可以获取到这些主机名等等这里直接就用字符串打印了 当我们打开一个c文件默认就打开了三个输入输出流&#xff1a;stdin(标准输入),stdout(标准输出),st…

一种具有肤质保留功能的磨皮算法

基本原理 1、复制原图Src作为HighPass层&#xff1a;HighPass Copy(Src)。 2、对HighPass层磨皮&#xff08;就是进行保边滤波&#xff0c;可以选择表面模糊、导向滤波、双边滤波、各向异性扩散、BEEP、局部均方差、Domain transfer、 Adaptive Manifolds、 Local Laplacian …

产品经理学习和认证PMP

PMP的功效 PMP是由美国项目管理协会&#xff08;PMI&#xff09;发起的项目管理认证考试&#xff0c;以项目管理知识体系&#xff08;PMBOK&#xff09;为考试基准。经过实际分析和总结&#xff0c;PMP具有如下几种功效&#xff1a; 首先&#xff0c;PMP是国际认可度最高的项目…

Essay写作论证基本的五要素讲解

对于出国留学而言的学生来说&#xff0c;Essay写作是非常重要的。每个学期导师都会布置下Essay课业&#xff0c;如果你的写作能力不够的话&#xff0c;是很难写出让导师满意的Essay来&#xff0c;Essay的写作更是如此。因为Essay的写作难度本身就比普通Essay难度大得多&#xf…

viewerjs -v 11 动态获取图片(ajax),以及重复初始化问题。

&#xff08;1&#xff09;viewerjs 源码&#xff0c;以及官方文档 ​​​​​i​​​​​​​​​​​​​https://github.com/fengyuanchen/viewerjs/blob/main/README.mdhttps://github.com/fengyuanchen/viewerjs/blob/main/README.md &#xff08;2&#xff09;静态图片…

免杀Veil-evasion

● 属于Veil-framework框架的一部分 ● 由Python语言编写 ● 用于自动生成免杀payload ○ 集成msf payload&#xff0c;支持自定义payload ○ 集成各种注入技术 ○ 集成各种第三方工具 ■ Hypersion、PEScrambler、BackDoor、Factory ○ 继承各种开发打包运行环境 ■ Python&am…

企业投放电视广告的正确姿势是什么

越来越多的互联网企业开始选择投放电视广告。 互联网革了电视以外的所有传统媒体的命。传统报刊、杂志大部分淡出人们的视野&#xff0c;报刊亭也在逐渐的减少直至关停&#xff0c;而只有电视媒体屹立不倒——其媒体价值非但没有降低&#xff0c;反而在逐年加大。电视媒体热播…

【模型训练】yolov7猫狗检测

yolov7猫狗检测 1、猫狗检测模型训练2、模型评估3、模型和数据集下载网盘链接1、本项目采用YOLOv7算法实现对猫狗检测,在几千多张猫狗检测数据集中能训练得到,我们训练了YOLOv7、,所有指标都是在同一个验证集上得到; 2、目标类别数:2 ;类别名:cat、dog; 3、本项目提供了…