虚拟化技术分析--基础篇

第二次作业(一)

第一题

  1. 系统虚拟化体系结构分为宿主型和独立监控型两种类型,请你分析一下这两种类型体系结构,并分别举例说明现有主流虚拟化软件的体系结构属于哪种类型。

虚拟化分类

计算机系统包括五个抽象层:硬件抽象层,指令集架构层,操作系统层,库函数层和应用程序层。相应地,虚拟化可以在每个抽象层来实现。无论是在哪个抽象层实现,其本质都是一样的,那就是它使用某些手段来管理分配底层资源,并将底层资源反映给上层。
请添加图片描述
指令级虚拟化又称为指令集架构级虚拟化(ISA虚拟化)。它通过纯软件方法,模拟出与实际运行的应用程序(或操作系统)不同的指令集去执行,采用这种方法构造的虚拟机一般称为模拟器(Emulator)。模拟器可以将客户虚拟机发出的所有指令翻译成本地指令集,然后在真实的硬件上执行。这些指令包括典型的处理器指令和特殊的I/O指令。

硬件抽象层(Hardware Abstraction Layer,HAL)虚拟化实际上与指令集架构级虚拟化非常相似,其不同之处在于,这种类型的虚拟化所考虑的是一种特殊情况:客户执行环境和主机具有相同指令集的情况,并充分利用这一特点,让绝大多数客户指令在主机上直接执行,从而大大提高了执行的速度。

VMW分类

目前,由于x86指令集的广泛运用,硬件级虚拟化是最为流行的虚拟化层次,业界最具影响力的VMware和Xen都属于硬件级虚拟化的范畴。有了虚拟机VM模拟出独立的、ISA结构和实际硬件相同的虚拟硬件系统,我们还需要在真实的操作系统中管理这些VM,这就是虚拟机监视器VMM(hypervisor)

一个计算机,上面运行着一个hypervisor,hypervisor上面又运行着一个或多个虚拟机,该计算机被称为host machine,每一个虚拟机被叫做Guest machine。hypervisor为Guest OS营造了一个虚拟的操作系统,并且对Guest OS的运行进行管理。Guest OS通过虚拟机监控器提供的抽象层来实现对物理资源的访问和操作。

目前存在各种各样的虚拟机,但基本上所有虚拟机都基于"计算机硬件 + 虚拟机监视器(VMM)+ 客户机操作系统(Guest OS)"的模型。从系统架构看,VMM是一个位于计算机硬件和操作系统之间的软件层,运行在特权级,并且为上层的虚拟机提供多个隔离的执行环境,为这些虚拟机提供安全、独立的运行环境。虚拟机监控器可以将运行在不同物理机器上的操作系统和应用程序合并到同一台物理机器上运行,减少了管理成本和能源损耗,并且便于系统的迁移。

根据VMM在整个物理系统中的实现位置,可以将VMM分为独立监控模式、宿主模式和混合模式。
请添加图片描述

独立监控模式(Hypervisor Model)

在该模式下,VMM直接运行在裸机上,可以使用和管理底层的硬件资源,具有最高的特权级,并向VM内的Guest OS提供抽象的底层硬件,而所有的Guest OS则运行在较低的特权级上,VMM可以截获所有Guest OS对系统资源的访问,Guest OS对真实硬件资源的访问都要通过VMM来完成。这种模型又称为Type-I型虚拟机监控器。

独立监控模式常用于大型服务器上搭建虚拟机,由于大型服务器对于操作系统的需求不是很高,对服务器的操作靠一些配置命令即可完成,更多的是需要在服务器上运行大型的程序,而非使用服务器的操作系统。因而独立监控模式在大型服务器上代替了普通操作系统地位,例如VMware ESX服务器(一种能直接在硬件上运行的企业级的虚拟平台)使用的就是独立监控模式,在服务器上虚拟多台虚拟机,供用户使用。

采用该模型的虚拟化平台有Wind River的Hypervisor 2.0, VMware ESXi, Xen等。

宿主模式(Host-based Model)

