内容字号:默认大号超大号

段落设置:段首缩进取消段首缩进

字体设置:切换到微软雅黑切换到宋体

virtio前后端配合限速分析:VIRTIO_RING_F_EVENT_IDX

2018-03-12 17:18 出处:清屏网 人气: 评论(0

virtio 前后端配合限速分析

——lvyilong316

VIRTIO 中,有个一个设备的特性叫做 VIRTIO_RING_F_EVENT_IDX ,这个特性是用来对前后端速率进行匹配限速的。我们知道 avail ring ,这个 ring 有两个用途,一是发送侧 (send queue) 前端驱动发送报文的时,将待发送报文加入 avail ring 等待后端的处理,后端处理完后,会将其放入 used ring ,并由前端将其释放 desc (free_old_xmit_skbs, detach_buf) ,最后通过 try_fill_recv 重新装入 availring ; 二是接收侧 (receive qeueu) ,前端将空白物理块加入 avail ring 中,提供给后端用来接收报文,后端接收完报文会放入 used ring 。可以看出: 都是后端用完前端的 avail ring 的东西放入 used ring ,也就是前端消耗 uesd ,后端消耗 avail 。所以 本特性中后端用了 used ring 的最后一个元素,告诉前端驱动后端处理到哪个 avail ring 上的元素了,同时前端使用 avail ring 的最后一个元素告诉后端,处理到那个 used ring 了。

发送方向限速

我们看发送方向的限速。首先看前端 guest 的发送逻辑 (kernel 3.10: virtio-net) ,在 guest 发送时会调用 virtqueue_add

函数:

virtqueue_add 函数将要发送的 skb 转换的 sg 再转换为 desc chain 。具体转换流程不是这里分析的重点,我们只看 virtqueue_add 函数的最后一部分。

l   virtqueue_add

点击( 此处 )折叠或打开

  1. static inline int virtqueue_add ( struct virtqueue * _vq ,)
  2. {
  3. / * 省略sg到desc chain的具体转换逻辑 * /
  4. add_head :
  5.           / * Set token . 记录数组记录本次发送的skb * /
  6.          vq - > data [ head ] = data ; / * head 为记录次skb的首个desc的下标,data为本skb的地址 * /
  7.  
  8.            / * 更新avail * /
  9.          avail = ( vq - > vring . avail - > idx & ( vq - > vring . num - 1 ) ) ;
  10.          vq - > vring . avail - > ring [ avail ] = head ; / * 将本次要发送的首个desc下标记录在avail - > ring [ vq - > vring . avail - > idx ] ,vq - > vring . avail - > idx 是avail ring下一个可用的index * /
  11.  
  12.          virtio_wmb ( vq - > weak_barriers ) ;
  13.          vq - > vring . avail - > idx + + ;
  14.          vq - > num_added + + ; / * 更新num_added ,num_added 记录从上一次kick后端后,前端新增加的desc数量 * /
  15.  
  16.           / * This is very unlikely , but theoretically possible . Kick
  17.            * just in case . * /
  18.            / * 如果avail的数量太多,则kick后端收包,这种情况是你很难发生的 * /
  19.           if ( unlikely ( vq - > num_added = = ( 1 < < 16 ) - 1 ) )
  20.                    virtqueue_kick ( _vq ) ;
  21.  
  22.          pr_debug ( "Added buffer head %i to %p\n" , head , vq ) ;
  23.          END_USE ( vq ) ;
  24.  
  25.          return 0 ;
  26. }

这里一个关键点就是 vq->num_added 这个变量在这里记录从上一次 kick 后端后,前端新增加的 desc 数量 。我们看到在 kick 后端前( virtqueue_kick ),有一个判断: vq->num_added == (1<<16) - 1) ,也就是 当前端上层发送 kick 后,这段期间如果累计填充的 desc 不足 65535 个时,就先不去 kick 后端处理,这样不用每次发送 skb kick 后端,提高后端的处理效率

下面看如果达到了 65535 调用 virtqueue_kick 的逻辑。

l   virtqueue_kick

点击( 此处 )折叠或打开

  1. void virtqueue_kick ( struct virtqueue * vq )
  2. {
  3.           if ( virtqueue_kick_prepare ( vq ) )
  4.                    virtqueue_notify ( vq ) ;
  5. }

在真正调用 virtqueue_notify kick 后端前,会调用 virtqueue_kick_prepare 来再次判断是否需要 kick ,这也是我们要分析的重点。

l   virtqueue_kick_prepare

