DPDK vhost-user之mergeable對接收端(guest)的影響(八)

這裏在分析一下guset內部對於開啓mergeable接收會有什麼影響,順便分析一下開啓GUEST_GSO/GUEST_TSO時,guset內部的接收流程。

首先我們從vhost-user,發送端分析一下,兩種情況是如何更新used->ring的。
在這裏插入圖片描述

mergeable情況

在reserve_avail_buf_mergeable(dpdk代碼)函數中有一下邏輯:

/*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);

這裏我們在“vhost_user mergeable特性”中已經分析過,fill_vec_buf是遍歷當前desc chain,然後將這個chain的信息記錄在buf_vec中,同時len中返回的是這個chain能存放的數據長度。

在update_shadow_used_ring中會將這個長度賦值給vq->shadow_used_ring[i].len,如下:

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;
}

最後在flush_shadow_used_ring中vq->shadow_used_ring[i].len最終被賦值給vq->used->ring[i].len。也就是vq->used->ring[i].len存放的是一個chain的長度。

開啓GUEST_GSO/GUEST_TSO (不開啓mergeable)

這種情況vhost_user後端不會去特殊處理,和普通報文一樣。在virtio_dev_rx的處理邏輯中有如下代碼:

for (i = 0; i < count; i++) {
          used_idx = (start_idx + i) & (vq->size - 1);
          desc_indexes[i] = vq->avail->ring[used_idx];
          vq->used->ring[used_idx].id = desc_indexes[i];
		  /* vq->used->ring[used_idx].len 存放的是整個數據包長加上virtio header的長度*/
          vq->used->ring[used_idx].len = pkts[i]->pkt_len + dev->vhost_hlen;
          vhost_log_used_vring(dev, vq,
                   offsetof(struct vring_used, ring[used_idx]),
                   sizeof(vq->used->ring[used_idx]));
}

這裏vq->used->ring[used_idx].len 存放的是整個數據包長加上virtio header的長度,因爲在非mergeable情況,一個數據包要麼被一個chain裝完,要麼丟棄,所以只有發送成功,就不存在一個chain只裝了部分數據的情況。

下面我們再看guset接收端代碼,就kernel(3.10)的virtio_net代碼。以下是guset的收包邏輯:
在這裏插入圖片描述

virtnet_poll

static int virtnet_poll(struct napi_struct *napi, int budget)
{
    /*……*/
         while (received < budget && /*virtqueue_get_buf取出要接收的skb*/
                (buf = virtqueue_get_buf(rq->vq, &len)) != NULL) {
                   receive_buf(rq, buf, len); /*真正的接收處理操作,最終調用netif_receive_skb*/
                   --rq->num;
                   received++;
         }
    /*……*/
}

我們只看和我們分析有關的邏輯。首先調用virtqueue_get_buf從隊列中取出一個mbuf,並返回一個長度len。

void *virtqueue_get_buf(struct virtqueue *_vq, unsigned int *len)
{
         struct vring_virtqueue *vq = to_vvq(_vq);
         void *ret;
         unsigned int i;
         u16 last_used;
    /*……*/
         virtio_rmb(vq->weak_barriers);
    /*獲取本次要是有的used_elem數組index*/
         last_used = (vq->last_used_idx & (vq->vring.num - 1));
         i = vq->vring.used->ring[last_used].id; /*本次要接受skb對應的data下標,也是skb對應第一個desc的index*/
         *len = vq->vring.used->ring[last_used].len;/*本次要接受skb的長度*/
 
         /* detach_buf clears data, so grab it now. */
         /*取出要接受的skb*/
         ret = vq->data[i];
         /*釋放skb對應的desc chain*/
         detach_buf(vq, i);
         vq->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;
                   virtio_mb(vq->weak_barriers);
         }
 
         END_USE(vq);
         return ret;
}

這裏注意以下幾點:

  1. 返回的len存放的是vq->vring.used->ring[last_used].len中的值,上面我們分析過,在mergeable情況下這是一個chain的長度(如果數據包的長度小於chain能裝的數據長度,則爲數據包的長度+virtio header),在GUEST_TSO*的情況,這是一個數據包的長度+virtio header;
  2. detach_buf 會釋放當前desc chain,而不僅是一個desc,因爲無論那種情況,這個chain中的數據再之後都會被取出,可以歸還給後端了。

receive_buf

