外设直通技术-intel VT-d

虚拟化中外设虚拟化

虚拟化的目标是仿真整个的计算机,从计算机资源角度可以分为CPU虚拟化,内存虚拟化,中断虚拟化,设备虚拟化几个基本的组件,下面主要是看一下设备虚拟化中的外设直通场景下的一些技术实现方式。

设备虚拟化通常有几种模型:软件仿真和设备直通。其中软件仿真可以细分为两种:仿真真实的硬件、专门为虚拟化设计的外设,前者的典型设备有网卡e1000,虚拟GPU VGA,后者的设备主要是标准的virtio系列设备:virtio-blk, virtio-net等。而设备直通是一种将物理设备呈现给虚拟机的方式,它通常是将PCI类设备直通给虚拟机,允许虚拟机直接访问物理设备,无需CPU仿真可以获取更多的硬件收益,另外通过post interrupt等虚拟场景下的优化技术来减少虚拟机的陷出损耗从而获取更好的性能。

下图简略展示了软件仿真和设备直通的数据流路径对比,软件仿真的路径较长会有额外的消耗并且延迟更大,而且随着硬件功能的增强,例如网卡从万兆网卡变为了100G网卡,转发数据占用的CPU资源会更多,而设备直通情况下无需VMM介入从而减少CPU额外消耗并且延迟会更小。
emulate vs passthrough
当然任何事情都是有代价的,我们对两种模型进行对比:

  • 软件仿真非常通用,不依赖于具体的硬件,唯一的缺陷是性能会稍低,毕竟软件仿真都是CPU来完成的,可能会占用较多的CPU资源,另外是需要更多的虚拟机陷出,虚拟化开销会更大。通用,占用额外的CPU资源,性能差。
  • 外设直通一方面减少了虚拟机的陷出,另外是将负载直接卸载到硬件上减少了CPU消耗。但是它的缺陷也很明显,一个设备只能同时被一个虚拟机直接访问,板卡上PCI插槽毕竟是有限的,不可能无限量地分给所有虚拟机,另外是虚拟机迁移到不同机器上需要两端物理环境保持一致。PCI规范为虚拟化场景新增SRIOV功能,SRIOV允许一个硬件PF虚拟出多个VF,将VF分配给不同的虚拟机,SRIOV可以缓解PCI插槽地问题,但毕竟数量是有限的,而且实际上VF共用硬件功能和带宽。对硬件资源有要求,不占用额外的CPU,性能好。

目前虚拟化场景下,要求不高的虚拟机仍然使用软件仿真的方式,其中virtio这种专为半虚拟化而设计的设备类型最为普遍,性能也更好,这种虚拟机占据大多数比例;而对于性能要求较高的虚拟机使用设备直通的方式,这种虚拟机比例占据较少。

外设直通的基本概念

计算机上PCI/PCIe本身的带宽最高,目前高性能设备也通常都是PIC/PCIe接口,下面基于PCI设备介绍外设直通场景下的技术实现一般性原理。

虚拟化中如何取得最好的性能?虚拟化的开销可以用虚拟机陷出次数来进行衡量,陷出次数越多性能越差,所以外设直通为了更高的性能需要减少访问设备的陷出次数。

如何减少陷出次数呢?首先间单回顾一下PCI的基础知识,PCI的标准软件接口是配置空间(可以参考另外一篇笔记),软件通过配置空间获取到设备的基本信息和对资源的需求,然后动态分配地址空间来映射设备的寄存器:通过bar映射的地址空间访问设备的寄存器从而操作设备,还可以通过配置MSI/MSI-X来设置设备有事件需要OS响应时如何发中断。这两个操作是高频操作,特别是软件主动访问设备的寄存器。所以为了减少陷出,需要想办法允许 guest 直接访问设备的寄存器空间。而中断也是不可忽视的一个方面,对于高性能网卡每秒可能有几万个中断,如此多的中断也需要仔细处理,最好能够直接在guest中就能收到设备中断。

