Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1910650
  • 博文数量: 164
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 6697
  • 用 户 组: 普通用户
  • 注册时间: 2013-01-23 18:56
个人简介

将晦涩难懂的技术讲的通俗易懂

文章分类

全部博文(164)

文章存档

2019年(18)

2018年(19)

2017年(9)

2016年(26)

2015年(18)

2014年(54)

2013年(20)

分类: 虚拟化

2019-11-03 18:37:18

Intel VT-d实现概述

Intel VT-dIOMMU技术在intel上的实现,全称是Intel Virtualization Technology for Direct I/O,它是Intel虚拟化技术的一部分,主要针对的是I/O子系统,它的实现主要是通过在硬件上引入重定向单元,该硬件重定向单元用于对I/O子系统的DMA操作和中断传递进行重定向,从而辅助VMMVirtual Machine Monitor)实现I/O子系统的虚拟化。也就是说VT-d技术的关键分为两部分:DMA重映射(DMA remapping)和中断重映射(interrupt remapping

一般情况下VMM支持I/O虚拟化可以通过以下四种方式实现:

(1)纯软件模拟。即VMM的软件模拟一个现有的I/O设备,这种方式具有较好的兼容性,但是纯软件模拟在性能和功能上就表现得比较差了。

(2)还是纯软件模拟,但是引入新的I/O操作接口。如virtio方式,这些接口针对I/O虚拟化进行一定的优化,这样虽然能够解决一定的性能问题,但是兼容性问题又出现了,因为需要使用新的操作接口。

(3)设备直通(passthough),直接将I/O设备分配给某个VMVirtual Machine),这样只有指定的VM能够使用该I/O设备,并且I/O设备的驱动位于VM中,并且VM能够直接对I/O设备进行操作,这样性能和兼容性都能达到最佳,但是一个I/O设备只能给一个VM使用。

(4)I/O设备分享(SR-IOV),这是硬件分配方式的一种扩展,主要还是需要I/O设备本身需要支持一定的功能,如能够同时提供多个功能接口,比如PCIe设备的SR-IOV功能,即PCIe设备本身能够将一个物理PCIe设备,变成多个逻辑设备,这多个逻辑设备共享该PCIe设备上的物理资源,并且可以独立地分配给不同的VM

以上I/O虚拟化的一个通用要求就是要求VMM能够将属于不同VMI/O设备安全地隔离起来,即需要满足以下两方面的要求:

属于一个VMvCPU无法访问到属于另外一个VMI/O设备,这可以通过VMXVirtual Machine Extension)中的EPTExtended Page Table)功能来实现,即VMM可以通过软件配置,将虚拟机物理内存,即Guest Physcial Address映射到不同的主机内存区域,即Host Physical Address

属于一个VMI/O设备不能访问到属于其他VM的内存或者向属于其他VM的中断控制器发送中断。如果是纯软件模拟的方式,则VMM也可以通过纯软件的方式来控制I/O设备访问内存(如DMA)和发送中断的行为。但是如果采用硬件分配的方式,则就需要在硬件上对I/O设备访问内存和发送中断的行为进行拦截,然后重定向到指定的VM中,这就是VT-d派上用场的时候了。

VT-d是一个位于CPU、内存和I/O设备之间的硬件设备,通常位于PCI设备树的根部,或者类似的位于I/O子系统的根部,当VT-d重定向硬件设备启用的时候,它会拦截位于它下面的所有I/O设备产生的中断请求和通过DMA方式对虚拟机内存访问的请求,然后通过查找中断重定向表或者I/O页表的方式(类似分页机制)来重新定位中断转发的目标LAPIC或者是I/O设备访问的目标主机物理内存地址。如下图所示:

 

VMM软件负责I/O设备的分配,即将指定I/O设备和相应的VM对应起来,并且负责建立中断重定向关系表和I/O地址转换页表,并将这些转换关系的配置设置到VT-d硬件设备上,而I/O设备发起的中断请求或者DMA内存访问请求中带有相应设备的ID,这样VT-d硬件单元就可以通过硬件查找的方式将不同的I/O设备中断和内存访问请求重定向到相应的VM上,从而达到隔离不同VMI/O设备的目的

总的来说,VT-d的主要功能就是将I/O设备的DMA访问请求和中断请求重定向到VMM设定好的VM中。

