KVM中virtio之vring(八)

1 virtio原理

拿网络驱动部分做案例,网络驱动virtio-net有两个队列:接收队列和发送队列;每个队列都对应一个virtqueue,两个队列之间是互不影响的。

前后端利用virtqueue的方式如下图所示:
在这里插入图片描述
当收包时,ReceiveQueue需要客户机 driver提前填充分配好的空buffer,然后记录到availRing,并在恰当的时机通知后端,当外部网络有数据包到达时,qemu后端就从availRing 中获取一个buffer,然后填充数据,完事后记录buffer head index到usedRing.最后在恰当的时机通知客户机(向客户机注入中断),客户机接收到信号便知道有数据包到达,这里只需要从usedRing 中获取到index,然后取data数组的第i个元素即可。因为在客户机填充buffer的时候把逻辑buffer的指针保存在data数组中。

而SendQueue同样需要客户机去填充,只不过这里是当客户机需要发送数据包时,把数据包构造成逻辑buffer,然后填充到send Queue,并在恰当的时机通知后端,qemu后端收到通知就知道那个队列有请求到达,如果当前没有处理其他数据包就着手处理这个数据包。
在这里插入图片描述
具体就同样是从AvailRing中取出buffer head index,然后从描述符表中get到buffer,这时就需要从buffer中copy数据了,因为要把数据包从host发送出去,然后更新usedRing。最后同样要在恰当的时机通知客户机。注意这里客户机同样需要从usedRing 中get index,但是这里主要是用于delay notify,因为数据包由客户机构造,其占用的buffer并不能重复使用,只是每次有数据包就把其构造成buffer而已。
在这里插入图片描述

2 Virtqueue

struct VirtQueue
{
    VRing vring;  /* vring的元数据 */
    hwaddr pa;  /* vring实际的内存地址 */
    uint16_t last_avail_idx;
    /* Last used index value we have signalled on */
    uint16_t signalled_used;

    /* Last used index value we have signalled on */
    bool signalled_used_valid;

    /* Notification enabled? */
    bool notification;

    uint16_t queue_index;

    int inuse;

    uint16_t vector;
    void (*handle_output)(VirtIODevice *vdev, VirtQueue *vq);
    VirtIODevice *vdev;
    EventNotifier guest_notifier;
    EventNotifier host_notifier;
};

每个设备拥有多个 virtqueue 用于大块数据的传输。virtqueue 是一个简单的队列,guest 把 buffers 插入其中,每个 buffer 都是一个分散-聚集数组。驱动调用 find_vqs()来创建一个与 queue 关联的结构体。virtqueue 的数目根据设备的不同而不同,比如 block 设备有一个 virtqueue,network 设备有 2 个 virtqueue,一个用于发送数据包,一个用于接收数据包,Balloon 设备有 3 个 virtqueue.

针对 virtqueue 的操作包括:

  1. int virtqueue_add_buf(struct virtqueue *_vq, struct scatterlist sg[], unsigned int out, unsigned int in, void *data, gfp_t gfp)
    用于向 queue 中添加一个新的 buffer,参数 data 是一个非空的令牌,用于识别 buffer,当 buffer 内容被消耗后,data 会返回。

  2. virtqueue_kick()
    Guest 通知 host 单个或者多个 buffer 已经添加到 queue 中,调用 virtqueue_notify(),notify 函数会向 queue notify(VIRTIO_PCI_QUEUE_NOTIFY)寄存器写入 queue index 来通知 host。

  3. void *virtqueue_get_buf(struct virtqueue *_vq, unsigned int *len)
    返回使用过的 buffer,len 为写入到 buffer 中数据的长度。获取数据,释放 buffer,更新 vring 描述符表格中的 index。

  4. virtqueue_disable_cb()
    示意 guest 不再需要再知道一个 buffer 已经使用了,也就是关闭 device 的中断。驱动会在初始化时注册一个回调函数,disable_cb()通常在这个 virtqueue 回调函数中使用,用于关闭再次的回调发生。

  5. virtqueue_enable_cb()
    与 disable_cb()刚好相反,用于重新开启设备中断的上报。

3 VRing

typedef struct VRing
{
    unsigned int num;
    unsigned int align;
    hwaddr desc;
    hwaddr avail;
    hwaddr used;
} VRing;

