virtio的vring隊列

上一篇已經提到,在virtio設備初始化的過程中,會通過setup_vp創建virtqueue,目前的virtqueue隊列都是通過vring來實際工作的,我們可以把virtqueue當做一個接口類,而把vring_virtqueue當做這個接口的一個實現

/**
 * virtqueue - a queue to register buffers for sending or receiving.
 * @list: the chain of virtqueues for this device
 * @callback: the function to call when buffers are consumed (can be NULL).
 * @name: the name of this virtqueue (mainly for debugging)
 * @vdev: the virtio device this queue was created for.
 * @priv: a pointer for the virtqueue implementation to use.
 */
struct virtqueue {
    struct list_head list;
    void (*callback)(struct virtqueue *vq);
    const char *name;
    struct virtio_device *vdev;
    void *priv;
};
對於pci設備而言,priv指向一個virtio_pci_vq_info的結構體,其中vq指向接口定義virtqueue,queue指向ring的實際地址,msix_vector是中斷號,etc.
struct virtio_pci_vq_info
{
    /* the actual virtqueue */
    struct virtqueue *vq;

    /* the number of entries in the queue */
    int num;

    /* the index of the queue */
    int queue_index;

    /* the virtual address of the ring queue */
    void *queue;

    /* the list node for the virtqueues list */
    struct list_head node;

    /* MSI-X vector (or none) */
    unsigned msix_vector;
};

一個vring_virtqueue的結構體定義如下

struct vring_virtqueue
{
    struct virtqueue vq;  /* virtqueue的接口類定義 */

    /* Actual memory layout for this queue */
    struct vring vring;  /* vring memory layout指針 */

    /* Other side has made a mess, don't try any more. */
    bool broken;

    /* Host supports indirect buffers */
    bool indirect;  /* transport feature位,是否支持indirect buffer */

    /* Host publishes avail event idx */
    bool event;  /* transport feature位,是否支持event idx interrupt/notify */

    /* Number of free buffers */
    unsigned int num_free;  /* vring desc裏面還剩餘的free buffer個數,free buffer是free_head開頭的一個list */
    /* Head of free buffer list. */
    unsigned int free_head;  /* vring desc的free buffer head index */
    /* Number we've added since last sync. */
    unsigned int num_added;  /* 從上次sync到現在增加的次數,注意這裏是次數,不是增加的buffer個數 */
    /* Last used index we've seen. */
    u16 last_used_idx;

    /* How to notify other side. FIXME: commonalize hcalls! */
    void (*notify)(struct virtqueue *vq);

#ifdef DEBUG
    /* They're supposed to lock for us. */
    unsigned int in_use;
#endif

    /* Tokens for callbacks. */
    void *data[];  /* token數組,個數同vring desc */
};

virtio規範定義了virtqueue的幾種標準操作,e.g.

struct virtqueue *vring_new_virtqueue(unsigned int num,
                      unsigned int vring_align,
                      struct virtio_device *vdev,
                      void *pages,
                      void (*notify)(struct virtqueue *),
                      void (*callback)(struct virtqueue *),
                      const char *name)
{
    struct vring_virtqueue *vq;
    unsigned int i;

    /* We assume num is a power of 2. */
    if (num & (num - 1)) {
        dev_warn(&vdev->dev, "Bad virtqueue length %u\n", num);
        return NULL;
    }

    vq = kmalloc(sizeof(*vq) + sizeof(void *)*num, GFP_KERNEL); /* vring_virtqueue + data數組的大小 */
    if (!vq)
        return NULL;

    vring_init(&vq->vring, num, pages, vring_align);
    vq->vq.callback = callback;
    vq->vq.vdev = vdev;
    vq->vq.name = name;
    vq->notify = notify;
    vq->broken = false;
    vq->last_used_idx = 0;
    vq->num_added = 0;
    list_add_tail(&vq->vq.list, &vdev->vqs);
#ifdef DEBUG
    vq->in_use = false;
#endif

    vq->indirect = virtio_has_feature(vdev, VIRTIO_RING_F_INDIRECT_DESC); /* 是否支持indirect buffer */
    vq->event = virtio_has_feature(vdev, VIRTIO_RING_F_EVENT_IDX);  /* 是否通過event idx來觸發中斷時間 */

    /* No callback?  Tell other side not to bother us. */
    if (!callback)
        vq->vring.avail->flags |= VRING_AVAIL_F_NO_INTERRUPT; /* 如果callback爲空,關閉中斷 */

    /* Put everything in free lists. */
    vq->num_free = num;
    vq->free_head = 0; /* idx 0作爲空閒buffer list的頭部 */
    for (i = 0; i < num-1; i++) { /* vring_desc[0], vring_desc[1]依次連接成一個list */
        vq->vring.desc[i].next = i+1;
        vq->data[i] = NULL;
    }
    vq->data[i] = NULL;

    return &vq->vq;
}
vring_new_virtqueue創建一個vring_virtqueue,其中內存分配大小是sizeof(struct vring_virtqueue) + num * sizeof(void *),傳入的pages內存是vring的layout memory
/* virtqueue_add_buf: expose buffer to other end
 *  vq: the struct virtqueue we're talking about.
 *  sg: the description of the buffer(s).
 *  out_num: the number of sg readable by other side
 *  in_num: the number of sg which are writable (after readable ones)
 *  data: the token identifying the buffer.
 *      Returns remaining capacity of queue (sg segments) or a negative error.
 */