DMA Remapping

负责DMA重定向硬件一般位于Root ComplexRC中,Root-ComplexPCIe系统中引入的概念,它将CPU、内存子系统和PCIe子系连接起来。如下图所示:

 

Root Complex则经常被集成到CPU芯片上、MCHMemory Controller Hub)上或者是IOHI/O hub)上。

DMA重定向硬件将来自于I/O子系统的内存访问请求分为两类:

不带地址空间ID的请求Request without Process Address Space Identifier,即Request-without_PASID),相当于GPAGuest Physical Address),这是一般Endpoint设备发出的内存访问请求,这类请求通常会表明该请求的类型(读、写或原子操作),DMA目标的地址、大小和发起请求的源设备的ID

带有地址空间ID的请求Request with Process Address Space Identifier,即Request-with-PASID),相当于GVAGuest Virtual Address),能够发出这类请求的源PCI设备需要拥有virtual address capability,该请求带有额外的信息用于定位目标地址空间和一些其他信息。

不同虚拟机之间的隔离是通过防止分配到其他虚拟机的资源(CPUI/O设备)访问到本虚拟机的物理地址。每个虚拟机都会有自己独立的物理地址空间,即GPAGuest Physical Address)空间,该空间不同于主机物理地址空间,即HPAHost Physical Address)空间。DMA重定向硬件将从I/O设备发过来的访问请求中包含的地址看做是DMA地址,根据不同的使用配置,该DMA地址可能是GPA;可能是跟PASIDProcess Address Space ID)相关的VAapplication Virtual Address);也可能是由软件定义的I/O虚拟地址(IOVA不管怎样,DMA重定向硬件将DMA地址最终转化为HPAHost Physical Address)实现最终主机物理地址的访问

如下图所示,系统中存在两个Domain12,也可以理解为存在两个虚拟机,这两个虚拟机发出的内存请求通过在CPU上的内存管理单元(MMU),在x86 CPU上可以理解为分页机制和EPTExtended Page Table)的组合,将发出的内存请求地址最终转化为主机的物理内存地址,即HPA,对应到主机物理内存上。而Device 12,则可以理解为分别分配给Domain 12I/O设备,虽然它们发出访问请求的地址数值一样,但是由于它们所属的Domain不一样,导致DMA Memory Management将会使用不同的地址转换页表IOMMU,将其分别转换到不同Domain所对应的HPAVMM/Hypervisor负责对DMA Memory Management所使用的I/O地址转换页表进行创建和维护,同时需要对DMA重定向和I/O设备进行配置,协商好使用什么类型的地址,GPA或者GVA

 

每个DMA重定向硬件的实现可以是一个硬件单元包含整个PCI Segment,也可以是多个硬件单元,每个硬件单元各自包含PCI Segment中的部分PCI设备。系统的BIOS或者UEFI负责在系统启动的时候对VT-d硬件进行检测,并分配相应的地址空间,让系统软件能够访问到VT-d硬件及其配置寄存器。BIOS/ACPIACPI表的子表(DMARDMA Remapping Reporting ACPT Table)的形式将VT-d硬件资源描述出来,这样VMM需要找到DMAR表,就可以对相应的VT-d硬件进行访问或配置了。DMAR表的组织形式以后再详细讲。

每个从I/O设备往上传输经过重定向硬件的数据包都会包含source-id用于定位源I/O设备,不同的I/O设备其source-id的实现可能不一样,对于PCI设备而言,其source-id是传输层头部中包含的requester Identifier,由BusDeviceFunction组成,其格式如下所示:

 

      所谓的重定向就是对目标地址进行转换或更改,DMA重定硬件向利用分层页表结构对地址进行转化。属于不同DomainI/O设备请求需要被分配到不同的转换页表中,该索引、分配过程以I/O设备请求的source-id为输入源,对于PCI设备发出的Request-without-PASID而言,它将会使用请求包中包含的PCI BusDeviceFunction号作为索引值。在进行这样的索引之前,VMM需要在内存中建立好一个4KBRoot-table,该table包含256entry,每个entry对应一个PCI Bus,每个Root-entry中包含一个指针,该指针指向一个4KBContext-table,该Context-table中包含了256entry,对应到该PCI Bus下的所有DeviceFunction。每个Context-entry都包含一个指向该PCI Function所对应的Domain的地址转换页表的指针。其结构如下图所示:

 