设备直通允许guest直接访问硬件,硬件接口主要包括:PCI配置空间,PCI bar映射的地址,MSI中断。但是guest不能直接访问物理设备的PCI 配置空间,主要是不能写配置空间。在虚拟机启动前,物理机已经为设备初始化过BAR地址,guest再修改则会产生冲突,所以guest只能访问初始化过的BAR地址。软件配置MSI主要就是MSI ADDR和DATA,中断发生时向ADDR中写DATA来完成触发,guest配置时写入的是GPA,这样它就可能向任意地址写数据,而不是向中断控制器中写中断数据。
所以,最终的形式就像VFIO中所坐的那样,将物理设备的各种资源信息在QEMU中进行重组,将重组后的PCI配置空间信息提供给guest,guest访问配置信息会陷出到QEMU中模拟,QEMU可以感知到 guest 对 BAR 和 MSI 的操作,然后将 物理设备资源 和虚拟机关联起来。

guest初始化 PCI 设备时也会为其分配BAR映射地址, QEMU 截获到 guest 操作并且知道GPA,然后通过页表映射的方式将物理机 PCI bar 映射地址再次映射到guest 中,从而让guest可以透明地访问设备bar。MSI/MSI-X 也是类似的,guest不能直接设置它的数据来随意产生中断,PCI设备只能直接产生物理中断,它和guest 中需要的物理中断并不相同。 QEMU 截获到 guest操作并且知道它期望的virq,将它通过irq remapping 或者 irqfd 的方式关联物理设备产生的hw irq 和 virq,在设备产生中断的过程中转换为 virq 并投递给虚拟机。

上面大概介绍了如何能够让guest直接访问设备和中断投递能取得更好的性能,但是guest直接操作设备会有一些安全问题,例如guest 通过设备 DMA 操作可以访问 host 或者其他 guest 的内存,这样会造成数据泄露。所以需要硬件提供隔离支持,intel IOMMU 和 arm SMMU提供地址空间隔离,通过IOMMU 的页表将其限制到它自己的 domain 中。

而设备直通上中断投递, intel VT-d 演进了两个版本:irq remapping和 post interrupt,前者还需要虚拟机陷出到宿主机之后通过软件辅助进行注入中断,后者可以直接在硬件上完成中断注入而无需软件参与。

总结:

  • 总体来说外设直通技术方案的目标是解决IO设备直通场景下虚拟化中的性能和安全这两个问题,这其中最为核心的技术就是DMA remapping和irq remapping/post interrupt。
  • DMA remapping通过IOMMU页表方式将直通设备对内存的访问限制到特定的domain中,在提高IO性能的同时完成了直通设备的隔离,保证了直通设备DMA的安全性。
  • irq remapping则提供IO设备的中断重映射和路由功能,来达到中断隔离和中断迁移的目的,提升了虚拟化环境下直通设备的中断处理效率。post interrupt 在 irq remmaping 的基础上进行了增强,硬件直接根据中断重映射表将物理设备的hw irq 转换为对应的vcpu 和 virq注入到虚拟机中。

DMA remapping

传统的不带IOMMU的设备如果直通给虚拟机,虚拟机可以通过DMA访问任意地址的内存数据,这样就会产生安全问题。intel VT-d引入了IOMMU,和MMU一样通过页表的方式进行地址转换。
DMA Address Translation
每个guest 宏观上看是一个进程,共享相同的地址空间,把每一个虚拟机都定义为一个domain,代表地址空间的隔离。
直通给 guest 的设备访问内存需要经过 IOMMU 的页表,cpu侧通过MMU 最终访问到的物理内存和设备通过IOMMU 最终访问到的内存视图应该保持一致。
PCI设备访问内存都需要经过RC,IOMMU 一般就位于 RC 中,PCI设备访问内存的所有事务都需要经过RC,RC 可以使用 IOMMU 进行地址转换然后才访问真实的物理内存。

IOMMU 是如何找到设备对应的页表呢?
PCIe设备的请求从DMA重定向硬件的角度来看,它的内存访问请求分为两类:
  不带地址空间ID的请求(不带PASID),相当于GPA,这是一般EP设备发出的内存访问请求,这类请求通常会表明该请求的类型(读、写或原子操作),DMA目标的地址、大小和发起请求的源设备的ID,一般是Bus/Dev/Function。
  带有地址空间ID的请求(带PASID),相当于GVA,能够发出这类请求的源PCI设备需要拥有virtual address capability,该请求带有额外的信息用于定位目标地址空间和一些其他信息。