int virtqueue_add_buf(struct virtqueue *_vq,
          struct scatterlist sg[],
          unsigned int out,
          unsigned int in,
          void *data)
{
    struct vring_virtqueue *vq = to_vvq(_vq);
    unsigned int i, avail, head, uninitialized_var(prev);

    START_USE(vq);

    BUG_ON(data == NULL);

    /* If the host supports indirect descriptor tables, and we have multiple
     * buffers, then go indirect. FIXME: tune this threshold */
    if (vq->indirect && (out + in) > 1 && vq->num_free) {
        head = vring_add_indirect(vq, sg, out, in);
        if (head != vq->vring.num)
            goto add_head;
    }

    BUG_ON(out + in > vq->vring.num);
    BUG_ON(out + in == 0);

    if (vq->num_free < out + in) {
        pr_debug("Can't add buf len %i - avail = %i\n",
             out + in, 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)
            vq->notify(&vq->vq);
        END_USE(vq);
        return -ENOSPC;
    }

    /* We're about to use some buffers from the free list. */
    vq->num_free -= out + in;

    head = vq->free_head; /* 從vring_desc[vq->free_head]開頭,依次填充vring_desc項,加入new free buffer */
    for (i = vq->free_head; out; i = vq->vring.desc[i].next, out--) {
        vq->vring.desc[i].flags = VRING_DESC_F_NEXT;
        vq->vring.desc[i].addr = sg_phys(sg);
        vq->vring.desc[i].len = sg->length;
        prev = i;
        sg++;
    }
    for (; in; i = vq->vring.desc[i].next, in--) {
        vq->vring.desc[i].flags = VRING_DESC_F_NEXT|VRING_DESC_F_WRITE;
        vq->vring.desc[i].addr = sg_phys(sg);
        vq->vring.desc[i].len = sg->length;
        prev = i;
        sg++;
    }
    /* Last one doesn't continue. */
    vq->vring.desc[prev].flags &= ~VRING_DESC_F_NEXT; /* 最後的vring_desc項沒有next flag */

    /* Update free pointer */
    vq->free_head = i; /* 後移vq->free_head,新加入的buffer準備傳遞給avail ring */

add_head:
    /* Set token. */
    vq->data[head] = data; /* 把data指向的token填入vq->data[head] */

    /* Put entry in available array (but don't update avail->idx until they
     * do sync).  FIXME: avoid modulus here? */
    avail = (vq->vring.avail->idx + vq->num_added++) % vq->vring.num;
    vq->vring.avail->ring[avail] = head;

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

    /* If we're indirect, we can fit many (assuming not OOM). */
    if (vq->indirect)
        return vq->num_free ? vq->vring.num : 0;
    return vq->num_free;
}

virtqueue_add_buf把傳入的scatterlist填入vring_desc的free buffer鏈表中,並更新avail ring的idx的entry,指向新加入的free buffer鏈表頭。由於在前後端idx同步之前,有可能會有多次的virtqueue_add_buf調用,因此vring_virtqueue用了一個num_added來表示virtqueue_add_buf被調用的次數,e.g. 看下面這段代碼

    /* Put entry in available array (but don't update avail->idx until they
     * do sync).  FIXME: avoid modulus here? */
    avail = (vq->vring.avail->idx + vq->num_added++) % vq->vring.num;
    vq->vring.avail->ring[avail] = head;
