浅析virtio技术(1)基本原理

virtio是主流的IO设备半虚拟化解决方案,提供统一通信框架和编程接口。介绍了virtio协议,包括版本、架构(前端驱动、后端设备、传输层),还阐述了其工作机制,通过前后端通信模型和虚拟队列进行数据交互。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

简介

virtio是当前主流的IO设备半虚拟化解决方案,其主要目标是在虚拟机和各种Hypervisor虚拟设备之间提供一个统一的通信框架和编程接口,减少跨平台所带来的兼容性问题,提升驱动程序开发效率。virtio是一种前后端架构,包括前端驱动和后端设备以及virtio定义的传输协议。通过传输协议,virtio可以应用到不同的虚拟化方案中,如QEMU/KVM、Iguest等,而且也允许实现不同的前后端。
在这里插入图片描述

virtio协议

virtio技术的实现描述在virtio标准协议中。virtio协议定义了virtio通信遵循的接口,包含设备类型、设备状态、设备支持的特性以及前后端数据传输的通道(virtqueue)等内容。

virtio协议版本

目前在使用的virtio规范有三个版本:0.95、1.0和1.1,其中,0.95版本是目前广泛在使用的版本,称为传统模式;1.0版本中协议将设备的配置部分做了一些调整,被称为现代模式;1.1版本则主要是新增了packed virtqueue的支持。

virtio协议架构

从总体上来看,virtio协议架构可分为三层:前端驱动、后端设备以及中间的传输层:
在这里插入图片描述

前端驱动

前端驱动是虚拟机内部的virtio模拟设备对应的驱动程序,其主要作用是发现virtio设备、接受来自虚拟机内部的请求,并依据virtio协议与virtio设备进行通信。

对于Linux系统,内核已经实现了一系列的virtio前端驱动程序,包括virtio-ballon、virtio-net、virtio-blk和virtio-scsi等;另外,DPDK也实现了用户态版本的virtio设备驱动程序virtio-pmd(主要针对virito网络设备实现),用于进一步提升virito数据通信的性能。

后端设备

后端设备承担的主体功能分为两部分:1、virtio后端设备的模拟;2、依据virtio协议处理来自虚拟机端发送的请求。

在QEMU的实现中,virtio设备是QEMU为虚拟机模拟的PCI设备,遵循PCI-SIG定义的PCI规范,具有配置空间、中断配置等功能;对于virtio设备的请求处理,最早的实现是放在QEMU内部,但随着技术演进,又逐渐出现了vhost、vhost-user以及vDPA等后端加速方案实现。

传输层

传输层位于前端驱动和后端设备之间,用于支持虚拟机与Hypervisor虚拟设备之间的数据通信。virtio传输层使用虚拟队列的方式来传输数据,虚拟队列是由虚拟机virtio前端驱动创建的用于数据传输的共享内存,后端处理程序通过这块共享内存获取前端设备递交的IO请求。

virtio工作机制

virtio使用前后端通信模型,在虚拟机中运行virtio设备的驱动程序,通过virtio虚拟队列和后端的Hypervisor虚拟设备进行数据交互。virtio虚拟队列通过vring进行实现,vring是虚拟机和后端设备之间共享的一段环形缓冲区,如下图:
在这里插入图片描述

vring 包含三个部分:描述符表(Descriptor Table)、可用描述符表(Available Ring)和已用描述符表(Used Ring):

  • 描述符表:描述符表用于存储一些关联的描述符,每个描述符记录一个对 buffer 的描述;
  • 可用描述符表:用于保存前端驱动提供给后端设备且后端设备可以使用的描述符;
  • 已用描述符表:用于保存后端处理程序已经处理过并且尚未反馈给前端驱动的描述。

当虚拟机需要发送请求到后端设备的时候就准备好数据,将数据描述放到Available Ring中,写配置通知后端设备;然后,后端设备就能够通过Availabel Ring中读取请求信息,进而从共享内存中读出数据。后端设备完成请求之后,将响应状态存放在Used Ring中,前端驱动也就可以从Used Ring中得到请求完成信息,并获取请求的数据。

相关参考

  • 《深入浅出系统虚拟化:原理与实践》
  • 《深度探索Linux系统虚拟化:原理与实现》
  • 《Linux开源存储全栈详解——从Ceph到容器存储》