该模型中,虚拟机监控器作为一个应用程序运行在宿主机操作系统(Host OS)上,而Guest OS运行于虚拟机监控器之上。Guest OS对底层硬件资源的访问要被虚拟机监控器拦截,虚拟机监控器再转交给Host OS进行处理。该模型中,Guest OS对底层资源的访问路径更长,故而性能相对独立监控模型有所损失。但优点是,虚拟机监控器可以利用宿主机操作系统的大部分功能,而无需重复实现对底层资源的管理和分配,也无需重写硬件驱动。

宿主模式对于普通用户较为容易使用,因为操作宿主模式的VMM就像平时打开一个软件一样,有完善的UI界面,对于不太熟悉虚拟化的具体细节,只是需要搭建虚拟机的普通人来讲,宿主模式简单易用,例如非常流行的VMware Workstation就是宿主模式。

混合模式

混合模式结合了独立监控模式和宿主模式的优点。

该模型中,虚拟机监控器直接运行在物理机器上,具有最高的特权级,所有虚拟机都运行在虚拟机监控器之上。与Type-I型虚拟化模型不同的是,这种模型中虚拟机监控器不需要实现硬件驱动,甚至虚拟机调度器等部分虚拟机管理功能,而把对外部设备访问、虚拟机调度等功能交给一个特权级虚拟机(RootOS、Domain 0、根操作系统等)来处理。特权级虚拟机可以管理其它虚拟机和直接访问硬件设备,只有虚拟化相关的部分,例如虚拟机的创建/删除和外设的分配/控制等功能才交由虚拟机监视控制。

混合模式相较于前2种模式有着优势互补的特点,既有比较好的性能也有良好的用户交互界面。但是此类虚拟机的配置较前2种模式而言复杂一些,因为用户需要先安装一个完善的操作系统,然后安装VMM后对系统启动流程进行更改,赋予VMM高权限,通过VMM来管理硬件。同时用户需要良好地处理特权VM和普通VM之间的关系,如果特权VM损坏,就会引起整个虚拟化环境的损坏。

采用该模型的虚拟化平台有Linux KVM、Jailhouse等。

第二题

  1. 请你分析一下系统虚拟化实现技术中全虚拟化、泛虚拟化和硬件辅助虚拟化技术,比较一下各自优缺点,并分别举例说明现有主流虚拟化软件实现技术属于哪种类型。

x86的虚拟化技术

经典的虚拟化技术采用的是“特权解除”(Privilege Deprivileging)和“陷入-模拟”(Trap-and-emulation)技术。“特权解除”是为了实现VMM对虚拟机的控制,降低Guest OS运行的权限级别,而将VMM运行在最高特权级的技术。

在解除了Guest OS的特权之后,Guest OS的大部分指令还是能在硬件上直接运行的,但是当执行到某些关系到CPU状态的特权指令时,会“陷入”到最高特权级的VMM来模拟执行这些特权指令,既实现“陷入-模拟”。

但是,目前流行的x86架构并不能适用于这种模型,严格来说,他并不是一种支持虚拟化的架构,在架构上的缺陷给他的虚拟化实现带来了巨大的困难和挑战。在x86架构中,其指令集中有一部分敏感指令不是特权指令(例如PUSH和POP),能在低特权级下运行,无法引起陷入。

由于x86的这种缺陷,长期以来对x86架构的虚拟化都是通过软件方式实现的。根据实现方式中是否需要修改Guest OS内核,可分为全虚拟化(Full Virtualization)和半虚拟化(Paravirtualization)两种。2005年Intel发布了处理器级的虚拟化技术,即VT技术后,x86的两大生产商Intel和AMD分别扩展了x86的指令集,使x86硬件也能支持“陷入-模拟”方式的虚拟化。于是有了第三种选择——硬件虚拟化(Hardware Virtualization)技术。

全虚拟化

在全虚拟化下,VMM可以向虚拟机虚拟出和硬件完全相同的硬件环境,为每个虚拟机提供完整的硬件支持服务,包括虚拟BIOS、虚拟设备和虚拟内存管理等。整个过程不需要硬件或Guest OS的协助,不需要修改Guest OS的内核,Guest OS完全感知不到是否发生了虚拟化。VMM以纯软件的方式来弥补x86的虚拟化缺陷。