初始化後vq->num_added = 0,avail = vq->vring.avail->idx,因此就把vring_avail.ring[idx]指向新加入的free buffer的鏈表頭。但此時vring.avail->idx保持不變,只讓vq->num_added增加1,因此下一次的virtqueue_add_buf調用時,vring_avail.ring[idx+1]會指向新加入的free buffer鏈表頭,依次類推。

如果vring支持indirect的話,新增free buffer就會簡單許多,通過vring_add_indirect實現

static int vring_add_indirect(struct vring_virtqueue *vq,
                  struct scatterlist sg[],
                  unsigned int out,
                  unsigned int in)
{
    struct vring_desc *desc;
    unsigned head;
    int i;

    desc = kmalloc((out + in) * sizeof(struct vring_desc), GFP_ATOMIC); /* 分配in+out個vring_desc項 */
    if (!desc)
        return vq->vring.num;

    /* Transfer entries from the sg list into the indirect page */
    for (i = 0; i < out; i++) {
        desc[i].flags = VRING_DESC_F_NEXT;
        desc[i].addr = sg_phys(sg);
        desc[i].len = sg->length;
        desc[i].next = i+1;
        sg++;
    }
    for (; i < (out + in); i++) {
        desc[i].flags = VRING_DESC_F_NEXT|VRING_DESC_F_WRITE;
        desc[i].addr = sg_phys(sg);
        desc[i].len = sg->length;
        desc[i].next = i+1;
        sg++;
    }

    /* Last one doesn't continue. */
    desc[i-1].flags &= ~VRING_DESC_F_NEXT;
    desc[i-1].next = 0;

    /* We're about to use a buffer */
    vq->num_free--; /* 對於vring_virtqueue而言,只使用了一個vring_desc項 */

    /* Use a single buffer which doesn't continue */
    head = vq->free_head;
    vq->vring.desc[head].flags = VRING_DESC_F_INDIRECT;
    vq->vring.desc[head].addr = virt_to_phys(desc);
    vq->vring.desc[head].len = i * sizeof(struct vring_desc);

    /* Update free pointer */
    vq->free_head = vq->vring.desc[head].next; /* free_head向後順移一項 */

    return head;
}
如果是indirect的話,vring_desc只會添加一條表項,指向一個indirect vring_desc數組。

/**
 * virtqueue_kick - update after add_buf
 * @vq: the struct virtqueue
 *
 * After one or more virtqueue_add_buf calls, invoke this to kick
 * the other side.
 *
 * Caller must ensure we don't call this with other virtqueue
 * operations at the same time (except where noted).
 */
void virtqueue_kick(struct virtqueue *vq)
{
    if (virtqueue_kick_prepare(vq))
        virtqueue_notify(vq);
}

bool virtqueue_kick_prepare(struct virtqueue *_vq)
{
    struct vring_virtqueue *vq = to_vvq(_vq);
    u16 new, old;
    bool needs_kick;

    START_USE(vq);
    /* Descriptors and available array need to be set before we expose the
     * new available array entries. */
    virtio_wmb();

    old = vq->vring.avail->idx;
    new = vq->vring.avail->idx = old + vq->num_added;
    vq->num_added = 0;

    /* Need to update avail index before checking if we should notify */
    virtio_mb();

    if (vq->event) {
        needs_kick = vring_need_event(vring_avail_event(&vq->vring),
                          new, old);
    } else {
        needs_kick = !(vq->vring.used->flags & VRING_USED_F_NO_NOTIFY);
    }
    END_USE(vq);
    return needs_kick;
}
virtqueue_kick用於通知qemu/vhost端avail ring有更新,其中virtqueue_kick_prepare用於計算是否需要kick,而virtqueue_notify通過寫入virtio bar0配置空間的QUEUE_NOTIFY字段產生VMEXIT從而被qemu/vhost捕獲