当找到相应的地址转换页表后,硬件才开始正常的地址分级页表转换,即所谓的page-walk,将请求中的GPA地址转化为HPA,实现最终的物理内存访问,该转化过程即称为Second-Level-TranslationRoot-table的地址需要VMM在启动VT-d硬件的时候,将Root-table的地址写到VT-d硬件相应的寄存器(Root Table Address Register)上,为VT-d硬件提供一个入口。

对于Request-with-PASID而言,其请求中包含的地址转换页表入口索引,即(PCIBusDeviceFunction值)是一样的,但是其包含的请求地址类型不一样,是GVA,而不是GPA,故需要现将GVA转化为GPA(即为First-Level-Translation),然后再将得到的GPA转化为HPA,即Second-Level-Translation,才能实现最终的物理内存访问。

该方法需要有两次索引地址转换页表的入口地址,所以需要用到Extended-root-table,该4KBtable每个Ext-root-entry中,包含两部分,分别包含Upper-context-table的指针和Lower-contex-table的指针,Upper-context-table指针指向的是PASID-TablePASID-Table则根据PASID来对该表进行索引,每个PASID-entry都包含一个指向分级页表的入口,该分级页表用于完成First-Level-Translation,即将请求中包含的GVA转化为GPASencond-Level-Translation分级页表的索引和前面Request-without-PASID一样。当Frist-Level-Translation完成后得到的GPA,将作为Second-Level-Translation作为输入地址,最终得到HPA,完成主机物理内存的访问。

 

不管是first-level translation (requests-with-PASID)还是second-level translation (request-without-PASID)。对于request-with-PASID而言,会先使用first-level translation tableDMA地址转换为一个不带PASID的地址,然后再将这个不带PASID的地址作为second-level-translation table的输入进行转换得到最终的物理地址。查询页表完成地址转换的方式就根普通的分页机制完全一致。页表的层数会根据具体使用的页框大小而变化,页框大小可以是4KB2MB1GB,下图以4KB为例。

 

      由此可见要完成一次DMA重定向访问到真正的主机物理地址,中间会有很多的内存访问,为了加快这些内存访问,VT-d硬件中,会引入各种各样的Cache加快这些物理地址的转换,或者是主机物理内存的访问这也是SRIOV方式和原始设备直通性能差异的一个原因。

Interrupt Remapping

   讲完DMA重映射后我们再来看中断重映射(interrupt remapping),中断重映射单元让系统软件能够对I/O设备产生的中断(包括从I/O APIC发送过来的中断,I/O设备产生的以MSIMSI-X形式传递的中断,不包含中断重映射硬件本身产生的中断)的传输进行控制,而不仅仅取决于硬件的连接。

对于VT-d硬件而言,中断请求就是从外面发送进来对物理地址范围0xFEEX-XXXXh的写请求。VT-d中,中断重映射功能由Extended Capability Register寄存器来决定,该寄存器的bit3表示Interrupt Remapping Support,如果为1,则表示支持中断重映射,如果为0,则表示不支持中断重映射。Extended Capability Register寄存器字段含义如下图。

 

为了实现同一物理系统中,不同Domain之间的隔离,需要VT-d的中断重映射硬件对接收到的中断请求提取其中断的来源(一般是PCI设备的BusDeviceFunction号之类的信息),然后根据不同设备所属的Domain,将该中断请求转发到相应的Domain中,实现不同Domain之间,中断请求的隔离。

Intel x86架构中,中断请求的格式有两种:兼容格式和可重映射格式。并且每个请求中都包含了访问的地址和数据,格式的选择由地址信息的bit 4Interrupt Format)来决定。

对于兼容格式而言,bit 4Interrupt Format)为0,表示兼容格式,这种格式的中断直接向上传递到CPULAPCI,不会被重新映射。兼容格式的中断请求消息格式如下。

 

对于可重映射的中断请求消息格式而言,bit 4Interrupt Format)为1,其具体格式如下所示:

消息分为AddressData两部分,其中Address信息中,bit 19 bit 5bit 2共同组成了16bitHandle,并且在Address bit 3SHV)为1的情况下,Data区域的bit 15 bit 0包含了Sub-Handle,这些值用于索引中断重映射表,后面会讲到。

