【重识云原生】第四章云网络4.7.2节——virtio网络半虚拟化简介

 《重识云原生系列》专题索引: 

  1. 第一章——不谋全局不足以谋一域
  2. 第二章计算第1节——计算虚拟化技术总述
  3. 第三章云存储第1节——分布式云存储总述
  4. 第四章云网络第一节——云网络技术发展简述
  5. 第四章云网络4.2节——相关基础知识准备
  6. 第四章云网络4.3节——重要网络协议
  7. 第四章云网络4.3.1节——路由技术简述
  8. 第四章云网络4.3.2节——VLAN技术
  9. 第四章云网络4.3.3节——RIP协议
  10. 第四章云网络4.3.4节——OSPF协议
  11. 第四章云网络4.3.5节——EIGRP协议
  12. 第四章云网络4.3.6节——IS-IS协议
  13. 第四章云网络4.3.7节——BGP协议
  14. 第四章云网络4.3.7.2节——BGP协议概述
  15. 第四章云网络4.3.7.3节——BGP协议实现原理
  16. 第四章云网络4.3.7.4节——高级特性
  17. 第四章云网络4.3.7.5节——实操
  18. 第四章云网络4.3.7.6节——MP-BGP协议
  19. 第四章云网络4.3.8节——策略路由
  20. 第四章云网络4.3.9节——Graceful Restart(平滑重启)技术
  21. 第四章云网络4.3.10节——VXLAN技术
  22. 第四章云网络4.3.10.2节——VXLAN Overlay网络方案设计
  23. 第四章云网络4.3.10.3节——VXLAN隧道机制
  24. 第四章云网络4.3.10.4节——VXLAN报文转发过程
  25. 第四章云网络4.3.10.5节——VXlan组网架构
  26. 第四章云网络4.3.10.6节——VXLAN应用部署方案
  27. 第四章云网络4.4节——Spine-Leaf网络架构
  28. 第四章云网络4.5节——大二层网络
  29. 第四章云网络4.6节——Underlay 和 Overlay概念
  30. 第四章云网络4.7.1节——网络虚拟化与卸载加速技术的演进简述
  31. 第四章云网络4.7.2节——virtio网络半虚拟化简介
  32. 第四章云网络4.7.3节——Vhost-net方案
  33. 第四章云网络4.7.4节vhost-user方案——virtio的DPDK卸载方案
  34. 第四章云网络4.7.5节vDPA方案——virtio的半硬件虚拟化实现
  35. 第四章云网络4.7.6节——virtio-blk存储虚拟化方案
  36. 第四章云网络4.7.8节——SR-IOV方案
  37. 第四章云网络4.7.9节——NFV
  38. 第四章云网络4.8.1节——SDN总述
  39. 第四章云网络4.8.2.1节——OpenFlow概述
  40. 第四章云网络4.8.2.2节——OpenFlow协议详解
  41. 第四章云网络4.8.2.3节——OpenFlow运行机制
  42. 第四章云网络4.8.3.1节——Open vSwitch简介
  43. 第四章云网络4.8.3.2节——Open vSwitch工作原理详解
  44. 第四章云网络4.8.4节——OpenStack与SDN的集成
  45. 第四章云网络4.8.5节——OpenDayLight
  46. 第四章云网络4.8.6节——Dragonflow

 1 前言

1.1 QEMU回顾

        在第二章的计算章节,我们在KVM一节有介绍过QEMU,因相隔较远,这里再将其基本架构做一下简要回顾

1.1.1 Qemu中的I/O请求工作流程

        virtio是通用虚拟化框架,在Qemu-kvm中的I/O是用qemu 来模拟的,性能比较差,用virtio来模拟I/O可以进一步提升I/O虚拟化的性能。

        传统的qemu-kvm 工作模式:

 1.Guest产生I/O请求,被KVM 截获;

2.Kvm 经过处理后将I/O请求存放在I/O共享页;

3.通知Qemu,I/O已经存入I/O共享页;

4.Qemu从I/O共享页拿到I/O请求;

5.Qemu模拟代码来模拟本次的I/O,并发送给相应的设备驱动;

6、7、8.   硬件去完成I/O操作并返回结果Qemu;

9.    Qemu将结果放回I/O共享页;

10.   Qemu通知Kvm去I/O共享页拿结果;

11.   Kvm去I/O共享页拿到结果;