先说简单的不带PASID下的DMA过程。
IOMMU会提供接口设置IOMMU页表集合的基址,然后根据PCIe设备的请求中的ID,即BDF找到对应的页表基址,然后根据VA转换为PA,地址转换的方式和MMU类似。当然intel IOMMU的这种查表方式只是一种,还可以有其他方式来组织页表,总归是需要根据请求中设备ID找到对应 domain 的 IOMMU 页表。
IOMMU table
如上图所示,不同的 device 可以属于相同的domain,那么他们就共享相同的页表,在 IOMMU 中隔离的单位是 domain 而不是 device 。

此外 IOMMU 有一些额外的限制,CPU访存经过MMU时,可以允许发生page fault,但是DMA访存经过IOMMU时,不允许发生page fault,也就是说guest 中所有ram区域都要一直有实际的物理内存对应,不能是未分配或者是被swap。姑且猜一下为什么不支持page fault,我理解为 DMA 的操作是异步的,CPU 发起 DMA 请求之后可能会被切换走,DMA搬运数据时发生page fault需要fix这个错误,为它建立正确的映射关系,怎么样为它建立合适的关系呢?在虚拟化场景中,CPU的MMU和设备的IOMMU映射关系要相同,在IOMMU发生page fault时要和CPU侧页表同步,首先需要获得并查询CPU侧的页表,如果没有映射关系需要分配物理内存并将为CPU和设备侧同时构建页表。还有一个问题是DMA遇到page fault 并被fix 后应该怎么样?当然应该重新执行请求,谁来重新发起呢?传统的设备并没有自动重发DMA请求的功能。我相信这些问题都会被解决,不过目前还不支持缺页,需要将 DMA 可能访问的内存都 pin 住。

在VFIO中,抽象出了三个对象:container, group, device,首先声明这三个对象纯粹是软件概念,不一定有对应的硬件概念。其中container代表一个虚拟机,对应一个硬件概念中的domain,而group代表一个最小的隔离单元,而device就是普通PCI device。设备直通时,一个group可能有一个device或者多个device,并且一个group中的device只能同时直通给一个虚拟机或者物理机。
那什么决定了多个device必须在一个group中呢?各个硬件平台都不同,猜测这个粒度取决于IOMMU 和 RC的实现,并且和PCI设备的拓扑结构密切相关。对于不同的PCI拓扑结构,它们的请求到达RC的过程中可能会被修改,RC如果不能识别出ID的不同那么就必须将他们放到同一个group中。

  1. RC中的设备:RC就是个大杂烩,不好分类的设备都可以放到RC中。这些设备的PCIe事务到达RC时都是自身的BDF,所以他们可以自由的直通给不同的domain,即他们每个都是一个group。
  2. PCIe root port设备:这部分直接接到RC下,和上面相同,都可以自由地分配到不同地group中。
  3. 通过PCI Express-to-PCI/PCI-X 桥接入的设备:当PCI事务经过桥时,它的请求头部信息会被替换成桥相关的信息,和netfilter NAT功能比较相似,到RC时桥下所有设备的头部信息都是桥的,IOMMU无法区分他们。所以这部分设备必须是位于同一个group中。
  4. 通过Conventional PCI 桥接入的设备:和上面类似,经过桥时请求信息都被替换成桥的了,他们也必须分配到同一个vfio group中。
  5. SRIOV:一个PCI设备实现了SRIOV之后,可以将自身分裂出多个分身,将一个PF虚拟出多个VF,从他们发起的事务会有独立的BDF,能不能分配给多个domain还得看它上面的拓扑结构,到RC那里能不能正确识别出BDF。

PCIe v2.1 中新增了PASID的支持,即设备发起的请求中还可以带有进程的相关信息,类似于ASID。IOMMU 中可以使用BDF + PASID来区分使用哪个IOMMU 页表。它的一种使用场景是设备直通到虚拟机中,虚拟机中使用用户态驱动来使用外设,外设可能会被多个进程使用,PASID 就是为了区分多个进程的。

Interrupt Remapping 简介

中断重映射功能是由中断重映射硬件单元来实现的,这个硬件单元和 DMA重映射都位于 RC 中。PCI的MSI/MSI-X中断是由驱动配置的,写入期望的中断信息 ADDR 和 DATA ,当设备有中断请求会向 ADDR 写 DATA ,RC对该数据进行转发,例如x86上的 0xFEEX_XXXXh 地址,正常来说会根据地址分发到不同的LAPIC,LAPIC 根据DATA内容选择中断投递的形式。