中断重映射硬件利用一张位于内存的单层表,即中断重映射表,来确定中断请求需要被如何重新生成并转发。该表由VMM配置,并且该表的物理地址将会被写到VT-d硬件中的Interrupt Remap Table Address Register,用于告知硬件中断重映射表的位置,其格式如下图。并且低4 bit用于表示中断重映射表中包含的entry个数,即2的(1+S)次方,最高达216次方,即64K

用来记录中断重映射表物理地址的寄存器

中断重映射表中的每个表项大小为128 bit,即16 Byte,称为Interrupt Remap Table EntryIRET),其格式如下所示:

中断重映射表表项

主要包含了重映射目标的Destination IDVector和一些其他中断传输相关的信息(对于兼容格式的中断而言,其中断的属性都是在中断请求中说明,而可重映射的中断,其中断属性则在IRTE中说明)。另外bit 15必须为0,该bit表示IRTE Mode,如果为0,则表示为Remapped Interrupt,如果为1,则表示Posted Interrupt(下一节讲解)。中断重映射硬件,将以前面提到的中断请求中包含的HandleSub-Handle计算索引值,对中断重映射表进行索引。其算法如下所示:

从硬件的角度来看,整个中断重映射的过程为:硬件检测到向0xFEEX-XXXXh地址DWORD的写请求,判定其为中断请求,并将其拦截。如果中断重映射的功能没有打开(Global Status RegisterIRES0),则所有的中断请求都以兼容格式的中断来处理。如果中断重映射功能被打开(Global Status RegisterIRES1),则查看中断请求的格式,如果是兼容格式,则直接跳过中断重映射硬件,继续中断请求的传递。如果是可重映射格式,则检测中断请求中的数据正确性,计算Interrupt_index,读取相应的IRTE,检测IRTE是否存在及其正确性,如果一切正常则中断重映射硬件将会根据读取的IRTE产生一个新的中断请求,并向上传递。其基本流程如下所示:

对于软件而言,为了实现中断重映射,则需要进行如下操作:

如果中断重映射表没有被分配,则在内存中分配一块区域作为中断重映射表,并将该表的位置告诉给中断重映射硬件,即将该表的位置写到Interrupt Remap Table Address Register

找到一个可用的IRTEInterrupt Remapping Table Entry),然后设置需要重新转发的中断的一些属性,如传递的目标、中断模式,中断向量等。

对中断源(一般是I/O设备或者是I/O APIC)进行设置,让中断源能够产生可重映射格式的中断,并且由其HandleSub-HandleSHV等区域计算出来的interrupt_index正好匹配到之前设置好的IRTE。不同的I/O设备的设置方法会有区别。

对于I/O xAPIC而言,系统软件通过设置设置I/O xAPICRedirection Table EntriesRTEs)来设置I/O xAPIC产生的可重映射中断。I/O xAPICRTE的格式如下所示:

Interrupt Format设置为1表示产生的中断为可重映射中断,并且可以设置Interrupt_IndexVectorTrigger Mode等信息。

对于可以产生MSIMessage Signaled Interrupt)或者MSI-X的设备而言,对产生中断的设置包括AddressData寄存器,其格式如下所示:

Addressbit 4Interrupt Format)为1的时候,则表示产生的中断为可重映射中断,并且可以设置Interrupt_IndexSHV等值。对于支持多个(必须是2n次方)Vector中断的MSI/MSI-X消息而言,其Data寄存器的低n位对应到具体的Vector数值,并且SHVSub-Handle Valid)为1,这时候IRTE的索引值就是Interrupt_index[15:0]的值加上Vector的值。

总的来说,VT-d的中断重映射就是指VT-d会拦截其下面挂载的I/O设备产生的中断,然后根据接收到的中断请求索引中断重映射表,根据找到的中断重映射表的表项产生新的中断请求,上传到CPULAPICVT-d就是通过这个重映射动作,实现了同一物理系统中不同DomainI/O设备中断请求的隔离。同时为了让I/O设备能够产生可重映射的中断,并对中断重映射表进行正确的索引,系统软件还需要对I/O设备的中断请求生成进行配置。

Interrupt Posting