原本的操作系统调直接调用硬件接口,给底层硬件发送的指令是二进制指令,加了一层虚拟机之后,操作系统不能直接调用底层,但依然发送二进制指令,这时候,虚拟机要拦截下指令,由虚拟机完成调用,所以虚拟机要转换二进制指令。优点在于为原始硬件设计的操作系统或其它系统软件完全不做任何修改就可以直接运行在全虚拟化的虚拟机监控器上,兼容性非常好。但是采用该技术的虚拟机监控器模拟了完整的底层硬件,并且需要额外的指令翻译,从而使得模拟过程比较复杂,导致效率较为低下。

在全虚拟化下,CPU虚拟化采用的是二进制代码动态翻译(Dynamic Binary Translation)技术,即在执行时动态地重写虚拟机自省代码,并在需要VMM监控和模拟的位置插入陷入指令。这种方式不需要修改Guest OS,但是这种动态翻译会带来一定的性能开销。

泛虚拟化(Paravirtualization)

泛虚拟化技术又称为半虚拟化技术、准虚拟化技术、协同虚拟化技术或者超虚拟化技术,是指通过暴露给Guest OS一个修改过的硬件抽象,将硬件接口以软件的形式提供给客户机操作系统。

在泛虚拟化下,VMM需要Guest OS的协助才能够完成对x86敏感特权指令的虚拟化。泛虚拟化通过修改Guest OS内核代码,将敏感指令的操作替换为对VMM的超级调用(Hypercall),通过VMM来模拟执行这些指令。由于需要对Guest OS的内核进行修改,以便操作系统能能够自行对有缺陷的指令进行替换,泛虚拟化也可以成为操作系统协助虚拟化(OS Assisted Virtualization)。在这种情况下,Guest OS是知道自己运行在虚拟机中的。

半虚拟化的优点是降低了虚拟化技术带来的性能开销,主要表现在消减代码冗余减少特权级别转换和减少内存复制。并且半虚拟化技术修改了Guest OS与虚拟机监控器协同工作,使得虚拟机监控器可以得知Guest OS内部的一些状态,消除了黑盒调度带来的一些问题。但缺点在于需要对Guest OS内核进行修改,因此对不开源的操作系统(如Windows系统)就很难支持。

硬件虚拟化(Hardware-Assisted Virtualization)

硬件辅助虚拟化技术是指借助硬件(CPU、芯片组以及I/O设备等)的虚拟化支持来实现高效的全虚拟化。原有的硬件体系结构在虚拟化方面存在虚拟化漏洞等缺陷,导致单纯的软件虚拟化方法存在一些问题;还有就是由于硬件架构的限制,某些功能即使可以通过软件的方式来实现,但是实现过程却异常复杂,甚至带来性能的大幅下降,这主要体现在以软件方式实现的内存虚拟化和I/O设备虚拟化。通过在硬件中加入专门针对虚拟化的支持,系统虚拟化的实现变得更加容易和高效。

硬件虚拟化始于两大CPU厂商Intel和AMD分别提出的Intel-VT和AMD-V技术,其基本思想是引入新的指令和处理器运行模式,使VMM进行监控和模拟时,硬件支持模式切换。Intel和AMD对x86架构CPU的改进使x86硬件也能支持“陷入-模拟”方式的虚拟化。这就意味着,不需要对敏感特权指令进行翻译或对Guest OS内核的修改就能很轻松地完成对CPU的虚拟化工作。硬件虚拟化提供了全新的架构,简化了VMM的设计和实现,显著提高了其对虚拟机的掌握灵活度和性能。

优点在于特权指令调用之时,不需要半虚拟化更改操作系统内核,也不需要二进制转换,因为有了硬件的支持。缺点:需要有硬件支持(如Intel VT, AMD SVM)。

第三题

  1. 请你简要描述一下Xen虚拟化系统中基于额度的调度算法思想,并通过Xen3.0以上版本的源代码分析,给出其中对应调度算法的主要工作流程、实现的具体功能。

Xen加载调度策略的过程

Xen作为虚拟机管理层,需要对各个VM进行调度,调度通过将各个VM上的VCPU轮流加载到PCPU上来进行。

/* 调度策略与调度器相分离分别在不同的文件中 */
extern const struct scheduler sched_sedf_def;/* 调度策略 */
extern const struct scheduler sched_credit_def;
/* 调度器,与其包含的调度策略 */
static const struct scheduler *__initdata schedulers[] = {
    &sched_sedf_def,
    &sched_credit_def,
    NULL
};