virtio_ring 是 virtio 传输机制的实现,vring 引入 ring buffers 来作为我们数据传输的载体。virtio_ring 包含 3 部分:

  1. 描述符数组(descriptor table)用于存储一些关联的描述符,每个描述符都是一个对 buffer 的描述,包含一个 address/length 的配对。
  2. 可用的 ring(available ring)用于 guest 端表示那些描述符链当前是可用的。
  3. 使用过的 ring(used ring)用于表示 Host 端表示那些描述符已经使用。

Ring 的数目必须是 2 的次幂。

avail ring有两个用途,一是发送侧(send queue)前端驱动发送报文的时,将待发送报文加入avail ring等待后端的处理,后端处理完后,会将其放入used ring,并由前端将其释放desc中(free_old_xmit_skbs, detach_buf),最后通过try_fill_recv重新装入avail ring中; 二是接收侧(receive qeueu),前端将空白物理块加入avail ring中,提供给后端用来接收报文,后端接收完报文会放入used ring。

可以看出,都是后端用完前端的avail ring的东西放入used ring,即avail ring是前端维护,后端访问;used ring 是后端修改,前端访问。

4 VRingDesc

描述符和描述符表格

vring descriptor 用于指向 guest 使用的 buffer。

typedef struct VRingDesc
{
    uint64_t addr;
    uint32_t len;
    uint16_t flags;
    uint16_t next;
} VRingDesc;

addr:guest 物理地址
len:buffer 的长度
flags:flags 的值含义包括:

  • VRING_DESC_F_NEXT:用于表明当前 buffer 的下一个域是否有效,也间接表明当前 buffer 是否是 buffers list 的最后一个。
  • VRING_DESC_F_WRITE:当前 buffer 是 read-only 还是 write-only。
  • VRING_DESC_F_INDIRECT:表明这个 buffer 中包含一个 buffer 描述符的 list

next:所有的 buffers 通过 next 串联起来组成 descriptor table

多个 buffer 组成一个 list 由 descriptor table 指向这些 list。

约定俗成,每个 list 中,read-only buffers 放置在 write-only buffers 前面。
在这里插入图片描述
图 2.descriptor table

有些设备可能需要同时完成大量数据传输的大量请求,设备 VIRTIO_RING_F_INDIRECT_DESC 特性能够满足这种需求。为了增加 ring 的容量,vring 可以指向一个可以处于内存中任何位置 indirect descriptors table,而这个 table 指向一组 vring descriptors,而这些 vring descriptor 分别指向一组 buffer list(如图所示)。当然 indirect descriptors table 中的 descriptor 不能再次指向 indirect descriptors table。单个 indirect descriptor table 可以包含 read-only 和 write-only 的 descriptors,带有 write-only flag 的 descriptor 会被忽略。
在这里插入图片描述
图 3.indirect decriptors

5 VRingAvail

typedef struct VRingAvail
{
    uint16_t flags;
    uint16_t idx;
    uint16_t ring[0];
} VRingAvail;

Available ring 指向 guest 提供给设备的描述符,它指向一个 descriptor 链表的头。Available ring 结构如下图所示。其中标识 flags 值为 0 或者 1,1 表明 Guest 不需要 device 使用完这些 descriptor 时上报中断。idx 指向我们下一个 descriptor 入口处,idx 从 0 开始,一直增加,使用时需要取模:idx=idx&(vring.num-1)
在这里插入图片描述
图 4.available ring

6 VRingUsed

typedef struct VRingUsedElem
{
    uint32_t id;
    uint32_t len;
} VRingUsedElem;

typedef struct VRingUsed
{
    uint16_t flags;
    uint16_t idx;
    VRingUsedElem ring[0];
} VRingUsed;

Used ring 指向 device(host)使用过的 buffers。Used ring 和 Available ring 之间在内存中的分布会有一定间隙,从而避免了 host 和 guest 两端由于 cache 的影响而会写入到 virtqueue 结构体的同一部分的情况。

flags 用于 device 告诉 guest 再次添加 buffer 到 available ring 时不再提醒,也就是说 guest 添加 buffers 到 available ring 时不必进行 kick 操作。

Used vring element 包含 id 和 len,id 指向 descriptor chain 的入口,与之前 guest 写入到 available ring 的入口项一致。

len 为写入到 buffer 中的字节数。

Guest 向设备提供 buffer

  1. 把 buffer 添加到 description table 中,填充 addr,len,flags
  2. 更新 available ring head
  3. 更新 available ring 中的 index
  4. 通知 device,通过写入 virtqueue index 到 Queue Notify 寄存器

