VirtIO实现原理——数据传输演示

初始化

示意图
  • 假设Host配置的virtqueue队列深度是10,初始化时Descriptor Table有10个条目,用于存放10个buffer指针,如下图所示,Descriptor Table的每一个条目初始化时都指向数组的下一个元素,所有10个entry组成一个descriptorchain。virtqueue的关键成员初始值如下:
    1. num_free:10,表示有10个空闲buffer
    2. free_head:0,表示当前Descriptor Table中,空闲buffer的头是第0个entry
    3. avail_idx_shadow:0,表示下一次Guest添加buffer后,Avail Ring中记录其头部的entry。这里为0,表示下一次Guest添加buffer后将其头部记录到Avail Ring的第0个entry Avail Ring
    成员初始化如下:
    4. idx:0,表示Guest下一次添加buffer将把其头部记录到Avail Ring的第0个entry

  • . Host VQ关键成员初始值如下:

    1. last_avail_idx:0,Host自己也维护了一个Avail Ring可用buffer的索引,用来记录自己实际的工作位置,当 Guest告知的可用buffer索引比自己的大时,说名Guest添加了新的可用buffer,Host需要处理
    2. shadow_avail_idx::0,Host从VQ中取出Guest设置的avail_idx_shadow,存放到shadow_avail_idx中,其初始化为0
      在这里插入图片描述
    代码分析
  • Guest初始化virtqueue的函数如下

truct virtqueue *__vring_new_virtqueue(unsigned int index,
                    struct vring vring,
                    struct virtio_device *vdev,
                    bool weak_barriers,
                    bool context,
                    bool (*notify)(struct virtqueue *),
                    void (*callback)(struct virtqueue *),
                    const char *name)
{
    unsigned int i;
    struct vring_virtqueue *vq;

    vq = kmalloc(sizeof(*vq) + vring.num * sizeof(struct vring_desc_state),
             GFP_KERNEL);
    if (!vq)
        return NULL;

    vq->vring = vring;
    vq->vq.callback = callback;
    vq->vq.vdev = vdev;
    vq->vq.name = name;
    vq->vq.num_free = vring.num;
    vq->vq.index = index;		// 设置的是设备的第几个virtqueue
    vq->we_own_ring = false;
    vq->queue_dma_addr = 0;
    vq->queue_size_in_bytes = 0;
    vq->notify = notify;	
    vq->weak_barriers = weak_barriers;
    vq->broken = false;
    vq->last_used_idx = 0;	
    vq->avail_flags_shadow = 0;
    vq->avail_idx_shadow = 0;	// 下一次添加buffer后index放到Avail Ring的第0个位置
    vq->num_added = 0;
    list_add_tail(&vq->vq.list, &vdev->vqs);
	......
    vq->event = virtio_has_feature(vdev, VIRTIO_RING_F_EVENT_IDX);

    /* No callback?  Tell other side not to bother us. */
    if (!callback) {
        vq->avail_flags_shadow |= VRING_AVAIL_F_NO_INTERRUPT;
        if (!vq->event)
            vq->vring.avail->flags = cpu_to_virtio16(vdev, vq->avail_flags_shadow);
    }

    /* Put everything in free lists. */
    vq->free_head = 0;	// Descriptor Table中空闲的buffer头
    for (i = 0; i < vring.num-1; i++)
        vq->vring.desc[i].next = cpu_to_virtio16(vdev, i + 1);	// 所有buffer初始化成一个Descriptor chain
    memset(vq->desc_state, 0, vring.num * sizeof(struct vring_desc_state));

    return &vq->vq;
}

  • Host 复位virtio设备函数如下
void virtio_reset(void *opaque)
{
    VirtIODevice *vdev = opaque;
    VirtioDeviceClass *k = VIRTIO_DEVICE_GET_CLASS(vdev);
    int i;

    virtio_set_status(vdev, 0);
    if (current_cpu) {
        /* Guest initiated reset */
        vdev->device_endian = virtio_current_cpu_endian();
    } else {
        /* System reset */
        vdev->device_endian = virtio_default_endian();
    }

    if (k->reset) {
        k->reset(vdev);
    }
    
    vdev->broken = false;
    vdev->guest_features = 0;
    vdev->queue_sel = 0;
    vdev->status = 0;
    atomic_set(&vdev->isr, 0);
    vdev->config_vector = VIRTIO_NO_VECTOR;
    virtio_notify_vector(vdev, vdev->config_vector);
    
    for(i = 0; i < VIRTIO_QUEUE_MAX; i++) {
        vdev->vq[i].vring.desc = 0;
        vdev->vq[i].vring.avail = 0;
        vdev->vq[i].vring.used = 0;
        vdev->vq[i].last_avail_idx = 0;	// Host维护的Avail Ring可用buffer的idx
        vdev->vq[i].shadow_avail_idx = 0;	// 存放VQ上取下Guest设置的avail_idx
        vdev->vq[i].used_idx = 0;	// host处理buffer的初始位置
        virtio_queue_set_vector(vdev, i, VIRTIO_NO_VECTOR);
        vdev->vq[i].signalled_used = 0;
        vdev->vq[i].signalled_used_valid = false;
        vdev->vq[i].notification = true;
        vdev->vq[i].vring.num = vdev->vq[i].vring.num_default;
        vdev->vq[i].inuse = 0;
        virtio_virtqueue_reset_region_cache(&vdev->vq[i]);
    }
}

