DPDK vhost-user之mergeable 特性(七)

vhost_user在收包時(將數據包發往vm內部)會調用rte_vhost_enqueue_burst函數,這個函數的實現如下:

rte_vhost_enqueue_burst

uint16_t rte_vhost_enqueue_burst(int vid, uint16_t queue_id,
         struct rte_mbuf **pkts, uint16_t count)
{
         struct virtio_net *dev = get_device(vid);
 
         if (!dev)
                   return 0;
 
         if (dev->features & (1 << VIRTIO_NET_F_MRG_RXBUF))
                   return virtio_dev_merge_rx(dev, queue_id, pkts, count);
         else
                   return virtio_dev_rx(dev, queue_id, pkts, count);
}

我們可以看到根據vhost_user後端設備是否支持VIRTIO_NET_F_MRG_RXBUF特性會調用不同函數,其中不支持時調用的virtio_dev_rx之前已經分析過,這裏看下virtio_dev_merge_rx的具體實現。

virtio_dev_merge_rx

static inline uint32_t __attribute__((always_inline))
virtio_dev_merge_rx(struct virtio_net *dev, uint16_t queue_id,
         struct rte_mbuf **pkts, uint32_t count)
{
         struct vhost_virtqueue *vq;
         uint32_t pkt_idx = 0;
         uint16_t num_buffers;
         struct buf_vector buf_vec[BUF_VECTOR_MAX];
         uint16_t avail_head;
 
    /*獲取對應的queue*/
         vq = dev->virtqueue[queue_id];
         if (unlikely(vq->enabled == 0))
                   return 0;
 
         count = RTE_MIN((uint32_t)MAX_PKT_BURST, count);
         if (count == 0)
                   return 0;
    /*avail->ring[vq->last_avail_idx & (vq->size - 1)]記錄着首個可用的desc index,
          *avail->ring[vq->avail->idx & (vq->size - 1)]記錄着最後一個可用的desc index*/
         rte_prefetch0(&vq->avail->ring[vq->last_avail_idx & (vq->size - 1)]);
 
         vq->shadow_used_idx = 0;
         /*avail->ring[avail_head]記錄着最後一個可用的desc index*/
         avail_head = *((volatile uint16_t *)&vq->avail->idx);
         /*遍歷每一個要發送的數據包*/
         for (pkt_idx = 0; pkt_idx < count; pkt_idx++) {
                   uint32_t pkt_len = pkts[pkt_idx]->pkt_len + dev->vhost_hlen;
        /*預留足夠的desc來存放mbuf,使用buf_vec來記錄,每個buf_vec對應一個desc,
             所以num_buffers就是存放這個數據包所需的desc chain的個數*/
                   if (unlikely(reserve_avail_buf_mergeable(dev, vq,
                                                        pkt_len, buf_vec, &num_buffers,
                                                        avail_head) < 0)) {
                            LOG_DEBUG(VHOST_DATA,
                                     "(%d) failed to get enough desc from vring\n",
                                     dev->vid);
                            vq->shadow_used_idx -= num_buffers;
                            break;
                   }
 
                   LOG_DEBUG(VHOST_DATA, "(%d) current index %d | end index %d\n",
                            dev->vid, vq->last_avail_idx,
                            vq->last_avail_idx + num_buffers);
        /*根據buf_vec中記錄的desc信息,將當前數據包(mbuf)拷貝到這些desc中*/
                   if (copy_mbuf_to_desc_mergeable(dev, pkts[pkt_idx],
                                                        buf_vec, num_buffers) < 0) {
                            vq->shadow_used_idx -= num_buffers;
                            break;
                   }
        /*更新last_avail_idx*/
                   vq->last_avail_idx += num_buffers;
         }
 
         if (likely(vq->shadow_used_idx)) {
                   flush_shadow_used_ring(dev, vq);
 
                   /* flush used->idx update before we read avail->flags. */
                   rte_mb();
 
                   /* Kick the guest if necessary. */
                   if (!(vq->avail->flags & VRING_AVAIL_F_NO_INTERRUPT)
                                     && (vq->callfd >= 0))
                            eventfd_write(vq->callfd, (eventfd_t)1);
         }
 
         return pkt_idx;
}

這裏主要引入了一個struct buf_vector的數組,這個數組和desc是一一對應的,其關係如下所示:
在這裏插入圖片描述
那麼爲什麼要引入這個buf_vector數組呢?首先我們知道所謂merge_rx的功能,主要是爲了vm接收大包(實現LRO的功能)。一般來說,一個mbuf會對應轉換成爲一個desc,但是當mbuf稍微大點,一個desc無法裝下怎麼辦呢?
這時不要忘了desc中的next成員,即desc也是可以形成chain的,如果當前desc無法裝下mbuf,那麼就用vq->desc[desc->next]來繼續存放。如下圖:
在這裏插入圖片描述
desc1,desc3,desc5形成一個chain,mbuf由desc1和desc3存放。那麼如果mbuf再大點呢?達到desc1,desc3,desc5三個desc都無法存放呢?這時如果沒有打開VIRTIO_NET_F_MRG_RXBUF調用virtio_dev_rx就會接收出錯了(儘管還有其他可用的desc,但由於不在一個chain,所以也不會使用)。但是如果打開了VIRTIO_NET_F_MRG_RXBUF,則會嘗試找其他其他的chain。

