版权声明:可以任意转载,转载时请务必以超链接形式标明文章原始出处和作者信息及本版权声明 (作者:张华 发表于:2018-01-09)
问题
日前遇到这么一个问题,客户反应虚机往外发包时丢包并看到”No buffer space available”相关的错误,虚机是windows虚机,宿主机是ubuntu并采用vhost-net机制。
systemtap
刚开始我们怀疑是这两个patches导致的问题:
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/drivers/vhost?id=8d65843c44269c21e95c98090d9bb4848d473853
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/drivers/vhost?id=809ecb9bca6a9424ccd392d67e368160f8b76c92
所以写了个systemtap脚本在宿主机监控vhost模块中的vhost_signal函数将vring的相关信息打印如下:
RX 0 values: old=34264 new=34264 last_used_event=34263 vring_used=0 vring_avail=0 TX 6297677760 values: old=30401 new=30402 last_used_event=0 vring_used=30402 vring_avail=30402RX 0 values: old=34264 new=34264 last_used_event=34263 vring_used=0 vring_avail=0 TX 6297677760 values: old=30403 new=30404 last_used_event=0 vring_used=30404 vring_avail=30404
根据打印的值,我们算出下面两个公式的结果均为False (https://github.com/torvalds/linux/blob/v4.10/drivers/vhost/vhost.c#L2189 )。
vring_need_event(vq->last_used_event, new + vq->num, new)
vring_need_event(vq->last_used_event, new, old)
TX 34936796160 values: old=7077 new=7077 last_used_event=0 vring_used=7077 vring_avail=7077 1. False: (7077 + 256) - 0 - 1 < (7077 + 256) - 7077 2. False: 7077 - 0 - 1 < 7077 - 7077
上面为False的话,vhost_notify()就为False,那样vhost也不会调用eventfd_signal通过中断向guest发通知(https://github.com/torvalds/linux/blob/v4.10/drivers/vhost/vhost.c#L2211)
void vhost_signal(struct vhost_dev *dev, struct vhost_virtqueue *vq)
{/* Signal the Guest tell them we used something up. */if (vq->call_ctx && vhost_notify(dev, vq))eventfd_signal(vq->call_ctx, 1);
}
理论分析
- Guest发数据:guest将发送报文Buffer的head index加入avial_ring中, 在合适的时间点通过ioeventfds消息来通知backend。backend发完报文后再将其加入到used_ring中,并在一个合适的时间点来通过irqdfs中断来通知guest。
- Guest收数据:两个queue都需要guest填充buffer, guest将空白Buffer的head index加入avail_ring中,在合适的时间点通过ioeventfds消息来通知backend。backend收完报文后再将其加入到used_ring中,并在一个合适的时间点来通过irqdfs中断来通知guest。
- Flags, avail_ring与used_ring中都有flags字段,例如avail_ring中的flags字段代表guest告诉host在host发完报文之后是否需要通知guest。
代码分析
在打开VIRTIO_RING_F_EVENT_IDX特性之后,virtio/vhost将不再依据flags来决定是否向对方发送消息(guest到host根据消息通知,host到guest通过中断),而是guest/host在发送一批报文后(flags是每个包发送性能较差)自行决定是否向对方发消息。
/* The standard layout for the ring is a continuous chunk of memory which looks* like this. We assume num is a power of 2.** struct vring* {* // The actual descriptors (16 bytes each)* struct vring_desc desc[num];** // A ring of available descriptor heads with free-running index.* __virtio16 avail_flags;* __virtio16 avail_idx;* __virtio16 available[num];* __virtio16 used_event_idx;** // Padding to the next align boundary.* char pad[];** // A ring of used descriptor heads with free-running index.* __virtio16 used_flags;* __virtio16 used_idx;* struct vring_used_elem used[num];* __virtio16 avail_event_idx;* };*/
此时,在数据结构方面,在used_ring中有avail_event_idx字段,在avail_ring中有used_event_idx字段,用下列方法相互填充。如:vring_avail_event(&vq->vring)用于将avail_ring中的index填充到used_ring的最后一个字段used_event_idx中去,反之亦然。
/* We publish the used event index at the end of the available ring, and vice* versa. They are at the end for backwards compatibility. */
#define vring_used_event(vr) ((vr)->avail->ring[(vr)->num])
#define vring_avail_event(vr) (*(__virtio16 *)&(vr)->used->ring[(vr)->num])
1, guest端virtio驱动, 根据vring_need_event()公式,guest在发送缓冲区满了之后才kick消息给host.
https://github.com/torvalds/linux/blob/v4.10/drivers/virtio/virtio_ring.c#L547bool virtqueue_kick_prepare(struct virtqueue *_vq){old = vq->avail_idx_shadow - vq->num_added;new = vq->avail_idx_shadow;vq->num_added = 0;...if (vq->event) {needs_kick = vring_need_event(virtio16_to_cpu(_vq->vdev, vring_avail_event(&vq->vring)),new, old);} else {needs_kick = !(vq->vring.used->flags & cpu_to_virtio16(_vq->vdev, VRING_USED_F_NO_NOTIFY));}vq->event = virtio_has_feature(vdev, VIRTIO_RING_F_EVENT_IDX);static inline int vring_need_event(__u16 event_idx, __u16 new_idx, __u16 old)
{return (__u16)(new_idx - event_idx - 1) < (__u16)(new_idx - old);
}struct vring_virtqueue {/* Last written value to avail->idx in guest byte order */u16 avail_idx_shadow;/* Head of free buffer list. */unsigned int free_head;
注: vring_need_event的公式(return (__u16)(new_idx - event_idx - 1) < (__u16)(new_idx - old);)实际是一种限速,new指针在前, old指标在后,如果:
- 如果event_idx也就是avail.idx的位置超过了old, vring_need_event=True, 表示后端处理的快(event_idx是后端通知给前端处理的索引值),此时guest将发通知给host请它继续处理。
- 如果event_idx在old之前,vring_need_event=False, 说明后端处理的慢,此时guest不会向host发通知。
2, guest端virtio驱动是如何发包的呢? xmit_skb会高用virtqueue_add_outbuf最终会调用virtqueue_add, 设置完ring的相关字段之后最后调virtqueue_kick
static int xmit_skb(struct send_queue *sq, struct sk_buff *skb)
{
...return virtqueue_add_outbuf(sq->vq, sq->sg, num_sg, skb, GFP_ATOMIC);
}static inline int virtqueue_add(struct virtqueue *_vq,struct scatterlist *sgs[],unsigned int total_sg,unsigned int out_sgs,unsigned int in_sgs,void *data,gfp_t gfp)
{
...head = vq->free_head;/* Put entry in available array (but don't update avail->idx until they* do sync). */avail = vq->avail_idx_shadow & (vq->vring.num - 1);vq->vring.avail->ring[avail] = cpu_to_virtio16(_vq->vdev, head);/* Descriptors and available array need to be set before we expose the* new available array entries. */virtio_wmb(vq->weak_barriers);vq->avail_idx_shadow++;vq->vring.avail->idx = cpu_to_virtio16(_vq->vdev, vq->avail_idx_shadow);vq->num_added++;pr_debug("Added buffer head %i to %p\n", head, vq);END_USE(vq);/* This is very unlikely, but theoretically possible. Kick* just in case. */if (unlikely(vq->num_added == (1 << 16) - 1))virtqueue_kick(_vq);
}
结论
从上面的代码分析中我们已经完成能够确定:
1, vhost后端没有给guest通过中断通知event_idx(avail_idx)值。
2,virtio前端也在根据vring_need_event的公式(return (__u16)(new_idx - event_idx - 1) < (__u16)(new_idx - old);)决定是否向vhost后端kick消息时, 由于event_idx在old的前面,vring_need_event值为False,所以前端就不会给后端发消息。这样前端就会丢包,所以前端也会看到“No buffer space available”之类的错误。通过上面的代码分析,我们也知道了造成这个的原因是因为后端发送的比前端慢。在检查宿主机的syslog后,发现了大量的MTU相关的日志”br-int: dropped over-mtu packet: 1500 > 1458”。OK,问题就在这里了,虚机里需要设置合适的MTU值,如sudo ip link set eth0 mtu 1400