点击( 此处 )折叠或打开

  1. bool virtqueue_kick_prepare ( struct virtqueue * _vq )
  2. {
  3.          struct vring_virtqueue * vq = to_vvq ( _vq ) ;
  4.          u16 new , old ;
  5.          bool needs_kick ;
  6.  
  7.          START_USE ( vq ) ;
  8.          virtio_mb ( vq - > weak_barriers ) ;
  9.      / * old上次kick后的avail . idx * /
  10.          old = vq - > vring . avail - > idx - vq - > num_added ;
  11.           / * new是当前的avail . idx * /
  12.          new = vq - > vring . avail - > idx ;
  13.          vq - > num_added = 0 ;
  14.      / * 当VIRTIO_RING_F_EVENT_IDX 被设置的时候vq - > event 为1 * /
  15.           if ( vq - > event ) {
  16.                    needs_kick = vring_need_event ( vring_avail_event ( & vq - > vring ) ,
  17.                                                      new , old ) ;
  18.           } else {
  19.                    needs_kick = ! ( vq - > vring . used - > flags & VRING_USED_F_NO_NOTIFY ) ;
  20.           }
  21.          END_USE ( vq ) ;
  22.          return needs_kick ;
  23. }

这里我们注意两个变量, old new old 表示上次 kick 后的 avail.idx new 是当前的 avail.idx ,两者的差值就是 vq->num_added ,也就是自上次 kick 后端后前端又积累的 desc chain 数量。另外 vq->event 是在 vq 初始化的时候设置的,在 vring_new_virtqueue 有如下代码:

vq->event = virtio_has_feature(vdev, VIRTIO_RING_F_EVENT_IDX);

所以当设置了 VIRTIO_RING_F_EVENT_IDX 后, vq->event 就会被置位,这里就会调用 needs_kick = vring_need_event(vring_avail_event(&vq->vring), new, old);

注意 vring_avail_event(&vq->vring) 为: (vr)->used->ring[(vr)->num] ,即 uesd ring 的最后一个元素, 后端用 used ring 的最后一个元素告诉前端后端处理的位置

l   vring_need_event

点击( 此处 )折叠或打开

  1. static inline int vring_need_event ( __u16 event_idx , __u16 new_idx , __u16 old )
  2. {
  3.          return ( __u16 ) ( new_idx - event_idx - 1 ) < ( __u16 ) ( new_idx - old ) ;
  4. }
这个公式决定了是否想后端 QEMU 发送通知

当满足公式的时候,后端处理的位置 event_idx 超过了 old ,表示后端 QEMU 处理的速度够快,索引返回 true ,通知 (kick) 后端,通知 后端 有新的 avail 逻辑 buf ,请你继续处理

如下下面情况:

后端处理的位置 event_idx 落后于上次添加 avail ring 的位置,说明后端处理较慢,返回 false ,那么前端就先不通知 (kick) ,积攒一下,反正 后端 正处理不过来,下次退出的时候,让 后端 一起尽情处理

然后我们看下后端是如何处理的,看 guest 发方向,为了简单起见我们选择 dpdk 18.02 中的 vhost_user 来分析( vhost_net 相对逻辑比较绕,另外较新版本的 dpdk 才支持 VIRTIO_RING_F_EVENT_IDX )。 guest 的发送,对应后端的 dequeue 操作。

l   rte_vhost_dequeue_burst

点击( 此处 )折叠或打开

  1. uint16_t
  2. rte_vhost_dequeue_burst ( int vid , uint16_t queue_id ,
  3.          struct rte_mempool * mbuf_pool , struct rte_mbuf * * pkts , uint16_t count )
  4. {
  5. / * 省略前面dequeue操作 * /
  6.            if ( likely ( dev - > dequeue_zero_copy = = 0 ) ) {
  7.                    do_data_copy_dequeue ( vq ) ;
  8.                    vq - > last_used_idx + = i ; / * 更新last_used_idx ,i为本次dequeue的mbuf数量,也就是清空的desc数量 * /
  9.                    update_used_idx ( dev , vq , i ) ;
  10.            }
  11. / * …… * /
  12. }

更新 vq->last_used_idx 后,调用 update_used_idx

l   update_used_idx

点击( 此处 )折叠或打开

  1. static __rte_always_inline void
  2. update_used_idx ( struct virtio_net * dev , struct vhost_virtqueue * vq ,
  3.                    uint32_t count )
  4. {
  5.           if ( unlikely ( count = = 0 ) )
  6.                    return ;
  7.  
  8.          rte_smp_wmb ( ) ;
  9.          rte_smp_rmb ( ) ;
  10.  
  11.          vq - > used - > idx + = count ; / * 更新vq - > used - > idx * /
  12.          vhost_log_used_vring ( dev , vq , offsetof ( struct vring_used , idx ) ,
  13.                             sizeof ( vq - > used - > idx ) ) ;
  14.          vhost_vring_call ( dev , vq ) ; / * 调用vhost_vring_call call前端 * /
  15. }

vhost_vring_call 主要作用是 Call 前端,告诉前端 desc 中的数据已经取出,前端可以回收了。

l   vhost_vring_call