Interrupt-postingVT-d中中断重映射功能的一个扩展功能,该功能也是针对可重映射的中断请求。Interrupt-posting功能让一个可重映射的中断请求能够被暂时以一定的数据形式post(记录)到物理内存中,并且可选择性地主动告知CPU物理内存中暂时记录着pending的中断请求。

x86处理器的虚拟化中,Interrupt-posting再加上APIC VirtualizationVMM能够更加高效地处理分配各虚拟机的设备产生的中断请求。VT-d重映射硬件是否支持Interrupt-posting功能可以通过查询Capability Registerbit 59 Posted Interrupt SupportPI)知道硬件是否支持该功能。

对于VT-d而言,所有可重定向的中断都需要经过IRTEInterrupt Remapping Table Entry)的处理,在进行处理之前会先通过IRTEbit 15IRTE Mode)判断该IRTE的模式,如果为0,则VT-d硬件将会以Remapped Interrupt的形式来解析该IRTE(前一篇文章讲的中断重映射),如果为1,则VT-d硬件将会以Posted Interrupt的形式来解析该IRTE,如下图所示:

Posted Interrupt格式的IRTE的定义会有一些不同,主要是:

(1)多了一个Posted Descriptor Address Low/High,该区域保存一个指向内存的指针,该指针指向的位置就是保存中断(Posted Interrupt)的结构体。

(2)Urgent位,该位用于表示该中断是否是紧急的,是否需要目标CPU的立即响应。

(3)Vector用于设置Posted Interrupt Descriptor数据结构中相应的位,而不是用于设置直接产生的中断的vector值。

每个Posted Interrupt Descriptor的大小为64 Byte,用于记录VT-d硬件那边post过来的中断,即在内存中暂时记录一些中断请求,等待CPU的处理,而不是主动打断CPU的运行,让其跳转过来处理,其格式如下所示:

(1)Posted Interrupt Requests PIR),一共256 bit,每个bit对应一个中断向量(Vector),当VT-d硬件将中断请求post过来的时候,中断请求相应的vector对应的bit将会被置起来。

(2)Outstanding NotificationON),表示对于该Posted Interrupt Descriptor当前是否已经发出了一个Notification Event等待CPU的处理。当VT-d硬件将中断请求记录到PIR的时候,如果ON0,并且允许立即发出一个Notification Event时,则将会将ON置起来,并且产生一个Notification Event;如果ON已经被置起来,则VT-d硬件不做其他动作。

(3)Suppress NotificationSN),表示当PIR寄存器记录到non-urgent的中断时,是否不发出Notification Event,如果该位为1,则当PIR记录到中断的时候,则不发出Notification Event,并且不更改Outstanding Notification位的值。

(4)Notification VectorNV)表示如果发出Notification Event时,具体的Vector值。

(5)Notification DestinationNDST),表示若产生Notification Event时,传递的目标逻辑CPULAPIC ID(系统中以逻辑CPULAPIC ID来表示具体的逻辑CPUBIOS/UEFI其初始化系统的时候,会为每个逻辑CPU分配一个唯一的LAPIC ID)。

在硬件上整个Posted Interrupt的处理过程如下所示:

VT-d硬件接收到其旗下I/O设备传递过来的中断请求时,会先查看自己的中断重定向功能是否打开,如果没有打开则,直接上传给LAPIC。如果中断重定向功能打开,则会查看中断请求的格式,如果是不可重定向格式,则直接将中断请求提交给LAPIC。如果是可重定向的格式,则会根据算法计算Interrupt_Index值,对中断重定向表进行索引找到相应的IRTE。然后,查看IRTE中的Interrupt Mode,如果为0,则该IRTE的格式为Remapped Format,即立即根据IRTE的信息产生一个新的中断请求,提交到LAPCI。如果Interrupt Mode1,则表示该IRTE的格式为Posted Format,根据IRTE中提供的Posted Interrupt Descriptor的地址,在内存中找到相应Posted Interrupt Descriptor,并根据其ONURGSN的设置判断是否需要立即产生一个Notification EventOutstanding Notification0,并且该中断为UrgentURG=1)或者不抑制该NotificationSN==0)),如果不需要,则只是将该中断信息记录到Posted Interrupt DescriptorPIRRPosted Interrupt Request Register)字段,等待CPU的主动处理。如果需要立即产生一个Notification Event,则根据Posted Interrupt Descriptor(会提供目标APIC IDvector、传输模式和触发模式等信息)产生一个Notification Event,并且将该中断请求记录到PIRR字段。