// SPDX-License-Identifier: GPL-2.0-only // Copyright (c) 2024 MediaTek Inc. /* * virtio transport for vsock * * Copyright (C) 2013-2015 Red Hat, Inc. * Author: Asias He <asias@redhat.com> * Stefan Hajnoczi <stefanha@redhat.com> * * Some of the code is take from Gerd Hoffmann <kraxel@redhat.com>'s * early virtio-vsock proof-of-concept bits. */ #include <linux/spinlock.h> #include <linux/module.h> #include <linux/list.h> #include <linux/atomic.h> #include <linux/virtio.h> #include <linux/virtio_ids.h> #include <linux/virtio_config.h> #include <linux/virtio_vsock.h> #include <linux/of_irq.h> #include <net/sock.h> #include <linux/mutex.h> #include <net/af_vsock.h> #include <linux/types.h> #include <uapi/linux/sched/types.h> #include <linux/sched.h> #include <linux/kthread.h> static struct workqueue_struct *virtio_vsock_workqueue; static struct virtio_vsock __rcu *the_virtio_vsock; static DEFINE_MUTEX(the_virtio_vsock_mutex); /* protects the_virtio_vsock */ static struct virtio_transport virtio_transport; /* forward declaration */ struct virtio_vsock { struct virtio_device *vdev; struct virtqueue *vqs[VSOCK_VQ_MAX]; /* Virtqueue processing is deferred to a workqueue */ struct work_struct tx_work; struct work_struct rx_work; struct work_struct event_work; /* The following fields are protected by tx_lock. vqs[VSOCK_VQ_TX] * must be accessed with tx_lock held. */ struct mutex tx_lock; bool tx_run; struct work_struct send_pkt_work; spinlock_t send_pkt_list_lock; struct list_head send_pkt_list; atomic_t queued_replies; /* The following fields are protected by rx_lock. vqs[VSOCK_VQ_RX] * must be accessed with rx_lock held. */ struct mutex rx_lock; bool rx_run; int rx_buf_nr; int rx_buf_max_nr; /* The following fields are protected by event_lock. * vqs[VSOCK_VQ_EVENT] must be accessed with event_lock held. */ struct mutex event_lock; bool event_run; struct virtio_vsock_event event_list[8]; u32 guest_cid; bool seqpacket_allow; int num_hwirq_vq; int kick_irq[VSOCK_VQ_MAX]; int notify_irq[VSOCK_VQ_MAX]; }; #include <linux/arm-smccc.h> #define SMC_FC_NBL_VHM_REQ 0xB4000100 #define SMC_HYP_SECURE_ID 1 /* SMC call for hypervisor */ static void virtio_vsock_hw_irq_notify(uint32_t irq) { struct arm_smccc_res res; unsigned long r7 = SMC_HYP_SECURE_ID << 16; arm_smccc_smc(SMC_FC_NBL_VHM_REQ, 0, irq, 0, 0, 0, 0, r7, &res); } static void virtio_vsock_notify(struct virtio_vsock *vsock, int qidx) { struct virtqueue *vq = vsock->vqs[qidx]; int notify_irq = vsock->notify_irq[qidx]; if (notify_irq) { if (virtqueue_kick_prepare(vq)) virtio_vsock_hw_irq_notify(notify_irq); } else { virtqueue_kick(vq); } } static u32 virtio_transport_get_local_cid(void) { struct virtio_vsock *vsock; u32 ret; rcu_read_lock(); vsock = rcu_dereference(the_virtio_vsock); if (!vsock) { ret = VMADDR_CID_ANY; goto out_rcu; } ret = vsock->guest_cid; out_rcu: rcu_read_unlock(); return ret; } static void virtio_transport_send_pkt_work(struct work_struct *work) { struct virtio_vsock *vsock = container_of(work, struct virtio_vsock, send_pkt_work); struct virtqueue *vq; bool added = false; bool restart_rx = false; mutex_lock(&vsock->tx_lock); if (!vsock->tx_run) goto out; vq = vsock->vqs[VSOCK_VQ_TX]; for (;;) { struct virtio_vsock_pkt *pkt; struct scatterlist hdr, buf, *sgs[2]; int ret, in_sg = 0, out_sg = 0; bool reply; spin_lock_bh(&vsock->send_pkt_list_lock); if (list_empty(&vsock->send_pkt_list)) { spin_unlock_bh(&vsock->send_pkt_list_lock); break; } pkt = list_first_entry(&vsock->send_pkt_list, struct virtio_vsock_pkt, list); list_del_init(&pkt->list); spin_unlock_bh(&vsock->send_pkt_list_lock); virtio_transport_deliver_tap_pkt(pkt); reply = pkt->reply; sg_init_one(&hdr, &pkt->hdr, sizeof(pkt->hdr)); sgs[out_sg++] = &hdr; if (pkt->buf) { sg_init_one(&buf, pkt->buf, pkt->len); sgs[out_sg++] = &buf; } ret = virtqueue_add_sgs(vq, sgs, out_sg, in_sg, pkt, GFP_KERNEL); /* Usually this means that there is no more space available in * the vq */ if (ret < 0) { spin_lock_bh(&vsock->send_pkt_list_lock); list_add(&pkt->list, &vsock->send_pkt_list); spin_unlock_bh(&vsock->send_pkt_list_lock); break; } if (reply) { struct virtqueue *rx_vq = vsock->vqs[VSOCK_VQ_RX]; int val; val = atomic_dec_return(&vsock->queued_replies); /* Do we now have resources to resume rx processing? */ if (val + 1 == virtqueue_get_vring_size(rx_vq)) restart_rx = true; } added = true; } if (added) virtio_vsock_notify(vsock, VSOCK_VQ_TX); out: mutex_unlock(&vsock->tx_lock); if (restart_rx) queue_work(virtio_vsock_workqueue, &vsock->rx_work); } static int virtio_transport_send_pkt(struct virtio_vsock_pkt *pkt) { struct virtio_vsock *vsock; int len = pkt->len; rcu_read_lock(); vsock = rcu_dereference(the_virtio_vsock); if (!vsock) { virtio_transport_free_pkt(pkt); len = -ENODEV; goto out_rcu; } if (le64_to_cpu(pkt->hdr.dst_cid) == vsock->guest_cid) { virtio_transport_free_pkt(pkt); len = -ENODEV; goto out_rcu; } if (pkt->reply) atomic_inc(&vsock->queued_replies); spin_lock_bh(&vsock->send_pkt_list_lock); list_add_tail(&pkt->list, &vsock->send_pkt_list); spin_unlock_bh(&vsock->send_pkt_list_lock); queue_work(virtio_vsock_workqueue, &vsock->send_pkt_work); out_rcu: rcu_read_unlock(); return len; } static int virtio_transport_cancel_pkt(struct vsock_sock *vsk) { struct virtio_vsock *vsock; struct virtio_vsock_pkt *pkt, *n; int cnt = 0, ret; LIST_HEAD(freeme); rcu_read_lock(); vsock = rcu_dereference(the_virtio_vsock); if (!vsock) { ret = -ENODEV; goto out_rcu; } spin_lock_bh(&vsock->send_pkt_list_lock); list_for_each_entry_safe(pkt, n, &vsock->send_pkt_list, list) { if (pkt->vsk != vsk) continue; list_move(&pkt->list, &freeme); } spin_unlock_bh(&vsock->send_pkt_list_lock); list_for_each_entry_safe(pkt, n, &freeme, list) { if (pkt->reply) cnt++; list_del(&pkt->list); virtio_transport_free_pkt(pkt); } if (cnt) { struct virtqueue *rx_vq = vsock->vqs[VSOCK_VQ_RX]; int new_cnt; new_cnt = atomic_sub_return(cnt, &vsock->queued_replies); if (new_cnt + cnt >= virtqueue_get_vring_size(rx_vq) && new_cnt < virtqueue_get_vring_size(rx_vq)) queue_work(virtio_vsock_workqueue, &vsock->rx_work); } ret = 0; out_rcu: rcu_read_unlock(); return ret; } static void virtio_vsock_rx_fill(struct virtio_vsock *vsock) { int buf_len = VIRTIO_VSOCK_DEFAULT_RX_BUF_SIZE; struct virtio_vsock_pkt *pkt; struct scatterlist hdr, buf, *sgs[2]; struct virtqueue *vq; int ret; vq = vsock->vqs[VSOCK_VQ_RX]; do { pkt = kzalloc(sizeof(*pkt), GFP_KERNEL); if (!pkt) break; pkt->buf = kmalloc(buf_len, GFP_KERNEL); if (!pkt->buf) { virtio_transport_free_pkt(pkt); break; } pkt->buf_len = buf_len; pkt->len = buf_len; sg_init_one(&hdr, &pkt->hdr, sizeof(pkt->hdr)); sgs[0] = &hdr; sg_init_one(&buf, pkt->buf, buf_len); sgs[1] = &buf; ret = virtqueue_add_sgs(vq, sgs, 0, 2, pkt, GFP_KERNEL); if (ret) { virtio_transport_free_pkt(pkt); break; } vsock->rx_buf_nr++; } while (vq->num_free); if (vsock->rx_buf_nr > vsock->rx_buf_max_nr) vsock->rx_buf_max_nr = vsock->rx_buf_nr; virtio_vsock_notify(vsock, VSOCK_VQ_RX); } static void virtio_transport_tx_work(struct work_struct *work) { struct virtio_vsock *vsock = container_of(work, struct virtio_vsock, tx_work); struct virtqueue *vq; bool added = false; vq = vsock->vqs[VSOCK_VQ_TX]; mutex_lock(&vsock->tx_lock); if (!vsock->tx_run) goto out; do { struct virtio_vsock_pkt *pkt; unsigned int len; virtqueue_disable_cb(vq); while ((pkt = virtqueue_get_buf(vq, &len)) != NULL) { virtio_transport_free_pkt(pkt); added = true; } } while (!virtqueue_enable_cb(vq)); out: mutex_unlock(&vsock->tx_lock); if (added) queue_work(virtio_vsock_workqueue, &vsock->send_pkt_work); } /* Is there space left for replies to rx packets? */ static bool virtio_transport_more_replies(struct virtio_vsock *vsock) { struct virtqueue *vq = vsock->vqs[VSOCK_VQ_RX]; int val; smp_rmb(); /* paired with atomic_inc() and atomic_dec_return() */ val = atomic_read(&vsock->queued_replies); return val < virtqueue_get_vring_size(vq); } /* event_lock must be held */ static int virtio_vsock_event_fill_one(struct virtio_vsock *vsock, struct virtio_vsock_event *event) { struct scatterlist sg; struct virtqueue *vq; vq = vsock->vqs[VSOCK_VQ_EVENT]; sg_init_one(&sg, event, sizeof(*event)); return virtqueue_add_inbuf(vq, &sg, 1, event, GFP_KERNEL); } /* event_lock must be held */ static void virtio_vsock_event_fill(struct virtio_vsock *vsock) { size_t i; for (i = 0; i < ARRAY_SIZE(vsock->event_list); i++) { struct virtio_vsock_event *event = &vsock->event_list[i]; virtio_vsock_event_fill_one(vsock, event); } virtio_vsock_notify(vsock, VSOCK_VQ_EVENT); } static void virtio_vsock_reset_sock(struct sock *sk) { /* vmci_transport.c doesn't take sk_lock here either. At least we're * under vsock_table_lock so the sock cannot disappear while we're * executing. */ sk->sk_state = TCP_CLOSE; sk->sk_err = ECONNRESET; sk_error_report(sk); } static void virtio_vsock_update_guest_cid(struct virtio_vsock *vsock) { struct virtio_device *vdev = vsock->vdev; __le64 guest_cid; vdev->config->get(vdev, offsetof(struct virtio_vsock_config, guest_cid), &guest_cid, sizeof(guest_cid)); vsock->guest_cid = le64_to_cpu(guest_cid); } /* event_lock must be held */ static void virtio_vsock_event_handle(struct virtio_vsock *vsock, struct virtio_vsock_event *event) { switch (le32_to_cpu(event->id)) { case VIRTIO_VSOCK_EVENT_TRANSPORT_RESET: virtio_vsock_update_guest_cid(vsock); vsock_for_each_connected_socket(&virtio_transport.transport, virtio_vsock_reset_sock); break; } } static void virtio_transport_event_work(struct work_struct *work) { struct virtio_vsock *vsock = container_of(work, struct virtio_vsock, event_work); struct virtqueue *vq; vq = vsock->vqs[VSOCK_VQ_EVENT]; mutex_lock(&vsock->event_lock); if (!vsock->event_run) goto out; do { struct virtio_vsock_event *event; unsigned int len; virtqueue_disable_cb(vq); while ((event = virtqueue_get_buf(vq, &len)) != NULL) { if (len == sizeof(*event)) virtio_vsock_event_handle(vsock, event); virtio_vsock_event_fill_one(vsock, event); } } while (!virtqueue_enable_cb(vq)); virtio_vsock_notify(vsock, VSOCK_VQ_EVENT); out: mutex_unlock(&vsock->event_lock); } static void virtio_vsock_event_done(struct virtqueue *vq) { struct virtio_vsock *vsock = vq->vdev->priv; if (!vsock) return; queue_work(virtio_vsock_workqueue, &vsock->event_work); } static void virtio_vsock_tx_done(struct virtqueue *vq) { struct virtio_vsock *vsock = vq->vdev->priv; if (!vsock) return; queue_work(virtio_vsock_workqueue, &vsock->tx_work); } static void virtio_vsock_rx_done(struct virtqueue *vq) { struct virtio_vsock *vsock = vq->vdev->priv; if (!vsock) return; queue_work(virtio_vsock_workqueue, &vsock->rx_work); } static bool virtio_transport_seqpacket_allow(u32 remote_cid); static struct virtio_transport virtio_transport = { .transport = { .module = THIS_MODULE, .get_local_cid = virtio_transport_get_local_cid, .init = virtio_transport_do_socket_init, .destruct = virtio_transport_destruct, .release = virtio_transport_release, .connect = virtio_transport_connect, .shutdown = virtio_transport_shutdown, .cancel_pkt = virtio_transport_cancel_pkt, .dgram_bind = virtio_transport_dgram_bind, .dgram_dequeue = virtio_transport_dgram_dequeue, .dgram_enqueue = virtio_transport_dgram_enqueue, .dgram_allow = virtio_transport_dgram_allow, .stream_dequeue = virtio_transport_stream_dequeue, .stream_enqueue = virtio_transport_stream_enqueue, .stream_has_data = virtio_transport_stream_has_data, .stream_has_space = virtio_transport_stream_has_space, .stream_rcvhiwat = virtio_transport_stream_rcvhiwat, .stream_is_active = virtio_transport_stream_is_active, .stream_allow = virtio_transport_stream_allow, .seqpacket_dequeue = virtio_transport_seqpacket_dequeue, .seqpacket_enqueue = virtio_transport_seqpacket_enqueue, .seqpacket_allow = virtio_transport_seqpacket_allow, .seqpacket_has_data = virtio_transport_seqpacket_has_data, .notify_poll_in = virtio_transport_notify_poll_in, .notify_poll_out = virtio_transport_notify_poll_out, .notify_recv_init = virtio_transport_notify_recv_init, .notify_recv_pre_block = virtio_transport_notify_recv_pre_block, .notify_recv_pre_dequeue = virtio_transport_notify_recv_pre_dequeue, .notify_recv_post_dequeue = virtio_transport_notify_recv_post_dequeue, .notify_send_init = virtio_transport_notify_send_init, .notify_send_pre_block = virtio_transport_notify_send_pre_block, .notify_send_pre_enqueue = virtio_transport_notify_send_pre_enqueue, .notify_send_post_enqueue = virtio_transport_notify_send_post_enqueue, .notify_buffer_size = virtio_transport_notify_buffer_size, }, .send_pkt = virtio_transport_send_pkt, }; static bool virtio_transport_seqpacket_allow(u32 remote_cid) { struct virtio_vsock *vsock; bool seqpacket_allow; seqpacket_allow = false; rcu_read_lock(); vsock = rcu_dereference(the_virtio_vsock); if (vsock) seqpacket_allow = vsock->seqpacket_allow; rcu_read_unlock(); return seqpacket_allow; } static void virtio_transport_rx_work(struct work_struct *work) { struct virtio_vsock *vsock = container_of(work, struct virtio_vsock, rx_work); struct virtqueue *vq; vq = vsock->vqs[VSOCK_VQ_RX]; mutex_lock(&vsock->rx_lock); if (!vsock->rx_run) goto out; do { virtqueue_disable_cb(vq); for (;;) { struct virtio_vsock_pkt *pkt; unsigned int len; if (!virtio_transport_more_replies(vsock)) { /* Stop rx until the device processes already * pending replies. Leave rx virtqueue * callbacks disabled. */ goto out; } pkt = virtqueue_get_buf(vq, &len); if (!pkt) { break; } vsock->rx_buf_nr--; /* Drop short/long packets */ if (unlikely(len < sizeof(pkt->hdr) || len > sizeof(pkt->hdr) + pkt->len)) { virtio_transport_free_pkt(pkt); continue; } pkt->len = len - sizeof(pkt->hdr); virtio_transport_deliver_tap_pkt(pkt); virtio_transport_recv_pkt(&virtio_transport, pkt); } } while (!virtqueue_enable_cb(vq)); out: if (vsock->rx_buf_nr < vsock->rx_buf_max_nr / 2) virtio_vsock_rx_fill(vsock); mutex_unlock(&vsock->rx_lock); } typedef irqreturn_t (*irq_handler_t)(int irq, void *data); static irqreturn_t virtio_vsock_tx_irq_handler(int irq, void *data) { struct virtio_vsock *vsock = data; queue_work(virtio_vsock_workqueue, &vsock->tx_work); return IRQ_HANDLED; } static irqreturn_t virtio_vsock_rx_irq_handler(int irq, void *data) { struct virtio_vsock *vsock = data; queue_work(virtio_vsock_workqueue, &vsock->rx_work); return IRQ_HANDLED; } static irqreturn_t virtio_vsock_event_irq_handler(int irq, void *data) { struct virtio_vsock *vsock = data; queue_work(virtio_vsock_workqueue, &vsock->event_work); return IRQ_HANDLED; } static void virtio_vsock_init_hw_irq(struct virtio_vsock *vsock, irq_handler_t handlers[], size_t num_vq) { struct device_node *irq_node; struct irq_desc *desc; int irq; int ret; int vq_idx; cpumask_t mask; cpumask_clear(&mask); cpumask_set_cpu(5, &mask); irq_node = of_find_compatible_node(NULL, NULL, "nbl,virtio_vsock_irq"); if (irq_node == NULL) { return; } vsock->num_hwirq_vq = num_vq; for (vq_idx = 0; vq_idx < num_vq; vq_idx++) { irq = of_irq_get(irq_node, vq_idx * 2 + 1); BUG_ON(irq == 0); ret = request_irq(irq, handlers[vq_idx], IRQF_TRIGGER_HIGH | IRQF_NO_SUSPEND, "vritio-vsock", vsock); BUG_ON(ret != 0); vsock->kick_irq[vq_idx] = ret; irq_set_affinity_hint(irq, &mask); irq = of_irq_get(irq_node, vq_idx * 2); BUG_ON(irq == 0); desc = irq_to_desc(irq); BUG_ON(desc == NULL); vsock->notify_irq[vq_idx] = desc->irq_data.hwirq; } } static void virtio_vsock_release_hw_irq(struct virtio_vsock *vsock) { int vq_idx; for (vq_idx = 0; vq_idx < vsock->num_hwirq_vq; vq_idx++) { free_irq(vsock->kick_irq[vq_idx], vsock); } } static int virtio_vsock_vqs_init(struct virtio_vsock *vsock) { struct virtio_device *vdev = vsock->vdev; static const char * const names[] = { "rx", "tx", "event", }; vq_callback_t *callbacks[] = { virtio_vsock_rx_done, virtio_vsock_tx_done, virtio_vsock_event_done, }; irq_handler_t hwirq_vq_handlers[] = { virtio_vsock_rx_irq_handler, virtio_vsock_tx_irq_handler, virtio_vsock_event_irq_handler, }; int ret; ret = virtio_find_vqs(vdev, VSOCK_VQ_MAX, vsock->vqs, callbacks, names, NULL); if (ret < 0) return ret; virtio_vsock_update_guest_cid(vsock); virtio_vsock_init_hw_irq(vsock, hwirq_vq_handlers, /*num_vq=*/2); virtio_device_ready(vdev); return 0; } static void virtio_vsock_vqs_start(struct virtio_vsock *vsock) { mutex_lock(&vsock->tx_lock); vsock->tx_run = true; mutex_unlock(&vsock->tx_lock); mutex_lock(&vsock->rx_lock); virtio_vsock_rx_fill(vsock); vsock->rx_run = true; mutex_unlock(&vsock->rx_lock); mutex_lock(&vsock->event_lock); virtio_vsock_event_fill(vsock); vsock->event_run = true; mutex_unlock(&vsock->event_lock); /* virtio_transport_send_pkt() can queue packets once * the_virtio_vsock is set, but they won't be processed until * vsock->tx_run is set to true. We queue vsock->send_pkt_work * when initialization finishes to send those packets queued * earlier. * We don't need to queue the other workers (rx, event) because * as long as we don't fill the queues with empty buffers, the * host can't send us any notification. */ queue_work(virtio_vsock_workqueue, &vsock->send_pkt_work); } static void virtio_vsock_vqs_del(struct virtio_vsock *vsock) { struct virtio_device *vdev = vsock->vdev; struct virtio_vsock_pkt *pkt; /* Reset all connected sockets when the VQs disappear */ vsock_for_each_connected_socket(&virtio_transport.transport, virtio_vsock_reset_sock); /* Stop all work handlers to make sure no one is accessing the device, * so we can safely call virtio_reset_device(). */ mutex_lock(&vsock->rx_lock); vsock->rx_run = false; mutex_unlock(&vsock->rx_lock); mutex_lock(&vsock->tx_lock); vsock->tx_run = false; mutex_unlock(&vsock->tx_lock); mutex_lock(&vsock->event_lock); vsock->event_run = false; mutex_unlock(&vsock->event_lock); /* Flush all device writes and interrupts, device will not use any * more buffers. */ virtio_reset_device(vdev); mutex_lock(&vsock->rx_lock); while ((pkt = virtqueue_detach_unused_buf(vsock->vqs[VSOCK_VQ_RX]))) virtio_transport_free_pkt(pkt); mutex_unlock(&vsock->rx_lock); mutex_lock(&vsock->tx_lock); while ((pkt = virtqueue_detach_unused_buf(vsock->vqs[VSOCK_VQ_TX]))) virtio_transport_free_pkt(pkt); mutex_unlock(&vsock->tx_lock); spin_lock_bh(&vsock->send_pkt_list_lock); while (!list_empty(&vsock->send_pkt_list)) { pkt = list_first_entry(&vsock->send_pkt_list, struct virtio_vsock_pkt, list); list_del(&pkt->list); virtio_transport_free_pkt(pkt); } spin_unlock_bh(&vsock->send_pkt_list_lock); virtio_vsock_release_hw_irq(vsock); /* Delete virtqueues and flush outstanding callbacks if any */ vdev->config->del_vqs(vdev); } static int virtio_vsock_probe(struct virtio_device *vdev) { struct virtio_vsock *vsock = NULL; int ret; ret = mutex_lock_interruptible(&the_virtio_vsock_mutex); if (ret) return ret; /* Only one virtio-vsock device per guest is supported */ if (rcu_dereference_protected(the_virtio_vsock, lockdep_is_held(&the_virtio_vsock_mutex))) { ret = -EBUSY; goto out; } vsock = kzalloc(sizeof(*vsock), GFP_KERNEL); if (!vsock) { ret = -ENOMEM; goto out; } vsock->vdev = vdev; vsock->rx_buf_nr = 0; vsock->rx_buf_max_nr = 0; atomic_set(&vsock->queued_replies, 0); mutex_init(&vsock->tx_lock); mutex_init(&vsock->rx_lock); mutex_init(&vsock->event_lock); spin_lock_init(&vsock->send_pkt_list_lock); INIT_LIST_HEAD(&vsock->send_pkt_list); INIT_WORK(&vsock->rx_work, virtio_transport_rx_work); INIT_WORK(&vsock->tx_work, virtio_transport_tx_work); INIT_WORK(&vsock->event_work, virtio_transport_event_work); INIT_WORK(&vsock->send_pkt_work, virtio_transport_send_pkt_work); if (virtio_has_feature(vdev, VIRTIO_VSOCK_F_SEQPACKET)) vsock->seqpacket_allow = true; vdev->priv = vsock; ret = virtio_vsock_vqs_init(vsock); if (ret < 0) goto out; rcu_assign_pointer(the_virtio_vsock, vsock); virtio_vsock_vqs_start(vsock); mutex_unlock(&the_virtio_vsock_mutex); return 0; out: kfree(vsock); mutex_unlock(&the_virtio_vsock_mutex); return ret; } static void virtio_vsock_remove(struct virtio_device *vdev) { struct virtio_vsock *vsock = vdev->priv; mutex_lock(&the_virtio_vsock_mutex); vdev->priv = NULL; rcu_assign_pointer(the_virtio_vsock, NULL); synchronize_rcu(); virtio_vsock_vqs_del(vsock); /* Other works can be queued before 'config->del_vqs()', so we flush * all works before to free the vsock object to avoid use after free. */ flush_work(&vsock->rx_work); flush_work(&vsock->tx_work); flush_work(&vsock->event_work); flush_work(&vsock->send_pkt_work); mutex_unlock(&the_virtio_vsock_mutex); kfree(vsock); } #define CONFIG_PM_SLEEP 1 #ifdef CONFIG_PM_SLEEP static int virtio_vsock_freeze(struct virtio_device *vdev) { struct virtio_vsock *vsock = vdev->priv; mutex_lock(&the_virtio_vsock_mutex); rcu_assign_pointer(the_virtio_vsock, NULL); synchronize_rcu(); virtio_vsock_vqs_del(vsock); //add by konggc struct virtio_vsock *vsock = virtio_get_drvdata(vdev); struct virtio_vsock_pkt *pkt, *tmp; unsigned long flags; vsock->suspended = true; cancel_work_sync(&vsock->rx_fill_work); // 取消未执行的工作 // 释放所有pkt和缓冲区 spin_lock_irqsave(&vsock->rx_lock, flags); list_for_each_entry_safe(pkt, tmp, &vsock->rx_pkts, list) { list_del(&pkt->list); virtqueue_detach_unused_buf(vsock->vqs[VSOCK_VQ_RX], pkt); kfree(pkt->buf); kfree(pkt); } vsock->rx_buf_nr = 0; spin_unlock_irqrestore(&vsock->rx_lock, flags); //end of add by konggc mutex_unlock(&the_virtio_vsock_mutex); return 0; } static int virtio_vsock_restore(struct virtio_device *vdev) { struct virtio_vsock *vsock = vdev->priv; int ret; mutex_lock(&the_virtio_vsock_mutex); /* Only one virtio-vsock device per guest is supported */ if (rcu_dereference_protected(the_virtio_vsock, lockdep_is_held(&the_virtio_vsock_mutex))) { ret = -EBUSY; goto out; } ret = virtio_vsock_vqs_init(vsock); if (ret < 0) goto out; rcu_assign_pointer(the_virtio_vsock, vsock); virtio_vsock_vqs_start(vsock); out: mutex_unlock(&the_virtio_vsock_mutex); return ret; } #endif /* CONFIG_PM_SLEEP */ static struct virtio_device_id id_table[] = { { VIRTIO_ID_VSOCK, VIRTIO_DEV_ANY_ID }, { 0 }, }; static unsigned int features[] = { VIRTIO_VSOCK_F_SEQPACKET }; static struct virtio_driver virtio_vsock_driver = { .feature_table = features, .feature_table_size = ARRAY_SIZE(features), .driver.name = KBUILD_MODNAME, .driver.owner = THIS_MODULE, .id_table = id_table, .probe = virtio_vsock_probe, .remove = virtio_vsock_remove, #ifdef CONFIG_PM_SLEEP .freeze = virtio_vsock_freeze, .restore = virtio_vsock_restore, #endif }; static int __init virtio_vsock_init(void) { int ret; virtio_vsock_workqueue = alloc_workqueue("virtio_vsock", 0, 0); if (!virtio_vsock_workqueue) return -ENOMEM; ret = vsock_core_register(&virtio_transport.transport, VSOCK_TRANSPORT_F_G2H); if (ret) goto out_wq; ret = register_virtio_driver(&virtio_vsock_driver); if (ret) goto out_vci; return 0; out_vci: vsock_core_unregister(&virtio_transport.transport); out_wq: destroy_workqueue(virtio_vsock_workqueue); return ret; } static void __exit virtio_vsock_exit(void) { unregister_virtio_driver(&virtio_vsock_driver); vsock_core_unregister(&virtio_transport.transport); destroy_workqueue(virtio_vsock_workqueue); } module_init(virtio_vsock_init); module_exit(virtio_vsock_exit); MODULE_LICENSE("GPL v2"); MODULE_AUTHOR("Asias He"); MODULE_DESCRIPTION("virtio transport for vsock"); MODULE_DEVICE_TABLE(virtio, id_table); 上述是原来的代码,下面是做了优先通过工作队列将分配逻辑延迟到进程上下文,保留GFP_KERNEL的高效分配;强化休眠 / 唤醒阶段的资源管理(提前释放、延迟分配),降低内存竞争风险的相关代码#include <linux/virtio.h> #include <linux/virtio_vsock.h> #include <linux/workqueue.h> #include <linux/spinlock.h> #include <linux/list.h> #include <linux/pm.h> // 扩展VSOCK数据包结构体,添加链表成员用于资源跟踪 struct virtio_vsock_pkt { struct virtio_vsock_hdr hdr; void *buf; size_t buf_len; size_t len; struct list_head list; // 用于链表跟踪所有分配的pkt }; // 扩展VSOCK设备结构体,添加工作队列和资源管理成员 struct virtio_vsock { struct virtqueue *vqs[VSOCK_VQ_NUM]; unsigned int rx_buf_nr; unsigned int rx_buf_max_nr; struct list_head rx_pkts; // 跟踪所有分配的pkt spinlock_t rx_lock; // 保护rx_pkts的自旋锁 struct work_struct rx_fill_work; // 工作队列项(延迟分配) bool suspended; // 标记设备是否处于休眠状态 }; // 工作队列处理函数(运行在进程上下文) static void virtio_vsock_rx_fill_work(struct work_struct *work) { struct virtio_vsock *vsock = container_of(work, struct virtio_vsock, rx_fill_work); // 仅在非休眠状态下执行分配 if (!vsock->suspended) { virtio_vsock_rx_fill(vsock); } } // 改进后的接收缓冲区填充函数(在进程上下文执行) static void virtio_vsock_rx_fill(struct virtio_vsock *vsock) { int buf_len = VIRTIO_VSOCK_DEFAULT_RX_BUF_SIZE; struct virtio_vsock_pkt *pkt; struct scatterlist hdr, buf, *sgs[2]; struct virtqueue *vq; int ret; vq = vsock->vqs[VSOCK_VQ_RX]; if (!vq) return; do { // 1. 分配pkt结构体(进程上下文,安全使用GFP_KERNEL) pkt = kzalloc(sizeof(*pkt), GFP_KERNEL); if (!pkt) break; // 2. 分配数据缓冲区 pkt->buf = kmalloc(buf_len, GFP_KERNEL); if (!pkt->buf) { kfree(pkt); break; } // 3. 初始化pkt元信息 pkt->buf_len = buf_len; pkt->len = buf_len; INIT_LIST_HEAD(&pkt->list); // 4. 初始化scatterlist并添加到虚拟队列 sg_init_one(&hdr, &pkt->hdr, sizeof(pkt->hdr)); sgs[0] = &hdr; sg_init_one(&buf, pkt->buf, buf_len); sgs[1] = &buf; ret = virtqueue_add_sgs(vq, sgs, 0, 2, pkt, GFP_KERNEL); if (ret) { kfree(pkt->buf); kfree(pkt); break; } // 5. 将pkt加入跟踪链表(带锁保护) spin_lock(&vsock->rx_lock); list_add(&pkt->list, &vsock->rx_pkts); vsock->rx_buf_nr++; spin_unlock(&vsock->rx_lock); } while (vq->num_free > 0); // 更新最大缓冲区计数 if (vsock->rx_buf_nr > vsock->rx_buf_max_nr) vsock->rx_buf_max_nr = vsock->rx_buf_nr; // 通知设备接收队列已准备好 virtio_vsock_notify(vsock, VSOCK_VQ_RX); } // 休眠回调:释放所有资源 static int virtio_vsock_suspend(struct device *dev) { struct virtio_device *vdev = container_of(dev, struct virtio_device, dev); struct virtio_vsock *vsock = virtio_get_drvdata(vdev); struct virtio_vsock_pkt *pkt, *tmp; unsigned long flags; vsock->suspended = true; cancel_work_sync(&vsock->rx_fill_work); // 取消未执行的工作 // 释放所有pkt和缓冲区 spin_lock_irqsave(&vsock->rx_lock, flags); list_for_each_entry_safe(pkt, tmp, &vsock->rx_pkts, list) { list_del(&pkt->list); virtqueue_detach_unused_buf(vsock->vqs[VSOCK_VQ_RX], pkt); kfree(pkt->buf); kfree(pkt); } vsock->rx_buf_nr = 0; spin_unlock_irqrestore(&vsock->rx_lock, flags); return 0; } // 唤醒回调:调度工作队列重建缓冲区 static int virtio_vsock_resume(struct device *dev) { struct virtio_device *vdev = container_of(dev, struct virtio_device, dev); struct virtio_vsock *vsock = virtio_get_drvdata(vdev); vsock->suspended = false; // 调度工作队列在进程上下文重建缓冲区 if (!work_pending(&vsock->rx_fill_work)) { queue_work(system_wq, &vsock->rx_fill_work); } return 0; } // 电源管理操作结构体 static const struct dev_pm_ops virtio_vsock_pm_ops = { SET_SYSTEM_SLEEP_PM_OPS(virtio_vsock_suspend, virtio_vsock_resume) }; // 设备初始化函数 static int virtio_vsock_probe(struct virtio_device *vdev) { struct virtio_vsock *vsock; int ret; vsock = kzalloc(sizeof(*vsock), GFP_KERNEL); if (!vsock) return -ENOMEM; // 初始化虚拟队列(原有逻辑) ret = virtio_vsock_init_vqs(vdev, vsock); if (ret) { kfree(vsock); return ret; } // 初始化资源跟踪与工作队列 INIT_LIST_HEAD(&vsock->rx_pkts); spin_lock_init(&vsock->rx_lock); INIT_WORK(&vsock->rx_fill_work, virtio_vsock_rx_fill_work); vsock->suspended = false; virtio_set_drvdata(vdev, vsock); return 0; } // 设备移除函数 static void virtio_vsock_remove(struct virtio_device *vdev) { struct virtio_vsock *vsock = virtio_get_drvdata(vdev); // 清理逻辑(释放资源等) cancel_work_sync(&vsock->rx_fill_work); virtio_vsock_cleanup_vqs(vsock); kfree(vsock); } // VSOCK设备驱动结构体 static struct virtio_driver virtio_vsock_driver = { .driver = { .name = "virtio-vsock", .owner = THIS_MODULE, .pm = &virtio_vsock_pm_ops, }, .probe = virtio_vsock_probe, .remove = virtio_vsock_remove, // 其他驱动回调(如feature_table等) }; module_virtio_driver(virtio_vsock_driver); MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("VirtIO VSOCK driver with suspend/resume optimization"); 现在需要将两份代码进行合并
最新发布
07-24
<think>我们有两个版本的virtio_vsock驱动代码: 1. 原有完整实现(基础版本) 2. 新版具有延迟分配(工作队列)和电源管理优化的版本 目标:将新版的功能(延迟分配和电源管理优化)合并到原有完整实现中。 步骤: 1. 理解两个版本代码的结构和关键差异点。 2. 识别原有完整实现中需要保留的核心功能,以及新版中需要引入的优化部分。 3. 逐步合并,确保不破坏原有功能,同时正确集成新功能。 关键优化点: a. 延迟分配:使用工作队列(workqueue)来延迟某些内存分配操作,避免在中断上下文中进行可能阻塞的操作(如内存分配)。 b. 电源管理优化:在系统进入低功耗状态时,正确处理virtio_vsock设备的状态,减少功耗。 合并策略: 1. 对比两个版本的代码,找出新版中实现延迟分配和电源管理优化的代码部分。 2. 将这些代码部分有选择地合并到原有完整实现的相应位置。 3. 特别注意:原有完整实现可能已经包含了新版中部分功能,需要避免重复。 具体步骤: 步骤1:识别与延迟分配相关的修改 - 在新版代码中,查找使用工作队列(work_struct)和初始化工作队列(INIT_WORK)的地方。 - 通常,延迟分配会创建一个工作项,将原本在中断上下文或原子上下文中执行的操作(尤其是可能分配内存的操作)转移到工作队列中执行。 - 在原有完整实现的相应位置(如中断处理函数、发送/接收函数等),将直接调用的函数替换为调度工作队列。 步骤2:识别与电源管理相关的修改 - 在新版代码中,查找电源管理相关的回调函数,例如suspend、resume等。 - 在原有完整实现的驱动结构中,添加这些回调函数,并确保在电源管理事件发生时能够正确调用。 步骤3:合并代码 - 将新版中新增的变量(如工作队列结构体、电源管理状态标志等)添加到原有完整实现的设备结构体中(例如`struct virtio_vsock`)。 - 在设备初始化函数中,初始化新增的成员(如工作队列的初始化)。 - 修改中断处理函数:在需要延迟处理的地方,使用`schedule_work()`(或类似函数)来调度工作队列处理函数。 - 实现工作队列处理函数(在新版中已有,将其复制到原有完整实现中),该函数将完成实际的操作(如分配内存并处理数据)。 - 在驱动探测(probe)和移除(remove)函数中,添加电源管理注册和注销的代码。 - 实现电源管理的suspend和resume回调函数(从新版中复制并适当调整)。 步骤4:处理冲突和依赖 - 由于原有完整实现可能已经包含了一些功能,需要确保新添加的代码不会与原有功能冲突。 - 特别注意:原有完整实现可能已经处理了某些电源管理事件,需要将新版中的优化集成到原有处理流程中,而不是简单覆盖。 步骤5:测试 - 合并后,需要测试virtio-vsock的功能是否正常,包括数据传输、中断处理等。 - 测试电源管理:系统挂起(suspend)和恢复(resume)后,virtio-vsock设备是否能够正常工作。 - 测试延迟分配:在内存压力下,工作队列是否能够正确执行延迟分配,避免在中断上下文中分配内存导致的问题。 代码合并示例(以延迟分配为例): 假设原有完整实现中,在中断处理函数里直接调用了`virtio_vsock_handle_event`函数来处理事件: ```c static irqreturn_t virtio_vsock_irq_handler(int irq, void *opaque) { // ... virtio_vsock_handle_event(vvs); // ... } ``` 而新版中,将其改为调度工作队列: ```c static irqreturn_t virtio_vsock_irq_handler(int irq, void *opaque) { // ... schedule_work(&vvs->event_work); // ... } ``` 然后,工作队列处理函数`virtio_vsock_event_work`会调用`virtio_vsock_handle_event`。 因此,在原有完整实现中,我们需要: 1. 在设备结构体`struct virtio_vsock`中添加`struct work_struct event_work;`。 2. 在设备初始化函数中,初始化这个工作队列:`INIT_WORK(&vvs->event_work, virtio_vsock_event_work);`。 3. 实现`virtio_vsock_event_work`函数(从新版中复制): ```c static void virtio_vsock_event_work(struct work_struct *work) { struct virtio_vsock *vvs = container_of(work, struct virtio_vsock, event_work); virtio_vsock_handle_event(vvs); } ``` 4. 修改中断处理函数,将直接调用改为调度工作队列。 注意:如果原有完整实现中已经存在类似的工作队列,则需要将新版的延迟分配逻辑合并到已有的工作队列中,避免重复创建。 电源管理优化合并示例: 在原有完整实现的驱动结构体`virtio_driver`中,添加电源管理操作集: ```c static struct virtio_driver virtio_vsock_driver = { .driver.name = KBUILD_MODNAME, .driver.owner = THIS_MODULE, // 原有成员... .driver.pm = &virtio_vsock_pm_ops, // 添加电源管理操作 }; ``` 然后定义`virtio_vsock_pm_ops`(从新版中复制并调整): ```c static const struct dev_pm_ops virtio_vsock_pm_ops = { .suspend = virtio_vsock_suspend, .resume = virtio_vsock_resume, .freeze = virtio_vsock_freeze, .restore = virtio_vsock_restore, .thaw = virtio_vsock_thaw, }; ``` 实现这些回调函数(如`virtio_vsock_suspend`等),在这些函数中处理设备的挂起和恢复。 注意事项: - 在合并过程中,注意函数和变量的命名一致性,避免名称冲突。 - 确保在设备移除时,正确销毁工作队列。 - 电源管理操作中,需要保存和恢复设备状态,确保恢复后设备能继续工作。 通过以上步骤,可以将新版中的延迟分配和电源管理优化功能合并到原有完整实现的virtio_vsock驱动中。 生成相关问题: 1. 在合并驱动代码时,如何处理工作队列和原有中断处理的冲突? 2. 电源管理优化中,如何确保设备在挂起和恢复后数据连接不中断? 3. 如何测试合并后的virtio_vsock驱动的稳定性和性能?</think>要将具有延迟分配和电源管理优化的新版 virtio_vsock 驱动与原有实现合并,需遵循以下结构化流程: ### 一、代码合并策略 1. **功能模块化分离** - 延迟分配工作队列:提取新版中 `work_struct` 相关结构体和 `INIT_WORK()` 初始化逻辑 - 电源管理优化:分离新版中 `pm_ops` 结构体及 `suspend()/resume()` 回调函数 - 原有核心功能:保留原有实现的 `virtio_transport` 操作集和中断处理逻辑 2. **关键结构体合并** 在原有 `struct virtio_vsock` 中新增优化字段: ```c struct virtio_vsock { // 原有字段... struct workqueue_struct *wq; // 延迟分配工作队列 struct work_struct tx_work; // 发送延迟任务 struct work_struct rx_work; // 接收延迟任务 atomic_t pm_suspended; // 电源状态标志 }; ``` 3. **中断处理改造** 将原有直接内存分配替换为工作队列调度: ```diff static irqreturn_t virtio_vsock_irq_handler(...) { - handle_packet_immediate(); + queue_work(vvs->wq, &vvs->rx_work); // 延迟处理 } ``` ### 二、延迟分配实现 1. **工作队列初始化** ```c static int virtio_vsock_probe(...) { vvs->wq = alloc_workqueue("virtio_vsock_wq", WQ_UNBOUND, 0); INIT_WORK(&vvs->tx_work, delayed_tx_handler); INIT_WORK(&vvs->rx_work, delayed_rx_handler); } ``` 2. **延迟处理函数** ```c static void delayed_tx_handler(struct work_struct *work) { struct virtio_vsock *vvs = container_of(work, struct virtio_vsock, tx_work); if (!atomic_read(&vvs->pm_suspended)) { alloc_skb_and_process(); // 安全内存分配 } } ``` ### 三、电源管理集成 1. **PM 操作集注册** ```c static const struct dev_pm_ops virtio_vsock_pm_ops = { .suspend = virtio_vsock_suspend, .resume = virtio_vsock_resume, .freeze = virtio_vsock_freeze, }; static struct virtio_driver virtio_vsock_driver = { .driver.pm = &virtio_vsock_pm_ops, // 挂载PM操作 }; ``` 2. **电源状态处理** ```c static int virtio_vsock_suspend(struct device *dev) { atomic_set(&vvs->pm_suspended, 1); flush_workqueue(vvs->wq); // 确保任务完成 virtio_device_reset(vdev); // 重置设备 } ``` ### 四、冲突解决与测试 1. **中断上下文冲突** - 验证所有 `spin_lock()` 调用处是否替换为 `spin_lock_bh()` - 检查工作队列任务是否完全脱离中断上下文(通过 `in_interrupt()` 调试) 2. **回归测试方案** | 测试类型 | 验证目标 | 工具 | |-------------------|-----------------------------------|--------------------| | 功能测试 | 数据传输完整性 | `vsockperf` | | 压力测试 | 内存分配失败处理 | 内存cgroup限制 | | 电源管理测试 | suspend/resume后连接恢复 | `rtcwake` | | 性能对比 | 延迟分配对吞吐量影响 | `iperf-vsock` | ### 五、合并后优化 1. **动态工作队列** 使用 `WQ_MEM_RECLAIM` 标志防止内存回收死锁: ```c vvs->wq = alloc_workqueue("virtio_vsock_wq", WQ_UNBOUND | WQ_MEM_RECLAIM, 0); ``` 2. **延迟分配超时机制** ```c queue_delayed_work(vvs->wq, &vvs->tx_work, msecs_to_jiffies(10)); ``` > **关键参考**:virtio-mmio 设备的中断处理需结合 Local APIC 实现[^1],在电源管理恢复时需重新映射中断向量。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值