点击( 此处 )折叠或打开

  1. static __rte_always_inline void
  2. vhost_vring_call ( struct virtio_net * dev , struct vhost_virtqueue * vq )
  3. {
  4.           / * Flush used - > idx update before we read avail - > flags . * /
  5.          rte_mb ( ) ;
  6.  
  7.           / * Don ' t kick guest if we don ' t reach index specified by guest . * /
  8.           if ( dev - > features & ( 1ULL < < VIRTIO_RING_F_EVENT_IDX ) ) {
  9.                    uint16_t old = vq - > signalled_used ;
  10.                    uint16_t new = vq - > last_used_idx ;
  11.  
  12.                    LOG_DEBUG ( VHOST_DATA , "%s: used_event_idx=%d, old=%d, new=%d\n" ,
  13.                             __func__ ,
  14.                             vhost_used_event ( vq ) ,
  15.                             old , new ) ;
  16.                     if ( vhost_need_event ( vhost_used_event ( vq ) , new , old )
  17.                              & & ( vq - > callfd > = 0 ) ) {
  18.                             vq - > signalled_used = vq - > last_used_idx ; / * 更新vq - > signalled_used为本次Call前端后的used idx * /
  19.                             eventfd_write ( vq - > callfd , ( eventfd_t ) 1 ) ;
  20.                     }
  21.           } else {
  22.                     / * Kick the guest if necessary . * /
  23.                     if ( ! ( vq - > avail - > flags & VRING_AVAIL_F_NO_INTERRUPT )
  24.                                       & & ( vq - > callfd > = 0 ) )
  25.                             eventfd_write ( vq - > callfd , ( eventfd_t ) 1 ) ;
  26.           }
  27. }

当设置 VIRTIO_RING_F_EVENT_IDX 后,会调用 vhost_need_event 判断是否需要 Call 前端,否则直接调用 eventfd_write Call 前端。在看 vhost_need_event 实现之前,先看其后的一行代码:

vq->signalled_used = vq->last_used_idx;

这里将 vq->signalled_used 更新为本次 Call 前端后的 used idx ,那么对于调用 vhost_need_event 时,这里存放的应该就是上次 Call 前端的 used idx ,弄清楚这个看 vhost_need_event 的实现就很容易了。

l   vhost_need_event

点击( 此处 )折叠或打开

  1. static __rte_always_inline int
  2. vhost_need_event ( uint16_t event_idx , uint16_t new_idx , uint16_t old )
  3. {
  4.          return ( uint16_t ) ( new_idx - event_idx - 1 ) < ( uint16_t ) ( new_idx - old ) ;
  5. }

其中第一个参数为 (vq)->avail->ring[(vq)->size] ,前端使用 avail ring 最后一个元素通知后端。

new_idx – old 大于 new_idx - event_idx – 1 ,说明前端也更新了 used idx ,则后端可以进行 Call 通知前端,否则则暂时不通知。

这里还有一点要注意,我们看到后端使用 avail->ring 的最后一个元素来判断前端的使用情况,那么前端是什么时候更新这个值呢?答案是在 virtqueue_get_buf 中( kernel 3.10 virtio-net ),这个函数在前端发送和接收时都会被调用,根据 used ring desc 中得到 skbbuf (发送时得到的是待填入数据的 skbbuf ,接收时得到的是带有有效数据的 skbbuf )。在函数的末尾有如下逻辑:

点击( 此处 )折叠或打开

  1. if ( ! ( vq - > vring . avail - > flags & VRING_AVAIL_F_NO_INTERRUPT ) ) {
  2.                    vring_used_event ( & vq - > vring ) = vq - > last_used_idx ;
  3.                    virtio_mb ( vq - > weak_barriers ) ;
  4. }

其中 vring_used_event 定义如下:

#define vring_used_event(vr) ((vr)->avail->ring[(vr)->num])

这样前端就更新了 avail ring 最后一个元素。

好像还是缺少点什么?我们开始看到前端 virtio-net 是根据 used ring 的最后一个元素来判断是否要 kick 后端,按照这个逻辑后端应该有地方来更新 used ring 的最后一个元素才对。可惜我们再 vhost-user 的逻辑中并没有发现相关操作。这是为什么呢?我们回头想想 整个逻辑的目的,是降低前端 kick ,后端 Call 的频率,是只在不影响对端使用的情况,尽可能的多积攒一些再通知。 而我们知道 vhost-user 采用的是 pmd ,根本不去管这个通知,所以也就没有必要去配合前端了,前端想 kick 就使劲 kick ,反正也不受影响。

接收方向限速

收方向类似,这里不再展开,只给出调用路径。先从后端 vhost-user 开始分析。

后端:

virtio_dev_rx à vhost_vring_call à vhost_need_event à 通过 avail->ring 的最后一个元素判断是否 Call 前端;

前端:

try_fill_recv à virtqueue_kick à virtqueue_kick_prepare à vring_need_event à 通过 used->ring 的最后一个元素判断是否 kick 后端。

分享给小伙伴们:
本文标签: virtio

相关文章

发表评论愿您的每句评论,都能给大家的生活添色彩,带来共鸣,带来思索,带来快乐。

CopyRight © 2015-2016 QingPingShan.com , All Rights Reserved.

清屏网 版权所有 豫ICP备15026204号