而在设备直通场景下设备的MSI/MSI-X 信息是由Guest直接分配的,那么问题来了设备发送中断的时候写的 ADDR 地址是 GPA,不能直接往host上投递,否则就乱套了。在虚拟化场景下,直通设备的中断是无法直接投递到Guest中的,那么我们该怎么办?可以由IOMMU截获中断,先将其中断映射到host的某个中断上,然后再重定向(由VMM投递)到Guest内部,也就是irq remapping。到后面硬件可以直接根据中断重映射表注入中断到guest,后面的post interrupt会展开。另外irq remapping的还用来校验中断是否是合法的,设备直通给虚拟机时,虚拟机使用DMA可能会操作 MSI 中断区域,它可以向其中发送任意的数据触发不同的中断,系统不能频繁响应中断或者不能正确响应中断触发软件异常,从而影响系统整体的性能和稳定性,中断重映射会根据设备的标识限定它只能触发VMM配置好的中断。

在有些平台上,没有irq remapping功能也能工作,Guest配置PCI MSI信息时,只需要关联guest的virq,设备的中断还是会触发宿主机的中断,当中断触发时,中断处理服务不直接处理中断而是将virq注入到guest,见VFIO 的 allow_unsafe_interrupts。所以从这个角度看irq remapping的最大作用是中断的合法性校验,防止不怀好意的虚拟机乱发中断。
irq remapping也需要设备的标示,和DMA reammping一样,它对于某些拓扑结构下的多个设备是无法区分的。

irq remapping的作用是将虚拟机配置的MSI/MSI-X中断映射成宿主机中断,宿主机中断注入virq到guest中,并且对它的合法性进行校验。

下面主要是看一下x86平台上irq remapping是如何实现的。

首先是看一下传统的MSI中断,然后再看一下irq remapping下的MSI。

传统设备的MSI 中断主要包含一个32bit的ADDR和一个32bit的DATA字段,ADDR字段包含了中断要投递的LAPIC ID信息,DATA字段主要包含了要投递的vecotr号和投递方式。相对于irq remapping的格式,称它为Compatibility format.
Compatibility format
其中ADDR的Interrupt Format位用来标记这个Request是否需要过 IRQ remapping。

而对于IRQ remapping来说,它的核心是irq remapping表,输入是设备发出的index,用来索引irq remapping表,输出自然是某个具体的irq remapping表项。
irq remapping -1
下面分别看一下输入,irq remapping表项和输出。

首先是PCI 设备发起MSI中断的改变。对于需要过irq remapping的MSI消息,为它定义了另外的格式,称为Remapping format,标记它需要irq remapping, 置位Interrupt Format,其次ADDR字段不再包含LAPIC ID信息而是仅包含了一个16bit的HANDLE索引,另外DATA中还有一个16bit的SUBHANDLE,通过这两个HANDLE组成一个index去 索引irq remapping表项。Remapping format的中断请求格式如下图:
Remapping format
计算irq remapping的索引方式:

if (address.SHV == 0) {
interrupt_index = address.handle;
} else {
interrupt_index = (address.handle + data.subhandle); 
}

然后我们需要看一下irq remapping表的内容,irq Remapping格式的中断重映射表项的格式如下,其中我们看到和传统MSI中断的要素相同,都需要指明向哪个LAPIC发送哪种类型的中断,包括向量号,边沿触发还是水平触发等。
VT-d Interrupt Remapping Table Entry
硬件会根据IRTE自动发起硬件中断,之后再由VMM 找到关联的virt 注入到 guest,这样就基本完成了一次物理设备到虚拟机的通知过程。

VMM向guest注入中断的过程是怎么样的?注入中断是一个软件行为,由CPUx完成,向vcpu运行所在的物理CPUy注入中断,那么发起注入中断的CPU有两种情况:x == y x != y。先考虑 x != y的情况,如果需要CPUy感知到,一般需要核间中断来通知CPUy,将CPUy从non root 中切换到 root 中,然后检查 pending 的中断,由CPUy自己完成注入。如果 x == y, 在PCI 设备中断的时候,CPU已经位于root模式,再返回 non root 时根据 pending 中断自己注入。所以我们看到相对于 x != y 的情况,PCI设备中断直接投递到vcpu 所在的 物理CPU 更为高效,避免了核间中断的开销。所以在vcpu迁移到不同物理CPU的时候修改IRTE中的 Destination ID是更为合理的做法。