virtqueue_kick_prepare如果支持VIRTIO_RING_F_EVENT_IDX的feature,則需要計算vq->vring.avail->idx的變化,同時對比avail ring裏的used_event_idx,那這個used_event_idx是幹嘛用的呢?說的通俗一點,每次判斷是否要kick另一端,主要是對比這次同步時,guest端增加的avail entry,是否大於host端看到增加的entry個數,e.g. 假設guest新增了5個avail entry,之後同步了一次,接着又增加了5個avail entry,又再次同步。但此時host端第一次同步之後只消費了4個avail entry,那麼第二次同步時會發現條件不成立,不會去kick virtqueue,只有host端第一次同步之後把5個avail entry都消費完畢之後,下一次同步纔會kick。這個機制有點類似邊緣觸發的概念。

void *virtqueue_get_buf(struct virtqueue *_vq, unsigned int *len)
{
    struct vring_virtqueue *vq = to_vvq(_vq);
    void *ret;
    unsigned int i;

    START_USE(vq);

    if (unlikely(vq->broken)) {
        END_USE(vq);
        return NULL;
    }

    if (!more_used(vq)) {
        pr_debug("No more buffers in queue\n");
        END_USE(vq);
        return NULL;
    }

    /* Only get used array entries after they have been exposed by host. */
    virtio_rmb();

    i = vq->vring.used->ring[vq->last_used_idx%vq->vring.num].id;  /* 獲取last_used_idx指向的used_elem */
    *len = vq->vring.used->ring[vq->last_used_idx%vq->vring.num].len;

    if (unlikely(i >= vq->vring.num)) {
        BAD_RING(vq, "id %u out of range\n", i);
        return NULL;
    }
    if (unlikely(!vq->data[i])) {
        BAD_RING(vq, "id %u is not a head!\n", i);
        return NULL;
    }

    /* detach_buf clears data, so grab it now. */
    ret = vq->data[i];
    detach_buf(vq, i); /* detach_buf把used ring idx對應的vring_desc鏈表放回到free_head指向的空閒鏈表頭部 */
    vq->last_used_idx++;  /* last_used_idx自增 */
    /* If we expect an interrupt for the next entry, tell host
     * by writing event index and flush out the write before
     * the read in the next get_buf call. */
    if (!(vq->vring.avail->flags & VRING_AVAIL_F_NO_INTERRUPT)) {
        vring_used_event(&vq->vring) = vq->last_used_idx; /* 更新last_used_idx值到前端驅動 */
        virtio_mb();
    }

    END_USE(vq);
    return ret;
}
virtqueue_get_buf用於回收last_used_idx指向的一個used ring的entry

static void detach_buf(struct vring_virtqueue *vq, unsigned int head)
{
    unsigned int i;

    /* Clear data ptr. */
    vq->data[head] = NULL;

    /* Put back on free list: find end */
    i = head;

    /* Free the indirect table */
    if (vq->vring.desc[i].flags & VRING_DESC_F_INDIRECT)
        kfree(phys_to_virt(vq->vring.desc[i].addr));

    while (vq->vring.desc[i].flags & VRING_DESC_F_NEXT) {
        i = vq->vring.desc[i].next;
        vq->num_free++;
    }

    vq->vring.desc[i].next = vq->free_head;
    vq->free_head = head;
    /* Plus final descriptor */
    vq->num_free++;
}
實際的回收工作由detach_buf完成,這裏不會對vring_desc裏實際的addr地址做任何操作,意味着這些地址在之前就已經被回收掉了

virtqueue_disable_cb用於關閉中斷,virtqueue_enable_cb用於打開中斷,比較簡單這裏不多分析了

void virtqueue_disable_cb(struct virtqueue *_vq)
{
    struct vring_virtqueue *vq = to_vvq(_vq);

    vq->vring.avail->flags |= VRING_AVAIL_F_NO_INTERRUPT;
}


bool virtqueue_enable_cb(struct virtqueue *_vq)
{
    struct vring_virtqueue *vq = to_vvq(_vq);

    START_USE(vq);

    /* We optimistically turn back on interrupts, then check if there was
     * more to do. */
    /* Depending on the VIRTIO_RING_F_EVENT_IDX feature, we need to
     * either clear the flags bit or point the event index at the next
     * entry. Always do both to keep code simple. */
    vq->vring.avail->flags &= ~VRING_AVAIL_F_NO_INTERRUPT;
    vring_used_event(&vq->vring) = vq->last_used_idx;
    virtio_mb();
    if (unlikely(more_used(vq))) {
        END_USE(vq);
        return false;
    }

    END_USE(vq);
    return true;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章