Guest第一次添加buffer

示意图
  • 假设第一次Guest添加4个buffer,Guest首先通过free_head找到Descriptor Table空闲buffer的入口,完成4个buffer的添加,然后更新Avail Ring,将添加的buffer头的索引记录到Availv Ring中。virtqueue关键成员更新如下:
  • num_free:6,Descriptor Table被占用了4个buffer,因此空闲buffer数减4
  • free_head:4,Descriptor Table前4个buffer已经被使用,因此空闲buffer头在Descriptor Table的第4个entry
  • avail_idx_shadow:1,下一次添加buffer,其头部将记录到Avail Ring的第1个entry
    Avail Ring成员更新如下:
  • idx:1,表示下一次添加buffer将把其头部记录到Avail Ring的第1个entry。Guest工作位置往下移动一个Avail Ring的entry。
  • Avail Ring[0]:0,记录每一次添加buffer的头部在Descriptor Table的索引值。这次添加的buffer在Descriptor Table索引为0
    在这里插入图片描述
  • Guest添加完buffer是否通知Host,有两种机制,一是判断Ring中的flags,二是Event_idx。我们介绍Event_idx机制,其核心原则是:如果Host能够处理VQ上的数据,处理够快,就通知;如果Host处理VQ上的数据慢,就不通知,Guest通过什么方法判断Host的处理速度,我们在最后的速度控制一节中介绍。
  • Guest通知Host的具体实现,就是往pci附加配置空间的virtio_pci_cap_notify_cfg字段写入数据,敏感指令触发VMExit,从而退出客户端,kvm检查是IO请求,从内核态返回到用户态qemu进行IO处理。