硬件在对Posted Interrupt Descriptor进行修改的时候,要保证该修改是原子操作,即对Posted Interrupt Descriptor的读取、修改和写入必须是原子操作,并且在写入之后,要保证相应内存在各个cache agent之间的一致性,即所有的CPU应该立马能够看到该内存修改。

从软件的角度来看,VMM可能会对Interrupt Posting做如下设置和操作:

1. 对于每个vCPU而言,VMM都会分配一个对应的Posted Interrupt Descriptor用于记录和传递经过重定向,并且目的地为对应vCPU的所有中断请求。

2. VMM软件为所有的Notification Event分配两个物理中断vector

(1)第一个称作Active Notification VectorANV),该Vector对应到当中断目标vCPU当前正在被逻辑CPU执行(即vCPU的状态为active)时,Notification Event所使用的中断vector

(2)第二个称作Wake-up Notification VectorWNV),该Vector对应到中断目标vCPU当前不在逻辑CPU上被执行时,由于Urgent被置起来产生的Notification Event所使用的中断Vector

3. 对于从直接分配给VMI/O设备产生的中断而言,VMM会为每个这样的中断分配一个IRTE。并且VMM可能会为vCPU使能硬件上的APIC VirtualizationAPIC Virtualization主要包括两方面功能:virtual-interrupt deliveryprocess posted-interrupts,其主要工作形式表现在:

(1)当一个vCPUVMM调度即将执行的时候,该vCPU的状态为active,该状态的一个表现形式是VMM会将Posted Interrupt DescriptorNotification Vector字段设置为ANVActive Notification Vector)。这样就允许当这个vCPU在逻辑CPU上执行的时候,所有指向该vCPU的中断都能够直接被该vCPU处理,不需要经过VMMvCPU通过将记录在Posted Interrupt Descriptor中的中断直接转移到Virtual-APIC Page中,并直接将中断信号传递给vCPU,让vCPU直接获取到该中断信号的方式来处理Notification Event

(2)当一个vCPU被抢占后,即vCPU的状态为ready-to-run,该状态的一个表现形式是VMM会将Posted Interrupt DescriptorNotification Vector字段设置为WNVWake-up Notification Vector),并且SNSuppress Notification)设置为1。只有当接收到的中断请求为Urgent的时候,才会发出Notification Event,触发VMM的执行,让VMM调度目标vCPU处理该紧急中断。

(3)当一个vCPU处于Halt状态的时候,逻辑CPU执行VMM软件,VMM将该vCPU标记为halted状态。该状态的一个表现形式就是将Posted Interrupt DescriptorNotification Vector字段设置为WNVWake-up Notification Vector),并且SNSuppress Notification)设置为0,即任何到达该Posted Interrupt Descriptor的中断请求都会触发Notification Event,让VMM唤醒vCPU,让vCPU处理中断请求。

4. VMM调度并执行一个vCPU的时候,VMM会对被记录到Posted Interrupt Descriptor的中断请求进行处理:

(1)首先,VMM会通过将Posted Interrupt DescriptorNotification Vector字段的值改为ANVvCPU的状态变为active

(2)VMM检测在Posted Interrupt Descriptor中是否有待处理的中断请求。

(3)如果有待处理的中断请求,则VMM会给当前CPU发送一个sefl-IPI中断(即CPU自己给自己发送一个中断),并且中断向量值为ANV。当vCPU一使能中断的时候,就能够立马识别到该中断。该中断的处理类似于vCPU处于active状态时,接收到了Active Notification Vector的中断请求,vCPU可以直接对其进行处理,不需要VMM的参与。

5. 同样VMM可以可以利用Posted Interrupt的处理机制,通过设置Posted Interrupt DescriptorvCPU注入中断请求。

    总体上来说,Interrupt Posting的功能就是让系统可以选择性地将中断请求暂存在内存中,让CPU主动去获取,而不是中断请求一过来就让CPU进行处理,这样在虚拟化的环境中,就能够防止外部中断频繁打断vCPU的运行,从而提高系统的能能

阅读(432) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~
评论热议
请登录后评论。

登录 注册