還以上圖爲例,desc數組中共有兩個chain1:desc1->desc3->desc5; chain2:desc2->desc4->desc6,那麼當chain1無法存下這個mbuf數據時,mbuf剩下的數據將由chain2存放。主要:一個mbuf絕不可能按照desc1àdesc2的順序存放,因爲desc1和desc2屬於不同的chain,只有當前chain使用完才能使用另一個chain。

由於desc數組的順序不一定是按照chain的順序組織的(如上圖),所以爲了方便後續的mbuf到各個desc的拷貝操作,我們增加了buf_vector這個數組,用它來記錄拷貝mbuf的desc順序。如下圖所示:
在這裏插入圖片描述
這樣我們拷貝mbuf時就可以按照buf_vector1到buf_vector6依次拷貝到對應的desc中了。

另外我們注意一點,在函數的最後會對last_avail_idx進行更新:vq->last_avail_idx += num_buffers; 注意這裏的num_buffers不是這個mbuf使用的desc個數,而是使用的desc chain個數。還以上面mbuf使用兩個chain爲例,last_avail_idx需要加2,如下圖所示,如果之前last_avail_idx爲1的話,這裏就要更新爲3了。
在這裏插入圖片描述
這裏容易混淆的一點是:avail->ring中存放的index不是和desc一一對應的,而是和desc chain一一對應的,即只存放desc chain header的index。所以avail->idx-last_avail_id也不是可用desc的個數,而是可用desc chain的個數。

有了以上背景,我們再看reserve_avail_buf_mergeable這個函數就容易理解多了。這個函數的作用就是:預留足夠的desc來存放mbuf,同時使用buf_vec來記錄,每個buf_vec對應一個desc,如果當前所有可用的desc都無法裝得下這個mbuf則返錯。

reserve_avail_buf_mergeable

static inline int
reserve_avail_buf_mergeable(struct virtio_net *dev, struct vhost_virtqueue *vq,
                                     uint32_t size, struct buf_vector *buf_vec,
                                     uint16_t *num_buffers, uint16_t avail_head)
{
         uint16_t cur_idx;
         uint32_t vec_idx = 0;
         uint16_t tries = 0;
 
         uint16_t head_idx = 0;
         uint16_t len = 0;
    /*num_buffers爲要使用的desc chain的個數*/
         *num_buffers = 0;
         cur_idx = vq->last_avail_idx;
    /*每次遍歷一個desc chain,遍歷過的chain加起來可以存放下這個mbuf*/
         while (size > 0) {
                  if (unlikely(cur_idx == avail_head))
                            return -1;
        /*fill_vec_buf的作用遍歷下一個desc chain,用來存放mbuf,然後buf_vec記錄這些desc的信息*/
                   if (unlikely(fill_vec_buf(dev, vq, cur_idx, &vec_idx, buf_vec,
                                                        &head_idx, &len) < 0))
                            return -1;
                   len = RTE_MIN(len, size);
                   update_shadow_used_ring(vq, head_idx, len);
                   size -= len;
 
                   cur_idx++;
                   tries++;
                   *num_buffers += 1;
 
                   /*
                    * if we tried all available ring items, and still
                    * can't get enough buf, it means something abnormal
                    * happened.
                    */
                    /*當嘗試次數大於了vq->size,說明所有可用的desc都被掃描過了,
                    即所有可用的desc加起來都無法滿足這個mbuf*/
                   if (unlikely(tries >= vq->size))
                            return -1;
         }
 
         return 0;
}

這裏需要注意的一點就是num_buffers不是使用desc的個數,而是使用的desc chain個數。而fill_vec_buf負責每次挑選一個desc chain填入對應的buf_vector,注意傳入參數head_idx每次隨着被desc填充而被修改。下面看fill_vec_buf。

fill_vec_buf

static inline int __attribute__((always_inline))
fill_vec_buf(struct virtio_net *dev, struct vhost_virtqueue *vq,
                             uint32_t avail_idx, uint32_t *vec_idx,
                             struct buf_vector *buf_vec, uint16_t *desc_chain_head,
                             uint16_t *desc_chain_len)
{
         uint16_t idx = vq->avail->ring[avail_idx & (vq->size - 1)];
         uint32_t vec_id = *vec_idx;
         uint32_t len = 0;
         struct vring_desc *descs = vq->desc;
 
         *desc_chain_head = idx;
    /* VRING_DESC_F_INDIRECT處理 */
         if (vq->desc[idx].flags & VRING_DESC_F_INDIRECT) {
                   descs = (struct vring_desc *)(uintptr_t)
                                               gpa_to_vva(dev, vq->desc[idx].addr);
                   if (unlikely(!descs))
                            return -1;
 
                   idx = 0;
         }
    /*遍歷這個desc chain知道結束,將這個chain中的desc信息記錄到buf_vec中,
     *chain能存放的數據長度賦值給desc_chain_len*/
         while (1) {
                   if (unlikely(vec_id >= BUF_VECTOR_MAX || idx >= vq->size))
                            return -1;
 
                   len += descs[idx].len;
                   buf_vec[vec_id].buf_addr = descs[idx].addr;
                   buf_vec[vec_id].buf_len = descs[idx].len;
                   buf_vec[vec_id].desc_idx = idx;
                   vec_id++;
 
                   if ((descs[idx].flags & VRING_DESC_F_NEXT) == 0)
                            break;
 
                   idx = descs[idx].next;
         }
 
         *desc_chain_len = len;
         *vec_idx = vec_id;
 
         return 0;
}