12 .  Kvm将结果返回给Guest;

注意:

        a)在这个操作中,客户机作为一个qemu进程在等待I/O时有可能被阻塞;

        b)当客户机通过DMA访问大块内存时候,Qemu不会把结果放回I/O共享页,而是直接通过内存映射的方式将结果直接写到客户机的内存中去,然后通过KVM模块告诉客户机DMA操作已经完成;

1.1.2 virtio诞生背景

        在完全虚拟化的解决方案中,guest VM 要使用底层 host 资源,需要 Hypervisor 来截获所有的请求指令,然后模拟出这些指令的行为,这样势必会带来很多性能上的开销。virtio半虚拟化方案通过底层硬件辅助虚拟化的方式,将部分没必要虚拟化的指令通过硬件来完成,Hypervisor 只负责完成部分指令的虚拟化,以此提高IO性能。

        纯软件模拟的设备和 Virtio 设备的区别:virtio 省去了纯模拟模式下的异常捕获环节,Guest OS 可以和 QEMU 的 I/O 模块直接通信。

        要做到这点,需要 guest 来配合,guest 完成不同设备的前端驱动程序,Hypervisor 配合 guest 完成相应的后端驱动程序,这样两者之间通过某种交互机制就可以实现高效的虚拟化过程。

         由于不同 guest 前端设备其工作逻辑大同小异(如块设备、网络设备、PCI设备、balloon驱动等),单独为每个设备定义一套接口实属没有必要,而且还要考虑扩平台的兼容性问题,另外,不同后端 Hypervisor 的实现方式也大同小异(如KVM、Xen等),这个时候,就需要一套通用框架和标准接口(协议)来完成两者之间的交互过程,virtio 就是这样一套标准,它极大地解决了这些不通用的问题。

1.2 virtio工作原理

        virtio由Rusty Russell开发,对准虚拟化 hypervisor 中的一组通用模拟设备IO的抽象。Virtio是一种前后端架构,包括前端驱动(Guest内部)、后端设备(QEMU设备)、传输协议(vring)。框架如下图所示:   

  • 前端驱动:     
    • 虚拟机内部的 virtio模拟设备对应的驱动。作用为接收用户态的请求,然后按照传输协议对请求进行封装,再写I/O操作,发送通知到QEMU后端设备。    
  • 后端设备:      
    • 在QEMU中创建,用来接收前端驱动发送的I/O请求,然后按照传输协议进行解析,在对物理设备进行操作,之后通过终端机制通知前端设备。    
  • 传输协议:      
    • 使用virtio队列(virtio queue,virtqueue)完成。设备有若干个队列,每个队列处理不同的数据传输(如virtio-balloon包含ivq、dvq、svq三个)。      
    • virtqueue通过vring实现。Vring是虚拟机和QEMU之间共享的一段环形缓冲区,QEMU和前端设备都可以从vring中读取数据和放入数据。

1.2.1 基本原理

        从总体上看,virtio 可以分为四层,包括前端 guest 中各种驱动程序模块,后端 Hypervisor (实现在Qemu上)上的处理程序模块,中间用于前后端通信的 virtio 层和 virtio-ring 层,virtio 这一层实现的是虚拟队列接口,算是前后端通信的桥梁,而 virtio-ring 则是该桥梁的具体实现,它实现了两个环形缓冲区,分别用于保存前端驱动程序和后端处理程序执行的信息。

        其中前端驱动(frondend,如virtio-blk、virtio-net等)是在客户机中存在的驱动程序模块,而后端处理程序(backend)是在QEMU中实现的。在这前后端驱动之间,还定义了两层来支持客户机与QEMU之间的通信。其中,“virtio”这一层是虚拟队列接口,它在概念上将前端驱动程序附加到后端处理程序。一个前端驱动程序可以使用0个或多个队列,具体数量取决于需求。例如,virtio-net网络驱动程序使用两个虚拟队列(一个用于接收,另一个用于发送),而virtio-blk块驱动程序仅使用一个虚拟队列。虚拟队列实际上被实现为跨越客户机操作系统和hypervisor的衔接点,但它可以通过任意方式实现,前提是客户机操作系统和virtio后端程序都遵循一定的标准,以相互匹配的方式实现它。而virtio-ring实现了环形缓冲区(ring buffer),用于保存前端驱动和后端处理程序执行的信息,并且它可以一次性保存前端驱动的多次I/O请求,并且交由后端去批量处理,最后实际调用宿主机中设备驱动实现物理上的I/O操作,这样做就可以根据约定实现批量处理而不是客户机中每次I/O请求都需要处理一次,从而提高客户机与hypervisor信息交换的效率。

        严格来说,virtio 和 virtio-ring 可以看做是一层,virtio-ring 实现了 virtio 的具体通信机制和数据流程。或者这么理解可能更好,virtio 层属于控制层,负责前后端之间的通知机制(kick,notify)和控制流程,而 virtio-vring 则负责具体数据流转发。