Device 使用 buffer 并填充 used ring

device 端使用 buffer 后填充 used ring 的过程如下:

  1. virtqueue_pop()——从描述符表格(descriptor table)中找到 available ring 中添加的 buffers,映射内存
  2. 从分散-聚集的 buffer 读取数据
  3. virtqueue_fill()——取消内存映射,更新 ring[idx]中的 id 和 len 字段
  4. virtqueue_flush()——更新 vring_used 中的 idx
  5. virtio_notify()——如果需要的话,在 ISR 状态位写入 1,通知 guest 描述符已经使用

VIRTIO的vring收发队列创建流程:

在初始化阶段,前端分配好内存区,并初始化好前端的vring后,就把内存区的信息传递到后端,后端也利用这个内存区的信息初始化队列相关的vring。这样vring就在前后端保持了一致。原理就是如此,下面看具体初始化代码:

// GUEST前端驱动,以网络设备为例: Virtio-net: PCI 发现后,通过PCI总线分配收发队列,函数调用依次如下:前端:virtnet_probe->init_vqs->virtnet_find_vqs->vi->vdev->config->find_vqs(vp_find_vqs)->vp_try_to_find_vqs->setup_vq,在setup_vp中通过IO端口和后端交互

static int virtnet_probe(structvirtio_device *vdev)
{
        /*
         * 初始化virtqueue
        * 创建和初始化发送/接收队列
        */
        err = init_vqs(vi);
}

/*创建和初始化发送/接收队列*/
static int init_vqs(struct virtnet_info*vi)
{
        /*分配*/
        ret = virtnet_alloc_queues(vi);
        if (ret)
                  goto err;

        /*通过find vqs来创建vring*/
        ret = virtnet_find_vqs(vi);
        if (ret)
                  goto err_free;
}

/*通过find vqs来创建vring*/
static int virtnet_find_vqs(structvirtnet_info *vi)
{
        /*最后调用的是vp_find_vqs,真正的创建virtqueue内部结构和分配地址,并将地址告诉后端QEMU驱动*/
        ret = vi->vdev->config->find_vqs(vi->vdev,total_vqs, vqs, callbacks, names);
}

//VIRTIO PCI总线
static int vp_find_vqs(struct virtio_device*vdev, unsigned nvqs,struct virtqueue *vqs[],vq_callback_t *callbacks[],const char *names[])
{
        int err;
        err = vp_try_to_find_vqs(vdev,nvqs, vqs, callbacks, names, true, true);
        err = vp_try_to_find_vqs(vdev,nvqs, vqs, callbacks, names, true, false);
        return vp_try_to_find_vqs(vdev,nvqs, vqs, callbacks, names, false, false);

}

static int vp_try_to_find_vqs()
{
                  /*最核心的是setup_vq()*/
                  vqs[i] = setup_vq(vdev,i, callbacks[i], names[i], msix_vec);

}

static struct virtqueue *setup_vq(structvirtio_device *vdev, unsigned index,void (*callback)(struct virtqueue *vq),const char *name,u16 msix_vec)
{

        /*这里实际上把info->queue的GPA(页框号写入到了设备的VIRTIO_PCI_QUEUE_PFN),这样后端就会得到这块内存区的信息。然后我们先看下前端利用这块内存区做了什么?看下面的vring_new_virtqueue函数,该函数中调用vring_init来初始化vring*/

        iowrite32(virt_to_phys(info->queue)>> VIRTIO_PCI_QUEUE_ADDR_SHIFT, vp_dev->ioaddr +VIRTIO_PCI_QUEUE_PFN);

/* create the vring */
    vq = vring_new_virtqueue(index, info->num, VIRTIO_PCI_VRING_ALIGN, vdev,
                 true, info->queue, vp_notify, callback, name);
}

static inline void vring_init(struct vring *vr, unsigned int num, void *p,
                  unsigned long align)
{
    vr->num = num;
    vr->desc = p;
    vr->avail = p + num*sizeof(struct vring_desc);
    vr->used = (void *)(((unsigned long)&vr->avail->ring[num] + sizeof(__u16)
        + align-1) & ~(align - 1));
}

// 这个函数正好体现了我们前面那个结构图。这样前端vring就初始化好了。对队列填充数据时就是根据这个vring填充信息。