static void receive_buf(struct receive_queue *rq, void *buf, unsigned int len)
{
         struct virtnet_info *vi = rq->vq->vdev->priv;
         struct net_device *dev = vi->dev;
         struct virtnet_stats *stats = this_cpu_ptr(vi->stats);
         struct sk_buff *skb;
         struct skb_vnet_hdr *hdr;
    /*……*/
         if (vi->mergeable_rx_bufs)
                   skb = receive_mergeable(dev, rq, buf, len);
         else if (vi->big_packets)
                   skb = receive_big(dev, rq, buf);
         else
                   skb = receive_small(buf, len);
 
         if (unlikely(!skb))
                   return;
 
         hdr = skb_vnet_hdr(skb);
 
         u64_stats_update_begin(&stats->rx_syncp);
         stats->rx_bytes += skb->len;
         stats->rx_packets++;
         u64_stats_update_end(&stats->rx_syncp);
 
         if (hdr->hdr.flags & VIRTIO_NET_HDR_F_NEEDS_CSUM) {
                   pr_debug("Needs csum!\n");
                   if (!skb_partial_csum_set(skb,
                                                 hdr->hdr.csum_start,
                                                 hdr->hdr.csum_offset))
                            goto frame_err;
         } else if (hdr->hdr.flags & VIRTIO_NET_HDR_F_DATA_VALID) {
                   skb->ip_summed = CHECKSUM_UNNECESSARY;
         }
 
         skb->protocol = eth_type_trans(skb, dev);
         pr_debug("Receiving skb proto 0x%04x len %i type %i\n",
                    ntohs(skb->protocol), skb->len, skb->pkt_type);
    /*根據後端填入virtio_net_hdr中的信息,設置gso的相關字段,說明收到的是大包*/
         if (hdr->hdr.gso_type != VIRTIO_NET_HDR_GSO_NONE) {
                   pr_debug("GSO!\n");
                   switch (hdr->hdr.gso_type & ~VIRTIO_NET_HDR_GSO_ECN) {
                   case VIRTIO_NET_HDR_GSO_TCPV4:
                            skb_shinfo(skb)->gso_type = SKB_GSO_TCPV4;
                            break;
                   case VIRTIO_NET_HDR_GSO_UDP:
                            skb_shinfo(skb)->gso_type = SKB_GSO_UDP;
                            break;
                   case VIRTIO_NET_HDR_GSO_TCPV6:
                            skb_shinfo(skb)->gso_type = SKB_GSO_TCPV6;
                            break;
                   default:
                            net_warn_ratelimited("%s: bad gso type %u.\n",
                                                    dev->name, hdr->hdr.gso_type);
                            goto frame_err;
                   }
 
                   if (hdr->hdr.gso_type & VIRTIO_NET_HDR_GSO_ECN)
                            skb_shinfo(skb)->gso_type |= SKB_GSO_TCP_ECN;
 
                   skb_shinfo(skb)->gso_size = hdr->hdr.gso_size;
                   if (skb_shinfo(skb)->gso_size == 0) {
                            net_warn_ratelimited("%s: zero gso size.\n", dev->name);
                            goto frame_err;
                   }
 
                   /* Header must be checked, and gso_segs computed. */
                   skb_shinfo(skb)->gso_type |= SKB_GSO_DODGY;
                   skb_shinfo(skb)->gso_segs = 0;
         }
    /*發往協議棧*/
         netif_receive_skb(skb);
         return;
 
frame_err:
         dev->stats.rx_frame_errors++;
         dev_kfree_skb(skb);
}

如果mergeable開啓,則vi->mergeable_rx_bufs會被置位,如果GUEST_TSO* 被打開,則 vi->big_packets會被置位。所以分析兩種情況的接收處理就是看相應的調用函數,即receive_mergeable和receive_big。在分析這兩個函數前,首先來看receive_buf的後半部分,根據後端填入virtio_net_hdr中的信息,設置gso(這裏用於收方向,即gro)的相關字段。 所以要想guset能夠接收大包(LRO)功能不但需要開啓相關flag(GUEST_TSO*或mergeable),還依賴後端對virtio header的設置,如果後端處理了切割大包邏輯,以鏈表形式給前端,並設置相應virtio header,則guset就可以不用再分片,否則如果後端沒有處理分片,僅僅把大包發給guset,則guset還需要進行GRO處理。 下面分析

receive_mergeable