/* 默认的调度策略"credit" */
static char __initdata opt_sched[10] = "credit";
string_param("sched", opt_sched);

/* 根据调度策略初始化调度器 */
void __init scheduler_init(void)
{
    int i;
    open_softirq(SCHEDULE_SOFTIRQ, schedule);
    for_each_possible_cpu ( i )    {
        spin_lock_init(&per_cpu(schedule_data, i).schedule_lock);
        init_timer(&per_cpu(schedule_data, i).s_timer, s_timer_fn, NULL, i);
    }
    for ( i = 0; schedulers[i] != NULL; i++ )    {
        ops = *schedulers[i];
        if ( strcmp(ops.opt_name, opt_sched) == 0 )
            break;
    }  
    if ( schedulers[i] == NULL )    {
        printk("Could not find scheduler: %s\n", opt_sched);
        ops = *schedulers[0];
    }
    printk("Using scheduler: %s (%s)\n", ops.name, ops.opt_name);
    SCHED_OP(init);
}

调度器的运行

static void schedule(void)
{
    /* 设置两个VCPU的指针prev和next,其中prev指向当前的VCPU */
    struct vcpu          *prev = current, *next = NULL;
    s_time_t              now = NOW();
    struct schedule_data *sd;
    struct task_slice     next_slice;

    ASSERT(!in_irq());
    ASSERT(this_cpu(mc_state).flags == 0);
    perfc_incr(sched_run);
    sd = &this_cpu(schedule_data);
    spin_lock_irq(&sd->schedule_lock);
    stop_timer(&sd->s_timer);
    
    /* 通过指定策略的得到需要调度的VCPU,将下一个要调度的VCPU用next指向它 */
    next_slice = ops.do_schedule(now);

    next = next_slice.task;

    sd->curr = next;
    /* 根据下一个VCPU调度的时间设定定时器,等待下次调度 */
    if ( next_slice.time >= 0 ) 
        set_timer(&sd->s_timer, now + next_slice.time);

    ...

    context_switch(prev, next);/* 切换prev和next两个VCPU的上下文,结束调度 */
}

Credit调度策略

Credit调度策略会为每一个guest操作系统设置一个二元数组,包括权重weight和上限cap。各个系统的权重weight所占的比例代表该系统可以占用CPU时间片的比例, 上限cap决定该系统可以占用CPU的数量上限,即使主机具有空闲的 CPU 周期也是如此。比如cap=100,则vcpu占一个物理cpu,cap=200,vcpu占两个物理cpu。默认值 0 表示没有上限。

  • Credit调度策略将每一个虚拟CPU(VCPU)分到两个队列,under和over队列。Under队列负责按照weight权重正常调度,但是每次调度后都减小信任值credit。信任值为负的任务被移入over队列, 不再进行调度。若所有的虚拟CPU都处于over队列时,调度算法调整所有的credit为其加上最初的credit值,然后把虚拟CPU重新加入到under队列。如此反复。
  • Credit调度策略每隔 30 毫秒的时间段进行 CPU 分配。虚拟机 (VCPU) 在被抢占以运行另一个虚拟机之前接收 30 毫秒。每 30 毫秒重新计算一次所有可运行 VM 的优先级(配额)。
    请添加图片描述

Credit调度策略工作流程

scheduler 结构中,由 do_schedule 指向的函数非常重要。当需要做出一个调度 决定时(比如:VCPU 时间片用完,外部发生了一个中断信号等),该函数就会被调用,上下文切换也就发生了。该函数在credit算法中是如何实现的呢?