Interrupt Posting

在irq remapping中,虚拟机要处理中断需要退出到root模式下,这样就带来了大量的切换开销,每一次虚拟机模式的切换都需要保存和加载虚拟机的上下文,虽然x86 使用VMCS由硬件完成保存恢复速度会快一些,但是毕竟还会有很多的开销。基于irq remapping 引入了Interrupt Posting, 它可以将中断直接投递到 guest 模式下的vcpu中,并且 guest 可以直接处理中断而不需要陷出清除中断,从而减少vcpu的退出。下面主要是看一下 post interrupt 下如何直接注入中断和无法直接注入时的一些处理。

根据上面的irq remapping的描述, 所有的Remapping格式中断请求都需要通过中断重映射表来投递,在post interrupt 中会 IRTE中的Mode域(IM)用来区分这个中断请求是irq remapping方式还是interrupt posting方式。此外post interrupt 还需要 vAPIC 支持,将post interrupt 投递到vcpu 能够感知到的虚拟APIC 上。IRTE中的vector不再是投递给 host 而是直接投递给 guest vAPIC的 virq,并且还新增了一个与VCPU相关的内存数据结构叫做Posted Interrupt Descriptor(PD), 这是一个64-Byte对齐的数据结构并且直接被硬件用来记录将要post的中断请求。
IRTE for post interrupt

PD

PD结构包含以下的域:
在这里插入图片描述

Posted Interrupt Request (PIR)域,提供记录需要post的中断,256bit,每个bit代表一个中断号。
Outstanding Notification (ON)域,由硬件来自动更新,用来表示是否有中断请求pending。当此位为0时,硬件通过修改其为1来产生一个通知事件告知中断请求到来。接收这个通知事件的实体(处理器或者软件)在处理这个posted interrupt时后必须将其清零。
Suppress Notification (SN)域,表示non-urgent中断请求的通知事件是否要被supressed(抑制)。
Notification Vector (NV)域,用来指定产生posted-interrupt“通知事件”(notification event)的vector号。
Notification Destination (NDST)域,用来指定此中断要投递的vCPU所运行物理CPU的APIC-ID。

我们先来看一下PD在vcpu处于不同状态下时对应的状态:
vcpu scheduling for interrupt posting

  • 对于每个vCPU而言,VMM都会分配一个对应的Posted Interrupt Descriptor,用于记录过重映射并且目标为对应vCPU的所有中断请求。
  • VMM软件为所有的Notification Event分配两个物理中断vector:
      第一个称作Active Notification Vector(ANV),该Vector对应到当中断目标vCPU当前正在被物理CPU执行(即vCPU的状态为active)时,Notification Event所使用的中断vector,这个中断并不会将CPU 从non root 强制切换到 root模式,而是用来通知vAPIC有中断来了,在 non root模式下可以直接处理。
      第二个称作Wake-up Notification Vector(WNV),该Vector对应到中断目标vCPU当前不在物理CPU上被执行时用来通知vCPU有中断来了,在 root模式下处理。一个场景是由于Urgent被置起来产生的Notification Event所使用的中断Vector,它需要实时地被处理。另外一个是当CPU halt时,VMM会将其仿真为阻塞状态,当有中断来时应该唤醒vCPU恢复执行。这个中断在arm64上称为doorbell,用于无法直接注入中断时并且vcpu不是running状态下的唤醒功能。