這個函數比較簡單,就是遍歷一個desc chain到結束爲止,然後將這個chain的信息存放在buf_vec中,將這個chain能存放的數據長度信息返回,以供上層判斷是否還需要再找下一個chain填充。

這裏有一個VRING_DESC_F_INDIRECT 的desc特性的判斷,這裏順便說下direct desc和indirect desc的區別。通常的desc->addr指向的是存放數據的page,這樣的desc叫做direct desc。但是indirect desc的desc->addr指向的是一個direct desc數組,如下圖所示。(主要indirect desc指向的只能是direct desc,即不能再繼續級聯下去)
在這裏插入圖片描述
最後在開啓mergeable後virtio_dev_merge_rx的調用和普通模式virtio_dev_rx的調用還有點不同。在virtio_dev_rx中,每次將mbuf中的數據存放在一個desc後都會更新vq->used->ring和vq->last_used_idx,即告訴前端那些desc中已經存放了數據。但在virtio_dev_merge_rx中卻沒有看到這個過程。其實這個過程是有的,我們注意到在reserve_avail_buf_mergeable中每次調用完fill_vec_buf就會調用一下update_shadow_used_ring,我們看一下其實現。

static inline void __attribute__((always_inline))
update_shadow_used_ring(struct vhost_virtqueue *vq,
                             uint16_t desc_idx, uint16_t len)
{
         uint16_t i = vq->shadow_used_idx++;
 
         vq->shadow_used_ring[i].id = desc_idx;
         vq->shadow_used_ring[i].len = len;
}

這裏會更新shadow_used_idx和shadow_used_ring。這裏引入的shadow_used_idx和shadow_used_ring其實是爲了最後批處理更新vq->used->ring,通常更新vq->used->ring要先找到對應的idx,在更新vq->used->ring[idx]。如果對於要使用多個desc chain的情況,這樣每次更新就會造成較大的訪存開銷。那我們看看使用shadow_used_idx和shadow_used_ring會怎麼更新。在virtio_dev_merge_rx中拷貝完mbuf數據後,最後會調用flush_shadow_used_ring函數。

flush_shadow_used_ring

static inline void __attribute__((always_inline))
flush_shadow_used_ring(struct virtio_net *dev, struct vhost_virtqueue *vq)
{
         uint16_t used_idx = vq->last_used_idx & (vq->size - 1);
         /*vq->shadow_used_idx存放的是本次使用的desc chain個數*/
    /*如果used_idx + vq->shadow_used_idx沒有產生環形隊列迴繞*/
         if (used_idx + vq->shadow_used_idx <= vq->size) {
                   do_flush_shadow_used_ring(dev, vq, used_idx, 0,
                                                 vq->shadow_used_idx);
         } else {
                   uint16_t size;
        /*used_idx + vq->shadow_used_idx產生了迴繞,需要分首尾兩部分更新*/
                   /* update used ring interval [used_idx, vq->size] */
                   size = vq->size - used_idx;
                   do_flush_shadow_used_ring(dev, vq, used_idx, 0, size);
 
                   /* update the left half used ring interval [0, left_size] */
                   do_flush_shadow_used_ring(dev, vq, 0, size,
                                                 vq->shadow_used_idx - size);
         }
         vq->last_used_idx += vq->shadow_used_idx;
 
         rte_smp_wmb();
    /*更新vq->used->idx*/
         *(volatile uint16_t *)&vq->used->idx += vq->shadow_used_idx;
    /*更新熱遷移位圖*/
         vhost_log_used_vring(dev, vq, offsetof(struct vring_used, idx),
                   sizeof(vq->used->idx));
}

我們再看下do_flush_shadow_used_ring的處理。

do_flush_shadow_used_ring

static inline void __attribute__((always_inline))
do_flush_shadow_used_ring(struct virtio_net *dev, struct vhost_virtqueue *vq,
                              uint16_t to, uint16_t from, uint16_t size)
{
         rte_memcpy(&vq->used->ring[to],
                            &vq->shadow_used_ring[from],
                            size * sizeof(struct vring_used_elem));
         vhost_log_used_vring(dev, vq,
                            offsetof(struct vring_used, ring[to]),
                            size * sizeof(struct vring_used_elem));
}

可以看到由於之前shadow_used_ring中有相關記錄,所以這裏可以一次性拷貝到uesd_ring中,減少了內存訪問次數。

原文鏈接:http://blog.chinaunix.net/uid-8574039-id-5826405.html

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