/*优先级 */
#define CSCHED_PRI_TS_BOOST      0      /* time-share waking up */
#define CSCHED_PRI_TS_UNDER     -1      /* time-share w/ credits */
#define CSCHED_PRI_TS_OVER      -2      /* time-share w/o credits */
#define CSCHED_PRI_IDLE         -64     /* idle */
/* Scheduling Domains 调度域 */
struct csched_dom {
    struct list_head active_vcpu;
    struct list_head active_sdom_elem;
    struct domain *dom;
    uint16_t active_vcpu_count;
    uint16_t weight;/* 对应的weight */
    uint16_t cap;/* 对应的cap */
};
/* 调度过程函数 */
csched_schedule(s_time_t now)
{
    const int cpu = smp_processor_id();
    struct list_head * const runq = RUNQ(cpu);
    struct csched_vcpu * const scurr = CSCHED_VCPU(current);
    struct csched_vcpu *snext;
    struct task_slice ret;

    CSCHED_STAT_CRANK(schedule);
    CSCHED_VCPU_CHECK(current);

    /* 
    * Update credits 
    * 首先判断了是否是空闲VCPU,如果不是则进行正常调度,
    * 通过burn_credits()来消耗该进程的credit值。若是VCPU则恢复他的优先级。 
    */
    if ( !is_idle_vcpu(scurr->vcpu) )
    {
        burn_credits(scurr, now);
        scurr->start_time -= now;
    }

    /*
     * 判断该VCPU是否是还需运行,
     * 如果还需要运行,则将该代码插入到CPU链表中从大到小的合适位置。
     * 插入后,snext指向调度的下一个VCPU
     * Select next runnable local VCPU (ie top of local runq)
     */
    if ( vcpu_runnable(current) )
        __runq_insert(cpu, scurr);/* 将当前的vcpu插入队列中合适的位置 */
    else
        BUG_ON( is_idle_vcpu(current) || list_empty(runq) );

    snext = __runq_elem(runq->next);/* 调度的下一个VCPU */

    /*
     * SMP Load balance 负载均衡:
     * 如果下一个最高优先级的VCPU已经用光了credits,看看其他pcpus有没有更紧急的工作
     * 没有的话,csched_load_balance() 将会返回snext,返回前该节点从队列移除
     */
    if ( snext->pri > CSCHED_PRI_TS_OVER )
        __runq_remove(snext);
    else
        snext = csched_load_balance(cpu, snext);

    /*
     * Update idlers mask if necessary. When we're idling, other CPUs
     * will tickle us when they get extra work.
     */
    if ( snext->pri == CSCHED_PRI_IDLE )
    {
        if ( !cpu_isset(cpu, csched_priv.idlers) )
            cpu_set(cpu, csched_priv.idlers);
    }
    else if ( cpu_isset(cpu, csched_priv.idlers) )
    {
        cpu_clear(cpu, csched_priv.idlers);
    }

    if ( !is_idle_vcpu(snext->vcpu) )
        snext->start_time += now;

    /*
     * Return task to run next...
     */
    ret.time = (is_idle_vcpu(snext->vcpu) ?/* 下一个VCPU需要的时间片 */
                -1 : MILLISECS(CSCHED_MSECS_PER_TSLICE));
    ret.task = snext->vcpu;/* 下一个需要调度的VCPU */

    CSCHED_VCPU_CHECK(ret.task);
    return ret;
}

第四题

  1. 请你简要描述一下MMU泛虚拟化、影子页表技术,以及基于硬件辅助内存虚拟化技术,比较一下他们的异同处,并分析自己所部署虚拟化环境的内存映射情况。

MMU与虚拟化

由于内存是计算机访问最频繁的设备之一,内存虚拟化的效率将对虚拟机的性能产生重大影响。现代计算机通常都采用段页式储存光里、多级页表等复杂的储存体系结构,计算机需要把进程使用的线性地址转换为实际内存上的物理地址,再对内存进行相应的操作。而这项工作是由硬件中的内存管理单元(Memory Management Unit,MMU)来实现的。
请添加图片描述

MMU利用进程提供的线性地址查询页目录和页表,找到对应的机器物理地址,将物理地址送到计算机地址总线上。但是由于虚拟化技术的引入,经过MMU转换所得到的“物理地址”已经不是真正硬件上的物理地址。请添加图片描述

如上图所示,而VMM与客户机操作系统在对物理内存的认识上存在冲突,这使得真正拥有物理内存的VMM必须对客户机操作系统所访问的内存进行一定程度的虚拟化。换句话说,就是VMM 负责将MMU进行虚拟化,为客户机操作系统提供一段连续的“物理”地址空间,而操作系统本身不会意识到这种变化,仍能够将虚拟机虚拟地址(Guest Virtual Address,GVA)映射到虚拟机物理地址(Guest Physical Address,GPA),但是需要VMM将虚拟机物理地址映射到物理机物理地址(Host Physical Address,HPA)。