static struct sk_buff *receive_mergeable(struct net_device *dev,
                                                struct receive_queue *rq,
                                                void *buf,
                                                unsigned int len)
{
    /*從第一個page中獲取到virtio header*/
         struct skb_vnet_hdr *hdr = page_address(buf);
         /*從virtio header中獲取這個數據包所用的desc chain個數*/
         int num_buf = hdr->mhdr.num_buffers;
         struct page *page = buf;
         /*將page中的數據轉換爲skb*/
         struct sk_buff *skb = page_to_skb(rq, page, len);
         int i;
 
         if (unlikely(!skb))
                   goto err_skb;
 
         while (--num_buf) { /*對應當前數據包使用的每個chain*/
                   i = skb_shinfo(skb)->nr_frags;
                   if (i >= MAX_SKB_FRAGS) {
                            pr_debug("%s: packet too long\n", skb->dev->name);
                            skb->dev->stats.rx_length_errors++;
                            goto err_frags;
                   }
        /*對接下來的每個desc chain 再次調用virtqueue_get_buf */
                   page = virtqueue_get_buf(rq->vq, &len);
                   if (!page) {
                            pr_debug("%s: rx error: %d buffers %d missing\n",
                                      dev->name, hdr->mhdr.num_buffers, num_buf);
                            dev->stats.rx_length_errors++;
                            goto err_buf;
                   }
 
                   if (len > PAGE_SIZE)
                            len = PAGE_SIZE;
 
                   set_skb_frag(skb, page, 0, &len);
 
                   --rq->num;
         }
         return skb;
}

可以看出mergeable的情況,由於一個數據包可能使用多個chain,則會對每個chain在此調用virtqueue_get_buf,獲取對應page(mergeable的情況每個chain的長度爲1,對應的也是一個page),然後通過set_skb_frag將之後的每個chain(desc)對應的page加入首個skb的skb_shinfo(skb)->frags[i]中。所以mergeable情況收到的大包,會有skb_shinfo(skb)->frags[],其對應的每個desc對應skb_shinfo(skb)->frags[]的一個page。下面看receive_big。

receive_big

static struct sk_buff *receive_big(struct net_device *dev,
                                        struct receive_queue *rq,
                                        void *buf)
{
         struct page *page = buf;
         struct sk_buff *skb = page_to_skb(rq, page, 0);
         return skb;
}

直接調用page_to_skb,這個在receive_mergeable中也有調用。

page_to_skb

static struct sk_buff *page_to_skb(struct receive_queue *rq,
                                        struct page *page, unsigned int len)
{
         struct virtnet_info *vi = rq->vq->vdev->priv;
         struct sk_buff *skb;
         struct skb_vnet_hdr *hdr;
         unsigned int copy, hdr_len, offset;
         char *p;
 
         p = page_address(page);
 
         /* copy small packet so we can reuse these pages for small data */
         skb = netdev_alloc_skb_ip_align(vi->dev, GOOD_COPY_LEN);
         if (unlikely(!skb))
                   return NULL;
 
         hdr = skb_vnet_hdr(skb);
 
         if (vi->mergeable_rx_bufs) {
                   hdr_len = sizeof hdr->mhdr;
                   offset = hdr_len;
         } else {
                   hdr_len = sizeof hdr->hdr;
                   offset = sizeof(struct padded_vnet_hdr);
         }
    /*提取virtio header*/
         memcpy(hdr, p, hdr_len);
 
         len -= hdr_len;
         p += offset;
    /*將剩餘數據儘可能的拷貝到當前的skb線性區中*/
         copy = len;
         if (copy > skb_tailroom(skb))
                   copy = skb_tailroom(skb);
         memcpy(skb_put(skb, copy), p, copy);
 
         len -= copy;
         offset += copy;
 
         /*
          * Verify that we can indeed put this data into a skb.
          * This is here to handle cases when the device erroneously
          * tries to receive more than is possible. This is usually
          * the case of a broken device.
          */
         if (unlikely(len > MAX_SKB_FRAGS * PAGE_SIZE)) {
                   net_dbg_ratelimited("%s: too much data\n", skb->dev->name);
                   dev_kfree_skb(skb);
                   return NULL;
         }
    /*如果第一個skb的線性區用完了,但是還有數據沒拷貝出來,則添加到skb_shinfo(skb)->frags[]*/
         while (len) {
                   set_skb_frag(skb, page, offset, &len);
                   page = (struct page *)page->private;
                   offset = 0;
         }
 
         if (page)
                   give_pages(rq, page);
 
         return skb;
}

從上面的過程總結一下:當開啓GUEST_TSO*時,guest收大包會盡可能的填充skb的線性區,剩餘數據填充skb_shinfo(skb)->frags[],而對於mergeable由於只有第一個chain(也就是一個desc)對應的page會填充skb線性區,其他數據都在skb_shinfo(skb)->frags[],所以mergeable可能會有更多frags。

另外注意一點,當mergeable和GUEST_TSO*同時開啓時,由於guest是優先判斷mergeable的,所以就會走mergeable邏輯。

在實現LRO時,建議使用mergeable特性,因爲如果使用GUEST_TSO*,則接收小包也會是由長爲17的desc chain,這會造成浪費。

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

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