1.2.2 优缺点

        Virtio半虚拟化驱动的方式,可以获得很好的I/O性能,其性能几乎可以达到和native(即:非虚拟化环境中的原生系统)差不多的I/O性能。所以,在使用KVM之时,如果宿主机内核和客户机都支持virtio的情况下,一般推荐使用virtio以达到更好的性能。当然,virtio也是有缺点的,它必须要客户机安装特定的Virtio驱动使其知道是运行在虚拟化环境中,且按照Virtio的规定格式进行数据传输,不过客户机中可能有一些老的Linux系统不支持virtio和主流的Windows系统需要安装特定的驱动才支持Virtio。不过,较新的一些Linux发行版(如RHEL 6.3、Fedora 17等)默认都将virtio相关驱动编译为模块,可直接作为客户机使用virtio,而且对于主流Windows系统都有对应的virtio驱动程序可供下载使用。

1.2.3 virtio 数据流交互机制

      从代码上看,virtio的代码主要分两个部分:QEMU和内核驱动程序。Virtio设备的模拟就是通过QEMU完成的,QEMU代码在虚拟机启动之前,创建虚拟设备。虚拟机启动后检测到设备,调用内核的virtio设备驱动程序来加载这个virtio设备。

      对于KVM虚拟机,都是通过QEMU这个用户空间程序创建的,每个KVM虚拟机都是一个QEMU进程,虚拟机的virtio设备是QEMU进程模拟的,虚拟机的内存也是从QEMU进程的地址空间内分配的。

      VRING是由虚拟机virtio设备驱动创建的用于数据传输的共享内存,QEMU进程通过这块共享内存获取前端设备递交的IO请求。

      如下图所示,虚拟机IO请求的整个流程:

    1)  虚拟机产生的IO请求会被前端的virtio设备接收,并存放在virtio设备散列表scatterlist里;

    2)  Virtio设备的virtqueue提供add_buf将散列表中的数据映射至前后端数据共享区域Vring中;

    3)  Virtqueue通过kick函数来通知后端qemu进程。Kick通过写pci配置空间的寄存器产生kvm_exit;

    4)  Qemu端注册ioport_write/read函数监听PCI配置空间的改变,获取前端的通知消息;

    5)   Qemu端维护的virtqueue队列从数据共享区vring中获取数据;

    6)  Qemu将数据封装成virtioreq;

    7)  Qemu进程将请求发送至硬件层。

      前后端主要通过PCI配置空间的寄存器完成前后端的通信,而IO请求的数据地址则存在vring中,并通过共享vring这个区域来实现IO请求数据的共享。

      从上图中可以看到,Virtio设备的驱动分为前端与后端:前端是虚拟机的设备驱动程序,后端是host上的QEMU用户态程序。为了实现虚拟机中的IO请求从前端设备驱动传递到后端QEMU进程中,Virtio框架提供了两个核心机制:前后端消息通知机制和数据共享机制。

       消息通知机制,前端驱动设备产生IO请求后,可以通知后端QEMU进程去获取这些IO请求,递交给硬件。

       数据共享机制,前端驱动设备在虚拟机内申请一块内存区域,将这个内存区域共享给后端QEMU进程,前端的IO请求数据就放入这块共享内存区域,QEMU接收到通知消息后,直接从共享内存取数据。由于KVM虚拟机就是一个QEMU进程,虚拟机的内存都是QEMU申请和分配的,属于QEMU进程的线性地址的一部分,因此虚拟机只需将这块内存共享区域的地址传递给QEMU进程,QEMU就能直接从共享区域存取数据。