但是,由于内存的读写频率非常高,如果每次内存操作都需要VMM的介入,那么整个虚拟机运行效率会非常低。因此必须利用现有的MMU机制,实现虚拟机中线性地址到真实内存的物理地址的一次性转换。

MMU泛虚拟化

这种方式主要为Xen所用,MMU半虚拟化主要原理是:

  1. 当Guest OS创建新页表时,VMM从维护的空闲内存中为其分配页面并进行注册。后续,Guest OS对该页表的写操作都会陷入VMM进行验证和转换;VMM检查页表中的每一项,确保它们只映射到属于该虚拟机的机器页面,而且不包含对页表页面的可写映射。
  2. 然后,VMM会根据其维护的映射关系PA-MA,将页表项中的虚拟机逻辑地址VA替换为相应的机器地址MA。
  3. 最后把修改过的页表载入MMU,MMU就可以根据修改过的页表直接完成虚拟地址VA到机器地址MA的转换。

这种方式的本质是将映射关系VA-MA直接写入Guest OS的页表中,以替换原来的映射VA-PA映射关系。说人话就是VMM根据虚拟机中的页表和虚拟机中虚拟内存与真实内存之间的映射关系,制作了一张新的页表,这个页表直接反应了进程线性地址到真实内存地址之间的映射关系,并用这张表替换掉虚拟机中的页表。对于Guest OS而言,他能通过这张新的页表,直接获得真正的物理地址。
请添加图片描述

使用这种方式对内存进行虚拟化时,需要注意的是Guest OS能够获得真正的物理地址信息,因此为了保证各个虚拟机内存空间的独立,要保证交给虚拟机的真实物理地址是各不相同的。同时,对于真实机器本身敏感的内存地址,不能交给虚拟机让虚拟机操作。

影子页表

相比较MMU半虚,大部分虚拟化厂商在VMM中还使用了一种称为影子页表(Shadow Page Table)的技术实现上述功能。对于每个虚拟机的主页表(Primary Page Table),VMM都维持一个影子页表来记录和维护GVA与HPA的映射关系。

影子页表技术的思想和MMU半虚拟化比较相似,都是通过改变交给MMU的地址映射关系表,来完成地址的一次性转换。但是与MMU半虚拟化中直接改变虚拟机里的映射关系表不同,全虚拟化和硬件虚拟化技术是由VMM为虚拟机维护一份影子页表,影子页表储存在VMM中,并在虚拟机对内存进行访问时由VMM将影子页表交给MMU,完成地址转换。
请添加图片描述

影子页表包含从虚拟机逻辑地址到虚拟机物理地址的映射关系(由虚拟机操作系统维护)和虚拟机物理地址到物理机物理地址的映射关系(由VMW维护),通过这种两级映射的方式,VMM为Guest OS的每个页表维护一个影子页表,并将GVA-HPA的映射关系写入影子页表,Guest OS的页表内容保持不变,然后,VMM将影子页表写入MMU。同时,又对虚拟机可访问的内存边界进行了有效控制。并且,使用TLB缓存影子页表的内容可以大大提高虚拟机问内存的速度。

影子页表的维护将带来时间和空间上的较大开销。时间开销主要体现在Guest OS构造页表时不会主动通知VMM,VMM必须等到Guest OS发生缺页错误时(必须Guest OS要更新主页表),才会分析缺页原因再为其补全影子页表。而空间开销主要体现在VMM需要支持多台虚拟机同时运行,每台虚拟机的 Guest OS通常会为其上运行的每个进程创建一套页表系统,因此影子页表的空间开销会随着进程数量的增多而迅速增大。

为权衡时间开销和空间开销,现在一般采用影子页表缓存(Shadow Page Table Cache)技术,即VMM在内存中维护部分最近使用过的影子页表,只有当影子页表在缓存中找不到时,才构建一个新的影子页表。当前主要的虚拟化技术都采用了影子页表缓存技术。

异同

相同点:都是通过改变交给MMU的地址映射关系表,来完成地址的一次性转换。

不同点:虚拟地址到机器地址的转换执行者不同,一个是Guest OS,一个是VMM。影子页表的维护将带来时间和空间上的较大开销,而MMU半虚拟化开销较小。MMU虚拟化Guest OS能够获得真正的物理地址信息,而影子页表中对于Guest OS是完全透明的。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值