// QEMU后端驱动:
// 下面在通过VIRTIO_PCI_QUEUE_PFN传递地址的时候,调用virtio_queue_set_addr设置后端相关队列的vring,该函数实现较简单

static void virtio_ioport_write(void*opaque, uint32_t addr, uint32_t val)
{
    switch (addr) {
    case VIRTIO_PCI_QUEUE_PFN:
        /*pa就是desc的GPA*/
        pa = (target_phys_addr_t)val << VIRTIO_PCI_QUEUE_ADDR_SHIFT;
        if (pa == 0) {
            virtio_pci_stop_ioeventfd(proxy);
            virtio_reset(proxy->vdev);
            msix_unuse_all_vectors(&proxy->pci_dev);
        }
        else
            //下面在通过VIRTIO_PCI_QUEUE_PFN传递地址的时候,调用virtio_queue_set_addr设置后端相关队列的vring
            virtio_queue_set_addr(vdev, vdev->queue_sel, pa);
        break;
    //仅仅是标记了下设备中的queue_sel表示当前操作的队列索引
    case VIRTIO_PCI_QUEUE_SEL:
        if (val < VIRTIO_PCI_QUEUE_MAX)
            vdev->queue_sel = val;
        break;
    case VIRTIO_PCI_QUEUE_NOTIFY:
        if (val < VIRTIO_PCI_QUEUE_MAX) {
            virtio_queue_notify(vdev, val);
        }
        break;
}

void virtio_queue_set_addr(VirtIODevice *vdev, int n, target_phys_addr_t addr)
{
    vdev->vq[n].pa = addr;
    virtqueue_init(&vdev->vq[n]);
}

static void virtqueue_init(VirtQueue *vq)
{
    target_phys_addr_t pa = vq->pa;

    vq->vring.desc = pa;
    vq->vring.avail = pa + vq->vring.num * sizeof(VRingDesc);
    vq->vring.used = vring_align(vq->vring.avail +
                                 offsetof(VRingAvail, ring[vq->vring.num]),
                                 VIRTIO_PCI_VRING_ALIGN);
}

看到这里有么有很面熟,没错,这个函数和前端初始化vring的函数很是类似,这样前后端的vring就同步起来了……

而在guest通知后端的时候,通过VIRTIO_PCI_QUEUE_NOTIFY接口,该函数调用了virtio_queue_notify_vq继而调用 vq->handle_output……就这样,后端就得到通知着手处理了!

还有个细节问题,前端驱动写入的应该是QEUEU的GPA

iowrite32(virt_to_phys(info->queue)>> VIRTIO_PCI_QUEUE_ADDR_SHIFT,vp_dev->ioaddr+ VIRTIO_PCI_QUEUE_PFN);

这个被后端QEMU截获后,QEMU怎么直接使用了GPA呢?哪里完成的GPA->HVA的转换呢?

这个是在QEMU从virtqueue中取消息的时候,进行转换的

QEMU代码,在收到VIRTIO通知后,会通过virtqueue_pop从共享队列中取出消息

intvirtqueue_pop(VirtQueue *vq, VirtQueueElement *elem)
{
    /*Now map what we have collected */
    virtqueue_map_sg(elem->in_sg,elem->in_addr, elem->in_num, 1);
    virtqueue_map_sg(elem->out_sg,elem->out_addr, elem->out_num, 0);
}

voidvirtqueue_map_sg()
{
   for (i = 0; i < num_sg; i++) {
       len = sg[i].iov_len;
       sg[i].iov_base =cpu_physical_memory_map(addr[i], &len, is_write);
       if (sg[i].iov_base == NULL || len != sg[i].iov_len) {
           error_report("virtio: trying to map MMIO memory");
           exit(1);
       }
    }
}

/*完成一个GUEST的物理地址GPA到HVA的转换*/
void*cpu_physical_memory_map(hwaddr addr,hwaddr *plen,int is_write)
{
   return address_space_map(&address_space_memory,addr, plen, is_write);
}

收包过程如下所示:
在这里插入图片描述

  1. 前端填充好desc(addr/len),并更新vring->avail(ring[0])
  2. 后端读取avail ring索引,找到desc(if ring[0]=2,then desctable[2] 记录的就是一个逻辑buffer的首个物理块的信息),填充buffer数据;将buffer索引存在desc,将desc索引存放在used ring中
  3. 前端读取used ring索引,找到desc,获取buffer数据

原文链接:https://blog.csdn.net/qq_15437629/article/details/82084470

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值