2 virtio机制详解

        接下来,我们以目前使用最广泛的QEMU/KVM场景为例子进一步解释virtio的基本原理。虚拟机在物理主机上是一个QEMU的进程,运行在用户态。虚拟机内部的virtio前端驱动所申请的缓存被映射到设备空间中,也在QEMU的地址空间里,这样QEMU就可以通过共享内存的方式对这些缓存进行读写操作。通过这样的方式,实现了virtio前端驱动程序(虚拟机Linux内核的驱动)和后端模拟设备(QEMU后端设备模拟程序)之间数据传输的零复制,进而大幅度提高了虚拟机的I/O性能。

2.1 virtio前后端在QEMU/KVM中的实现

        virtio在虚QEMU拟机内核中实现了前端驱动,在QEMU中实现了后端模拟设备,前后端之间通过虚拟队列(Virtqueue)通信交换数据。针对不同的总线机制,virtio设备有不同的实现方式,因为PCI设备是最广泛使用的设备,所以我们以virtio的PCI网卡为例子进行讲解。virtio-net前后端的实现如图2-1所表示。

图2-1. virtio-net前后端在QEMU/KVM中的实现

  • virtio设备发现和初始化

        在虚拟机启动之后,virtio前端驱动会把自己标识成一个PCI设备,其中包括PCI厂家标识符,PCI设备标识符。这样虚拟机的内核可以基于这个标识符判断使用哪种驱动程序。因为虚拟机中的Linux内核已经包括了virtio驱动程序,所以virtio驱动会被调用去初始化这个virtio设备。除了完成PCI设备通常的初始化操作之外,virtio前端驱动还在初始化的过程中和后端设备模拟程序协商特性位(Feature Bits),并把最终的结果记录在设备状态(Device Status)中。具体的实现代码可以参考内核代码在linux-3.10.0-957.1.3.el7/drivers/virtio/virtio.c中的virtio_dev_probe()函数,如图2-2所示。

 图2-2. virtio设备初始化,协商特性并最终设置设备状态位

        这里有两个比较重要的数据结构需要介绍一下。

  • 特性位(Feature Bits),用来表示设备所能支持的特性。在virtio设备初始化的时候,驱动会去读取特性位,并且告诉设备哪些是它能接受特性。如果后端模拟设备升级了,使能了某个新特性,但是虚拟机里面的驱动还不能识别的话,那么两者就是通过特性进行协商。
  • 设备状态位(Device Status),用来表示设备的当前状态。在virtio设备发现,初始化和特性协商的过程中,都可以查看设备状态位的方式查看virtio设备的状态。比如,virtio_CONFIG_S_FEATURES_OK表示特性协商成功,virtio_CONFIG_S_DRIVER_OK表示驱动已经配置成功。
  • virtio网卡发送数据处理过程

        虚拟队列(Virtqueue)是被用来在virtio前端驱动和virtio后端模拟设备之间双向数据传输的数据结构。每个virtio设备都维护着一个或者多个虚拟队列。以virtio网络设备为例,它至少维护两个虚拟队列,一个用来存储要发送的数据,一个用来存储接收的收据。每个虚拟队列数据结构都由三部分组成,分别是descriptor table,available ring和used ring。

  • descriptor table用来描述一组缓存,是virtio前端驱动创建的。和缓存相关的信息主要是物理地址和长度;缓存数组的数量是有队列大小(Queue Size)决定的;
  • available ring是给virtio前端驱动给virtio后端模拟设备传输数据时使用的,比如虚拟机用virtio-net设备发送数据的时候,所发送的数据就会先缓存在这里,再通知virtio后端模拟设备来读取;也就是说available ring的缓存,只能让前端写,后端读;
  • used ring是给virtio后端模拟设备给virtio前端驱动传输数据时使用的,比如virtio后端模拟设备从tap网络接口收到数据之后,会把收到的数据缓存到这里,再通知virtio前端驱动程序;也就是说used ring的缓存,只能让后端写,前端读;

 图2-3. Virtio规范中虚拟队列的定义

 图2-4. used ring和available ring在virtio规范中的定义

2.2 具体实现示例

        下面我们以虚拟机发送数据为例,结合Linux 3.10和QEMU1.5的代码实现,详细说明一下在QEMU/KVM场景下具体的实现过程。