下面我们主要是看一下其他几个字段在vcpu处于不同状态下应该如何设置来完成中断注入的,我们先不考虑urgent的情况。

  • vcpu 从blocked到 runnable
    当vcpu被调度器选中进入 run queue 中时,此时只是在等待执行,是无法完成中断投递的。所以此时发送中断是没有意义的,SN(Suppress Notification ) 应该被设置,什么中断都不应该发,NV设置是没有意义的。
  • vcpu 从runnable到 running
    当vcpu 真正要放到物理cpu上运行时,SN(Suppress Notification ) 应该被清除掉,NV(Notification Vector) 更新成 ANV,并且 NDST(Notification Destination), 这样后续的外设中断可以通过向 NDST 中发送 ANV 来让 vAPIC 注入中断。NDST主要是vcpu 在不同物理CPU上迁移时需要更新。
  • vcpu 从running 到 blocked
    当vcpu 执行 halt 指令时,调度器会将其从runqueue中移除,处于 block 状态。这种情况下外设中断也是无法完成投递的,当有中断来时,唯一需要做的是将vcpu重新加入 runqueue 中,在真正running时检查pending 的中断进行注入。所以此时SN(Suppress Notification ) 应该被清除, NV(Notification Vector) 更新成 WNV,通过中断kick vcpu继续运行。
  • vcpu 从running 到runnable
    vcpu的时间片到期之后需要让出物理CPU给其他任务,这个状态下也是无法完成中断投递的,发送中断也没有意义,所以SN(Suppress Notification ) 应该被设置, NV 没有意义。

下面我们主要看一下直接中断投递和无法完成中断投递两种情况下,硬件和软件是如何处理的。

直接中断投递

当VT-d硬件接收到I/O设备传递过来的中断请求时,中断请求标记了需要过irq remapping,会根据HANDLE/SUBHANDLE找到对应的IRTE项,之后根据Mode判断自己是post interrupt,并且知道要投递的中断号;然后获取IRTE 中的PD,将中断请求记录到PIRR字段,之后根据ON、URG和SN的设置发送Notification Event,RC中的负责投递中断的硬件的工作就结束了。

X = ((ON == 0) & (URG | (SN == 0)))

如何投递virq 到虚拟机中是APIC虚拟化组件的工作,下面我们先看一下它的工作原理。
在使能post interrupt时需要设置VM-execution的标置位,并且在VMCS 中设置PD中的ANV 还有PD的基地址。

vAPIC关心的PD数据如下,和IRTE 中的PD 是同一块地址和同样的布局,只是关心的字段稍有区别:

Bit
Position(s)
NameDescription
255:0Posted-interrupt requestsOne bit for each interrupt vector.
There is a posted-interrupt request for a vector if the corresponding bit is 1
256Outstanding notificationIf this bit is set, there is a notification outstanding for
one or more posted interrupts in bits 255:0
511:257Reserved for software and other agentsThese bits may be used by software and by other agents in the system
(e.g., chipset). The processor does not modify these bits

PIR 字段自不必多提,post interrupt设置的256个中断中的一个。ON是硬件置位的,标示PIR有待处理的数据。

处理器收到ANV中断之后,如果当前使能了post interrupt, 则会对比 VMCS 中的 ANV。如果相等,则vAPIC会清除 PD中 ON,然后通过写 EOI 来清除 ANV 中断。之后会将 PIR 中的数据 复制到VIRR 中并清除 PIR的数据,然后就会拉起 non root 下的中断,之后 guest 就能看到期望的 virq。

无法直接中断投递

如果vcpu 不在running 状态,post interrtupt 只是将 pending 中断记录在 PD 中的 PIR 中,之后根据 PD 中的设置 X = ((ON == 0) & (URG | (SN == 0))) 发送中断。之后如何投递到vcpu 中呢?
当vcpu 在running 之前,软件会将 NV 切换成 ANV,并且负责检查是否 PIR 中是否有 pending 的中断,如果有则需要通知vAPIC。在从 root 切换到 non root时,为了防止中断打断和抢占,最后一段代码是在关中断的情况下进行的,此时 VMM 会 通过 LAPIC 向自己发送一个vector号为ANV 中断 ,在切换到 non root模式下时,中断会被打开,那么vAPIC就能被通知到,之后的处理过程同上。

总结

以上就是设备直通场景下 intel 上 VT-d 的主要技术实现,从软硬件接口的角度而言,硬件需要提供DMA remapping 和 post interrupt 的功能,都需要通过以下几个步骤。

  • IOMMU 需要根据设备标识找到对应的 IOMMU 页表进行地址翻译
  • IOMMU 需要根据设备标示进行中断重映射,得到virq 号 和vcpu 所在的物理CPU,然后向中断控制器进行中断注入
  • 中断控制器需要改造,以支持non root 模式下能够直接感知到 virq的 注入
  • 3
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值