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中,減少了內存訪問次數。