2.2.1 virtio前端驱动填充数据包,并发出通知

        QEMU虚拟机内的virtio网卡驱动在初始化的时候,会和其他的网络驱动一样注册发送函数xmit_skb()。具体的实现如图5,6所示,所以虚拟机内的virtio网卡发送数据的时候,会调用预先注册的函数xmit_skb()。要发送的数据会调用virtqueue_add_outbuf()放置在available ring中。最终在virtqueue_add_outbuf()函数中,会调用virtqueue_kick()函数,并进一步调用virtqueue_notify()函数。在virtqueue_notify()函数中,如图7所表示的virtio前端通过I/O写寄存器的方式通知virtio后端模拟设备。这部分前端驱动的代码在drivers/virtio/virtio_ring.c中。

 图2-5. virtio设备发送数据报文

图2-6. virtio前端驱动通知QEMU

图2-7. virtio通知函数最终会写寄存器

2.2.2 KVM截获I/O后通知后端

        虚拟机virtio前端驱动程序发送通知的函数最终是执行I/O写指令。在QEMU/KVM环境中,虚拟机执行I/O指令,会触发VMExit。在KVM的VMExit代码中会判断退出的原因,I/O操作对应的处理函数是handle_io(),具体的代码在linux-3.10.0-957.1.3.el7/arch/x86/kvm/vmx.c,如图8所示。最终再经由KVM通知到QEMU中的virtio-net后端模拟设备,其中还涉及到KVM和eventfd等通信机制,因限于篇幅在这里不详细描述了。

 图2-8. KVM中处理I/O操作导致的VMExit代码

2.2.3 virtio后端模拟设备处理通知

        如图8所表示的,在接收到来自KVM的通知之后,QEMU后端设备模拟程序会调用virtio_queue_host_notifier_read()函数,进而调用预先注册的函数virtio_ioprt_write()处理来自前端驱动的I/O写操作。在接收到前端发来的通知之后,会调用virtio_queue_notify()函数进行处理。在接收网络数据包的时候,virtio_queue_notify()会再进一步调用virtio-net网络设备注册的数据包接收函数virtio_net_handle_rx()。如图9所表示的,在qemu_flush_queued_packets()中,QEMU会把数据复制到对应的队列中(QEMU中对应后端的不同tap都维护着不同的队列),之后再调用qemu_notify_event()通知virtio前端,最终会调用kvm_set_irq()触发vCPU的中断的方式通知virtio前端。

图2-9. virtio后端设备接收通知后的处理

 图2-10. virtio-net预先注册的数据报接收函数

 图2-11. virtio后端设备处理前端发送的数据包

参考链接

DPDK系列之十二:基于virtio、vhost和OVS-DPDK的容器数据通道_cloudvtech的博客-CSDN博客_dpdk容器化

DPDK系列之六:qemu-kvm网络后端的加速技术_cloudvtech的博客-CSDN博客_dpdk kvm

DPDK系列之十五:Virtio技术分析之一,virtio基础架构_cloudvtech的博客-CSDN博客_virtio

从dpdk1811看virtio1.1 的实现—packed ring-lvyilong316-ChinaUnix博客

qemu-kvm中的virtio浅析 - 骑着蜗牛追太阳 - 博客园

Qemu模拟IO和半虚拟化Virtio的区别以及I/O半虚拟化驱动介绍_weixin_34051201的博客-CSDN博客

virtio blk原理 - 简书

virtio-blk简介_sdulibh的博客-CSDN博客

virtio-net原理(二) - 蓝色魔兽 - 博客园

virtio-net - 网络半虚拟化 - 知乎

DPU和CPU互联的接口之争:Virtio还是SR-IOV? - 极术社区 - 连接开发者与智能计算生态

virtio简介(一)—— 框架分析 - Edver - 博客园

virtio简介(二) —— virtio-balloon guest侧驱动 

virtio简介(三) —— virtio-balloon qemu设备创建 

virtio简介(四)—— 从零实现一个virtio设备 - Edver - 博客园

virtio简介(五)—— virtio_blk设备分析 - Edver - 博客园

KVM之Virtio介绍 (十五) - 程序员大本营

虚拟化之Virtio-Net基础篇-51CTO.COM

KVM 虚拟化详解 - 知乎

virtio 与vhost_net介绍_魏言华的博客-CSDN博客_vhost-net

用户态虚拟化IO通道实现概览及实践(上)

用户态虚拟化IO通道实现概览及实践(下)

本文由[mdnice](https://mdnice.com/?platform=6)多平台发布
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容