代码分析
static inline int virtqueue_add(struct virtqueue *_vq,
                struct scatterlist *sgs[],
                unsigned int total_sg,
                unsigned int out_sgs,	// 要发送的数据buffer
                unsigned int in_sgs,	// 要接受的数据buffer
                void *data,
                void *ctx,
                gfp_t gfp)
{
    struct vring_virtqueue *vq = to_vvq(_vq);
    struct scatterlist *sg;
    struct vring_desc *desc;
    unsigned int i, n, avail, descs_used, uninitialized_var(prev), err_idx;
    int head;
    bool indirect;

    START_USE(vq);
	......
    if (unlikely(vq->broken)) {
        END_USE(vq);
        return -EIO;
    }
	......
    head = vq->free_head;	// 取出Descriptor Table的空闲数据buffer头索引
	......
	{
        indirect = false;
        desc = vq->vring.desc;	// 读取Descriptor Table地址
        i = head;
        descs_used = total_sg;
    }

    if (vq->vq.num_free < descs_used) {	// 如果要添加的buffer数超过了Descriptor Table空闲的buffer数,报错没有空间
        pr_debug("Can't add buf len %i - avail = %i\n",
             descs_used, vq->vq.num_free);
        /* FIXME: for historical reasons, we force a notify here if
         * there are outgoing parts to the buffer.  Presumably the
         * host should service the ring ASAP. */
        if (out_sgs)
            vq->notify(&vq->vq);
        if (indirect)
            kfree(desc);
        END_USE(vq);
        return -ENOSPC;
    }

    for (n = 0; n < out_sgs; n++) {
        for (sg = sgs[n]; sg; sg = sg_next(sg)) {	// 从scatter-gather(向量化IO)缓存中挨个取出其地址,记录到Descriptor中
            dma_addr_t addr = vring_map_one_sg(vq, sg, DMA_TO_DEVICE);
            if (vring_mapping_error(vq, addr))
                goto unmap_release;

            desc[i].flags = cpu_to_virtio16(_vq->vdev, VRING_DESC_F_NEXT);	// 发送的数据buffer客户端只读
            desc[i].addr = cpu_to_virtio64(_vq->vdev, addr);
            desc[i].len = cpu_to_virtio32(_vq->vdev, sg->length);
            prev = i;
            i = virtio16_to_cpu(_vq->vdev, desc[i].next);
        }
    }
    for (; n < (out_sgs + in_sgs); n++) {
        for (sg = sgs[n]; sg; sg = sg_next(sg)) {
            dma_addr_t addr = vring_map_one_sg(vq, sg, DMA_FROM_DEVICE);
            if (vring_mapping_error(vq, addr))
                goto unmap_release;

            desc[i].flags = cpu_to_virtio16(_vq->vdev, VRING_DESC_F_NEXT | VRING_DESC_F_WRITE);	// 接收的数据buffer客户端可写
            desc[i].addr = cpu_to_virtio64(_vq->vdev, addr);
            desc[i].len = cpu_to_virtio32(_vq->vdev, sg->length);
            prev = i;
            i = virtio16_to_cpu(_vq->vdev, desc[i].next);
        }
    }
    /* Last one doesn't continue. */
    desc[prev].flags &= cpu_to_virtio16(_vq->vdev, ~VRING_DESC_F_NEXT);
	......
    /* We're using some buffers from the free list. */
    vq->vq.num_free -= descs_used;	// 减去用掉的buffer空间
	......
  	vq->free_head = i;				// 更新Descriptor Table可用buffer的头部
	......
 	vq->desc_state[head].indir_desc = ctx;

    /* Put entry in available array (but don't update avail->idx until they
     * do sync). */
    avail = vq->avail_idx_shadow & (vq->vring.num - 1);	// 获取要记录的Avail Ring的索引
    vq->vring.avail->ring[avail] = cpu_to_virtio16(_vq->vdev, head);	// 将本次添加的buffer头的索引写入Avail Ring中

    /* Descriptors and available array need to be set before we expose the
     * new available array entries. */
    virtio_wmb(vq->weak_barriers);
    vq->avail_idx_shadow++;	// Avail Ring索引加1
    vq->vring.avail->idx = cpu_to_virtio16(_vq->vdev, vq->avail_idx_shadow);		// 将下一次要更新的Avail Ring索引放到idx中
    vq->num_added++;

    pr_debug("Added buffer head %i to %p\n", head, vq);
    END_USE(vq);

    /* This is very unlikely, but theoretically possible.  Kick
     * just in case. */
    if (unlikely(vq->num_added == (1 << 16) - 1))
        virtqueue_kick(_vq);

    return 0;

Host第一次处理buffer
示意图
  • Host在VMEixt后,KVM查看退出原因是IO,返回给用户态qemu处理,如果IO的地址是virtio
    pci附加配置空间地址,并且是VIRTIO_PCI_QUEUE_NOTIFY对应的地址,最后会触发各个设备注册VQ时的处理函数回调,对于virtio-blk设备,就是virtio_blk_handle_output函数。最后会从VQ中取数据.
  • Host首先取自己的工作位置last_avail_idx,初始化时被设置为0,再从VQ上取出Guest的工作位置avail_idx_shadow,如果Guest比Host位置超前说明Guest添加了buffer(从下图小绿人看,Guest每添加1次,小绿人就往下移动1个Avain
    Ring的单位),需要从VQ上取出数据,否则VQ为空,不做任何操作,返回。
  • Host根据last_avail_idx再Avail Ring中索引buffer头的索引,为0,根据buffer头的索引从Descriptor Table中找到buffer的入口,依次映射成iov entry
  • 完成映射后,Host更新last_avail_idx加1,工作位置往下摞1,同时更新used_idx为1,并记录到Used Ring的idx字段,记录下一次处理buffer的索引

在这里插入图片描述

代码分析

void *virtqueue_pop(VirtQueue *vq, size_t sz)
{
    unsigned int i, head, max;
    VRingMemoryRegionCaches *caches;
    MemoryRegionCache indirect_desc_cache = MEMORY_REGION_CACHE_INVALID;
    MemoryRegionCache *desc_cache;
    int64_t len;
    VirtIODevice *vdev = vq->vdev;
    VirtQueueElement *elem = NULL;
    unsigned out_num, in_num, elem_entries;
    hwaddr addr[VIRTQUEUE_MAX_SIZE];
    struct iovec iov[VIRTQUEUE_MAX_SIZE];
    VRingDesc desc;
    int rc;
	......
    rcu_read_lock();
    if (virtio_queue_empty_rcu(vq)) {		// 判断VQ中是否有数据,当Host记录的工作位置last_avail_idx和从VQ取下的
        goto done;							// Guest记录的工作位置shadow_avail_idx相等。说明Guest没有添加buffer,因此VQ为空
    }
	......
    max = vq->vring.num;					// 获取VQ深度

    if (vq->inuse >= vq->vring.num) {
        virtio_error(vdev, "Virtqueue size exceeded");
        goto done;
    }

    if (!virtqueue_get_head(vq, vq->last_avail_idx++, &head)) {	// 根据Host记录的工作位置last_avail_idx从Avail Ring中
        goto done;												// 取下可用buffer的头部索引
    }

    if (virtio_vdev_has_feature(vdev, VIRTIO_RING_F_EVENT_IDX)) {
        vring_set_avail_event(vq, vq->last_avail_idx);	// 设置last_avail_idx到Used Ring的最后一项
    }

    i = head;

    caches = vring_get_region_caches(vq);
    if (caches->desc.len < max * sizeof(VRingDesc)) {
        virtio_error(vdev, "Cannot map descriptor ring");
        goto done;
    }

    desc_cache = &caches->desc;
    vring_desc_read(vdev, &desc, desc_cache, i);	// 读取Descriptor Table的第一个buffer地址到descr
	......
    /* Collect all the descriptors */				// 顺序读取整个Descriptor chain包含的descriptor
    do {
        bool map_ok;

        if (desc.flags & VRING_DESC_F_WRITE) {
            map_ok = virtqueue_map_desc(vdev, &in_num, addr + out_num,	// 将Guest映射的sg entry转换成iov
                                        iov + out_num,
                                        VIRTQUEUE_MAX_SIZE - out_num, true,
                                        desc.addr, desc.len);
        } else {
            if (in_num) {
                virtio_error(vdev, "Incorrect order for descriptors");
                goto err_undo_map;
            }
            map_ok = virtqueue_map_desc(vdev, &out_num, addr, iov,
                                        VIRTQUEUE_MAX_SIZE, false,
                                        desc.addr, desc.len);
        }
        if (!map_ok) {
            goto err_undo_map;
        }

        /* If we've got too many, that implies a descriptor loop. */
        if (++elem_entries > max) {	// 超过了队列深度,循环了
            virtio_error(vdev, "Looped descriptor");
            goto err_undo_map;
        }

        rc = virtqueue_read_next_desc(vdev, &desc, desc_cache, max, &i);
    } while (rc == VIRTQUEUE_READ_DESC_MORE);

    if (rc == VIRTQUEUE_READ_DESC_ERROR) {
        goto err_undo_map;
    }

    /* Now copy what we have collected and mapped */
    elem = virtqueue_alloc_element(sz, out_num, in_num);
    elem->index = head;
    for (i = 0; i < out_num; i++) {
        elem->out_addr[i] = addr[i];
        elem->out_sg[i] = iov[i];
    }
    for (i = 0; i < in_num; i++) {
        elem->in_addr[i] = addr[out_num + i];
        elem->in_sg[i] = iov[out_num + i];
    }

    vq->inuse++;

    trace_virtqueue_pop(vq, elem, elem->in_num, elem->out_num);
done:
    address_space_cache_destroy(&indirect_desc_cache);
    rcu_read_unlock();

    return elem;
}

Guest第二次添加buffer

  • 假设第二次添加5个buffer,首选取出Descriptor Table空闲buffer头free_head 4,映射5个sg,之后更新VQ参数,分别是:
    1. num_free:1,Descriptor Table被占用了5个buffer,空闲buffer数减5
    2. free_head:9,Descriptor Table前9个buffer已经被使用,空闲buffer从第9个开始
    3. avail_idx_shadow:2,下一次添加buffer,头部将记录到Avail Ring的第2个entry
      Avail Ring成员更新如下:
    4. idx:2,下次添加buffer将头部索引Avail Ring的位置。Guest工作位置往下移
    5. Avail Ring[1]:4,这次添加的buffer头在Descriptor Table的索引

在这里插入图片描述

Host第二次处理buffer

在这里插入图片描述

  • 4
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
在Linux上,virtio的具体实现涉及到几个关键的结构和函数。首先,在virtio驱动中,使用了virtio_device_id结构来匹配设备,并在virtio_driver结构中包含了支持的virtio_device_id列表。\[1\]其次,对于特定的虚拟设备,比如virtio-net网卡驱动,它的实现包括了网卡驱动和virtio操作。\[2\]在创建virtqueue时,核心流程包括了一系列的操作,其中重点是核心流程的梳理。\[2\]最后,对于挂接在virtio总线上的设备,对应了struct virtio_device结构。对于virtio-net设备,它提供了自己的驱动即struct virtio_driver virtio_net_driver。当virtio-net设备挂接到virtio总线上或者virtio_net_driver注册到virtio总线上时,会调用virtio bus的探测函数virtio_dev_probe来找到驱动探测函数virtnet_probe。最终,通过register_netdev()函数将网络设备注册到Linux网络协议栈中。\[3\]这些是virtio在Linux上的具体实现的一些关键点。 #### 引用[.reference_title] - *1* *2* [virtio-net 实现机制【一】(图文并茂)](https://blog.csdn.net/m0_74282605/article/details/128114444)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [Linux virtio-net driver](https://blog.csdn.net/lingshengxiyou/article/details/127771119)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值