所謂前後端通知,必然涉及兩個方向:前端通知後端,後端通知前端。而我們知道vhost有txq和rxq,對於每種queue都伴隨有這兩種通知。而通知方式又根據是否支持event_idx有着不同的實現,最後virtio1.1引入的packed ring後,通知相對split ring又有不同。下面我們以txq,rxq的兩個方向共四種情況來分析前後端的通知實現。其中前端以kernel4.9 virtio_net實現爲例分析,後端以dpdk 18.11 vhost_user實現分析。在展開前後端通知分析前,我們先了解兩個背景知識:前端中斷處理函數的註冊和後端vhost_user的kick方式。
前端中斷處理函數註冊
後端對前端的通知,是以中斷方式傳遞到前端的。分析通知的接收處理就少不了要了解這些中斷處理函數。所以我們先看一下前端是怎麼註冊中斷處理函數的。這些需要從virtio_net的加載函數virtnet_probe說起,具體如下圖所示。
我們知道virtio設備分爲morden和lagecy兩種,我們以morden設備爲例。對於morden設備,會調用virtio_pci_modern_probe初始化config ops:
vp_dev->vdev.config = &virtio_pci_config_ops;
其find_vqs函數對應爲vp_modern_find_vqs。vp_modern_find_vqs 其中主要調用vq_find_vqs函數。vp_find_vqs函數完成隊列中斷處理函數的初始化,根據設備對中斷的支持,分爲以下三種情況:
- 所有txq,rxq以及ctrlq都共享一箇中斷處理;
- ctrlq單獨使用一箇中斷處理,其他txq和rxq共享一箇中斷處理;
- 可以每個queue(包含txq,rxq以及ctrlq)各一箇中斷處理;
vp_find_vqs (kernel 4.9)
/* the config->find_vqs() implementation */
int vp_find_vqs(struct virtio_device *vdev, unsigned nvqs,
struct virtqueue *vqs[],
vq_callback_t *callbacks[],
const char * const names[])
{
int err;
/* Try MSI-X with one vector per queue. */
err = vp_try_to_find_vqs(vdev, nvqs, vqs, callbacks, names, true, true);
if (!err)
return 0;
/* Fallback: MSI-X with one vector for config, one shared for queues. */
err = vp_try_to_find_vqs(vdev, nvqs, vqs, callbacks, names,
true, false);
if (!err)
return 0;
/* Finally fall back to regular interrupts. */
return vp_try_to_find_vqs(vdev, nvqs, vqs, callbacks, names,
false, false);
}
函數首先嚐試方式3每個queue各一箇中斷處理,如果失敗再嘗試方式2,如果再失敗就只能使用方式1了。
我們看到每種方式都是調用同一個函數vp_try_to_find_vqs,只是傳入的參數不同。那我們就來看下這個函數的主要內容。
vp_try_to_find_vqs
static int vp_try_to_find_vqs(struct virtio_device *vdev, unsigned nvqs,
struct virtqueue *vqs[],
vq_callback_t *callbacks[],
const char * const names[],
bool use_msix,
bool per_vq_vectors)
{
struct virtio_pci_device *vp_dev = to_vp_device(vdev);
u16 msix_vec;
int i, err, nvectors, allocated_vectors;
vp_dev->vqs = kmalloc(nvqs * sizeof *vp_dev->vqs, GFP_KERNEL);
if (!vp_dev->vqs)
return -ENOMEM;
if (!use_msix) {
/* 方式1,所有txq,rxq以及ctrlq都共享一箇中斷處理 */
/* Old style: one normal interrupt for change and all vqs. */
err = vp_request_intx(vdev);
if (err)
goto error_find;
} else {
if (per_vq_vectors) {
/* Best option: one for change interrupt, one per vq. */
/* 方式3,可以每個queue一箇中斷處理 */
nvectors = 1;
for (i = 0; i < nvqs; ++i)
if (callbacks[i])
++nvectors;
} else {
/* Second best: one for change, shared for all vqs. */
/* 方式2,ctrlq一箇中斷,其他txq和rxq共享一箇中斷處理 */
nvectors = 2;
}
err = vp_request_msix_vectors(vdev, nvectors, per_vq_vectors);
if (err)
goto error_find;
}
vp_dev->per_vq_vectors = per_vq_vectors;
allocated_vectors = vp_dev->msix_used_vectors;
for (i = 0; i < nvqs; ++i) { /* 方式3的處理 */
if (!names[i]) {
vqs[i] = NULL;
continue;
} else if (!callbacks[i] || !vp_dev->msix_enabled)
msix_vec = VIRTIO_MSI_NO_VECTOR;
else if (vp_dev->per_vq_vectors)
msix_vec = allocated_vectors++;
else
msix_vec = VP_MSIX_VQ_VECTOR;
vqs[i] = vp_setup_vq(vdev, i, callbacks[i], names[i], msix_vec);
if (IS_ERR(vqs[i])) {
err = PTR_ERR(vqs[i]);
goto error_find;
}
if (!vp_dev->per_vq_vectors || msix_vec == VIRTIO_MSI_NO_VECTOR)
continue;
/* allocate per-vq irq if available and necessary */
snprintf(vp_dev->msix_names[msix_vec],
sizeof *vp_dev->msix_names,
"%s-%s",
dev_name(&vp_dev->vdev.dev), names[i]);
err = request_irq(vp_dev->msix_entries[msix_vec].vector,
vring_interrupt, 0,
vp_dev->msix_names[msix_vec],
vqs[i]);
if (err) {
vp_del_vq(vqs[i]);
goto error_find;
}
}
return 0;
error_find:
vp_del_vqs(vdev);
return err;
}
其中vp_request_intx函數完成方式1的處理,具體就是通過request_irq註冊中斷處理函數vp_interrupt;vp_request_msix_vectors函數完成方式2的處理,其中調用request_irq給ctrlq註冊中斷處理函數爲vp_config_changed(方式3的ctrlq中斷處理也是這裏註冊),調用request_irq給數據queue(txq,rxq)註冊中斷處理函數爲vp_vring_interrupt;而方式3的剩餘處理在本函數的後半部分,爲每個數據queue調用request_irq註冊中斷處理函數vring_interrupt。如果查看代碼會發現方式2數據queue共享的中斷處理vp_vring_interrupt函數中也是通過遍歷所有queue調用vring_interrupt實現的。所以我們重點關注vring_interrupt函數的實現,這是數據queue中斷處理的核心。
vring_interrupt
irqreturn_t vring_interrupt(int irq, void *_vq)
{
struct vring_virtqueue *vq = to_vvq(_vq);
if (!more_used(vq)) { /* 如果沒有更新uesd desc則不需要特殊處理直接返回 */
pr_debug("virtqueue interrupt with no work for %p\n", vq);
return IRQ_NONE;
}
if (unlikely(vq->broken))
return IRQ_HANDLED;
pr_debug("virtqueue callback for %p (%p)\n", vq, vq->vq.callback);
if (vq->vq.callback)
vq->vq.callback(&vq->vq);
return IRQ_HANDLED;
}
static inline bool more_used(const struct vring_virtqueue *vq)
{
return vq->last_used_idx != virtio16_to_cpu(vq->vq.vdev, vq->vring.used->idx);
}
如果more_used返回false表示vq->last_used_idx== vring.used->idx,這說明當前沒有uesd desc需要更新處理,所以中斷直接返回。否則就調用對應queue的callback函數。而callback函數在之前virtnet_find_vqs的調用中被設置。rxq和txq的callback分別註冊爲了skb_recv_done和skb_xmit_done。
callbacks[rxq2vq(i)] = skb_recv_done;
callbacks[txq2vq(i)] = skb_xmit_done;
所以接收隊列和發送隊列的中斷處理主要就是分別調用skb_recv_done和skb_xmit_done函數。
後端vhost_user的kick方式
下面我們再看下後端vhost_user是如果kick前端的。首先是split ring的情況。
vhost_vring_call_split (dpdk 1811)
static __rte_always_inline void
vhost_vring_call_split(struct virtio_net *dev, struct vhost_virtqueue *vq)
{
/* Flush used->idx update before we read avail->flags. */
rte_smp_mb();
/* Don't kick guest if we don't reach index specified by guest. */
if (dev->features & (1ULL << VIRTIO_RING_F_EVENT_IDX)) {
uint16_t old = vq->signalled_used;
uint16_t new = vq->last_used_idx;
if (vhost_need_event(vhost_used_event(vq), new, old)
&& (vq->callfd >= 0)) {
vq->signalled_used = vq->last_used_idx;
eventfd_write(vq->callfd, (eventfd_t) 1);
}
} else {
/* Kick the guest if necessary. */
if (!(vq->avail->flags & VRING_AVAIL_F_NO_INTERRUPT)
&& (vq->callfd >= 0))
eventfd_write(vq->callfd, (eventfd_t)1);
}
}
split的方式比較簡單,當開啓event_idx的時候,就根據old,new以及前端消耗到的位置avail->ring[(vr)->size]來判斷是否kick;如果沒有開啓event_idx時則只要前端沒有設置VRING_AVAIL_F_NO_INTERRUPT就kick前端,注意不開啓event_idx時,後端也不是無腦kick的,只有前端沒設置VRING_AVAIL_F_NO_INTERRUPT時纔會kick,至於前端什麼時候設置VRING_AVAIL_F_NO_INTERRUPT,一會再分析。
下面我們看packed ring的kick前端處理方式。在介紹packed處理之前我們先看其相關結構。
struct vhost_virtqueue {
union {
struct vring_desc *desc;
struct vring_packed_desc *desc_packed;
};
union {
struct vring_avail *avail;
struct vring_packed_desc_event *driver_event;
};
union {
struct vring_used *used;
struct vring_packed_desc_event *device_event;
};
……
};
在packed 方式中,由於uesd ring和avail ring不再需要,取而代之的是兩個desc_event結構。在split方式中我們知道前後端控制是否相互通知是通過avail->flag和uesd->flag的設置來完成的,但packed中分別是通過driver_event和device_event來完成的。
其中driver_event是後端只讀的,是前端控制後端更新uesd desc時是否發送通知的,對應於split 方式的avail->flag;
device_event是前端只讀的,是後端控制前端更新avail desc時是否發送通知的,對應於split方式的uesd->flag。
driver_event和device_event都是vring_packed_desc_event結構,其具體結構如下。
struct vring_packed_desc_event {
uint16_t off_wrap;
uint16_t flags;
};
其中flag可以取三個值分別爲:
#define VRING_EVENT_F_ENABLE 0x0
#define VRING_EVENT_F_DISABLE 0x1
#define VRING_EVENT_F_DESC 0x2
driver_event取值VRING_EVENT_F_DISABLE相當於split方式avail->flag設置VRING_AVAIL_F_NO_INTERRUPT,device_event取值VRING_EVENT_F_DISABLE相當於uesd->flag設置VRING_USED_F_NO_NOTIFY。
剩下關鍵的就是最後這個VRING_EVENT_F_DESC flag了,官方spec是這麼解釋這個flag的: Enable events for a specific descriptor,(as specified by Descriptor Ring Change Event Offset/Wrap Counter),Only valid if VIRTIO_F_RING_EVENT_IDX has been negotiated. 可以看出這個flag的作用是指定某一個desc發生變化後觸發通知,而這個flag生效的前提就是開啓了event_idx。這個解釋似乎還是不太直觀,其實我們可以對比split 的event_idx處理方式來理解。我們知道split方式中,如果開啓了event_idx,則前端需要通過avail->ring的最後一個desc告訴後端前端的uesd desc處理到哪裏了,後端根據這個值來決定是否需要kick前端;而後端則使用uesd->ring的最後一個desc告訴前端後端的avail desc處理到哪裏了,前端根據這個來決定是否來通知後端。但是在packed方式中沒有了avail ring和uesd ring,那如果開啓了event_idx前端後端如何才能告知對方自己處理到什麼位置了呢?答案就是通過這裏的off_wrap成員,可以看到這個成員是一個uint16_t,其中後15位指定了前後端處理到什麼位置了,而最高位是爲了解決翻轉的Wrap Counter,而整個off_wrap字段有意義的前提就是flag被設置爲了VRING_EVENT_F_DESC。
下面我們對比packed和split方式後端是否開啓通知,來了解下vring_packed_desc_event這三個flag的作用。首先看split的開關中斷處理,即vhost_enable_notify_split :
vhost_enable_notify_split(dpdk 1811)
static inline void
vhost_enable_notify_split(struct virtio_net *dev,
struct vhost_virtqueue *vq, int enable)
{
/* 沒有開啓EVENT_IDX以vq->used->flags爲準 */
if (!(dev->features & (1ULL << VIRTIO_RING_F_EVENT_IDX))) {
if (enable)
vq->used->flags &= ~VRING_USED_F_NO_NOTIFY;
else
vq->used->flags |= VRING_USED_F_NO_NOTIFY;
} else {/* 開啓EVENT_IDX後不再使用used->flags */
if (enable)
vhost_avail_event(vq) = vq->last_avail_idx;
}
}
可以看到在沒有開啓EVENT_IDX時,控制guest是否通知後端是用過控制vq->used->flags設置VRING_USED_F_NO_NOTIFY來實現的,但是如果開啓了EVENT_IDX就不再使用used->flags,而是使用EVENT_IDX特有中斷限速方式。
下面再看packed的guest通知開啓關閉方式實現vhost_enable_notify_packed。
vhost_enable_notify_packed(dpdk 1811)
static inline void
vhost_enable_notify_packed(struct virtio_net *dev,
struct vhost_virtqueue *vq, int enable)
{
uint16_t flags;
if (!enable) {
vq->device_event->flags = VRING_EVENT_F_DISABLE;
return;
}
flags = VRING_EVENT_F_ENABLE;
if (dev->features & (1ULL << VIRTIO_RING_F_EVENT_IDX)) {
flags = VRING_EVENT_F_DESC;
vq->device_event->off_wrap = vq->last_avail_idx |
vq->avail_wrap_counter << 15;
}
rte_smp_wmb();
vq->device_event->flags = flags;
}
當不開啓event_idx時,packed 方式使用device_event->flags是否設置VRING_EVENT_F_DISABLE代替split 方式設置的VRING_USED_F_NO_NOTIFY。如果開啓了event_idx則device_event->flags一定會被設置爲VRING_EVENT_F_DESC的,另外注意device_event->off_wrap的初始設置,低15位被設置爲vq->last_avail_idx,高位被設置爲vq->avail_wrap_counter。
最後我們看一下packed方式下後端是如何決定是否通知前端的,即vhost_vring_call_packed函數的實現。
vhost_vring_call_packed(dpdk 1811)
static __rte_always_inline void
vhost_vring_call_packed(struct virtio_net *dev, struct vhost_virtqueue *vq)
{
uint16_t old, new, off, off_wrap;
bool signalled_used_valid, kick = false;
/* Flush used desc update. */
rte_smp_mb();
/* 如果沒有開啓EVENT_IDX, 則以dev->driver_event->flags是否設置VRING_EVENT_F_DISABLE爲準確定是否通知前端 */
if (!(dev->features & (1ULL << VIRTIO_RING_F_EVENT_IDX))) {
if (vq->driver_event->flags !=
VRING_EVENT_F_DISABLE)
kick = true;
goto kick;
}
/* old 表示上一次通知前端時的used idx,new表示當前的uesd idx */
old = vq->signalled_used;
new = vq->last_used_idx;
vq->signalled_used = new; //注意和split的區別,split是kick前端後才進行signalled_used賦值,而這裏是直接賦值
signalled_used_valid = vq->signalled_used_valid;
vq->signalled_used_valid = true;
/* 如果開啓了event_idx但是driver_event->flags沒有設置VRING_EVENT_F_DESC(正常情況是不存在的)則按照不開啓event_idx時處理,及根據VRING_EVENT_F_DISABLE決定是否kick */
if (vq->driver_event->flags != VRING_EVENT_F_DESC) {
if (vq->driver_event->flags != VRING_EVENT_F_DISABLE)
kick = true;
goto kick;
}
if (unlikely(!signalled_used_valid)) {
kick = true;
goto kick;
}
rte_smp_rmb();
off_wrap = vq->driver_event->off_wrap;
off = off_wrap & ~(1 << 15); /* 從低15位獲取desc的idx */
if (new <= old)
old -= vq->size;
/* 根據最高位的warp counter決定是否翻轉 */
if (vq->used_wrap_counter != off_wrap >> 15)
off -= vq->size;
/* off表示前端當前處理的位置,根據off,new,old決定是否kick前端 */
if (vhost_need_event(off, new, old))
kick = true;
kick:
if (kick)
eventfd_write(vq->callfd, (eventfd_t)1);
}
在不開啓event_idx的時候,和開啓event_idx但是driver_event->flags != VRING_EVENT_F_DESC時(正常情況不存在)都是按照前端是否設置VRING_EVENT_F_DISABLE來決定的。
下面一點和split略有不同,就是vq->signalled_used賦值的位置,split是kick前端後條件滿足時才進行signalled_used賦值,而這裏是直接賦值,並且引入了一個signalled_used_valid變量。首先明確一下,這個變化和packed本身無關,其實split在後續patch也採用了類似處理(詳見:http://mails.dpdk.org/archives/dev/2019-March/126684.html )。其中vq->signalled_used_valid的引入是爲了在熱遷移後以及前端驅動reload後,將vq->signalled_used的值標記爲無效,正常情況vq->signalled_used記錄的是上次通知前端時後端處理到的uesd idx,但是當熱遷移發生或者前端驅動reload後這個值將不再有意義。signalled_used_valid是用在第一次更新used ring、virtio-net driver reload和live migration之後,所以在ring初始化的時候、guest發送VHOST_USER_GET_VRING_BASE的時候才賦值爲false。
另外將vq->signalled_used賦值位置前移,而不是隻有滿足kick條件才賦值,這個改動是爲了一個優化。之前產生interrupt的條件是: last_used_idx – event_idx <= last_used_idx – signalled_used (event_idx爲前端更新到的uesd idx)。以前的實現是產生interrupt的時候纔會更新signalled_used,而現在是每次更新uesd_ring之後,不管是否有interrupt都更新它,現在的做法和kernel vhost_net的實現是一樣的。這樣做的好處是可以減少不必要的kick事件,以vm接收方向爲例,如果guest kernel有NAPI,那麼在guest以poll的方式收包的時候會停止更新event_idx。假設NAPI在Δt時間內都會poll(也就是不更新event_idx),那麼對原來的實現而言,last_used_idx就是一直增長,而event_idx和signalled_used都是不變的;在這種情況下,產生Interrupt只有兩種情況,一是guest NAPI停止poll、更新event_idx,另一種是last_used_idx超過2^16,比如(last_used_idx=13, signaled_used=65535, event_idx=10 )。而對現在的實現而言,last_used_idx也一直增長,event_idx也是固定不變的,不過(last_used_idx – signalled_used)是一個相對固定的值(其值等於這一次更新的used ring的descriptor數,我們稱它爲Δf);因此在這種情況下guest NAPI結束後,更新event_idx,但(last_used_idx – event_idx)的值未必小於等於Δf,所以不一定需要產生interrupt。所以,在guest如果沒有NAPI,現在的實現與以前的相比可以減小(last_used_idx – signaled_used)的值,這樣可以減小(last_used_idx – event_idx)小於等於這個值的的概率,從而減小產生interrupt的概率。
最後一點就是packed方式是如何獲取前端當前的消耗位置的,也就是event_idx的,是通過off_wrap的低15位以及高位的warp counter來完成的。
有了以上背景知識,我們就開始分別分析前後端通知的四種情況。
發送隊列通前端知後端
我們看發送隊列(txq)通知後端的情況。首先明確對於發送隊列,前端通知(kick)後端的作用是什麼?發送隊列kick後端就是爲了告訴後端前端已經將數據放入了共享ring(具體就是avail desc)中,後端可以來取數據了。
所以我們看前端virtio_net驅動的代碼實現,在發送函數start_xmit的結尾有如下調用:
if (kick || netif_xmit_stopped(txq))
virtqueue_kick(sq->vq);
我們來看virtqueue_kick的實現。
virtqueue_kick (kernel4.9 )
bool virtqueue_kick(struct virtqueue *vq)
{
if (virtqueue_kick_prepare(vq))
return virtqueue_notify(vq);
return true;
}
可以看到真正發送kick通知的函數是virtqueue_notify,其調用的條件是virtqueue_kick_prepare,只有其返回true的時候纔會kick後端。我們看下virtqueue_kick_prepare的實現。
virtqueue_kick_prepare (kernel4.9 )
bool virtqueue_kick_prepare(struct virtqueue *_vq)
{
struct vring_virtqueue *vq = to_vvq(_vq);
u16 new, old;
bool needs_kick;
START_USE(vq);
/* We need to expose available array entries before checking avail
* event. */
virtio_mb(vq->weak_barriers);
old = vq->avail_idx_shadow - vq->num_added; /*上次通知時的avail_idx*/
new = vq->avail_idx_shadow; /* 本次發送報文後的avail_idx */
vq->num_added = 0;
if (vq->event) { /* 如果支持event_idx */
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));
}
END_USE(vq);
return needs_kick;
}
可以看到如果支持event_idx就更加old和new以及used->ring[(vr)->num]的範圍來通知後端,詳細過程可以參考之前寫的event_idx相關文章。如果不支持event_idx就看used->flags是否設置了VRING_USED_F_NO_NOTIFY,如果沒有設置就通知後端,否則就不通知。這裏我們也可以看到,當開啓event_idx後,VRING_USED_F_NO_NOTIFY也就失去了作用。此外VRING_USED_F_NO_NOTIFY這個flag是後端設置的,對前端是隻讀的,用來告訴前端是否需要通知後端。
然後我們看後端處理,這裏我們因爲使用的是dpdk,而dpdk一般採用的polling模式,設置VRING_USED_F_NO_NOTIFY,所以前端是不會kick後端的,但是如果開啓了event_idx呢?我們知道開啓event_idx時,前端kick後端需要後端通過used->ring[(vr)->num]告訴前端當前avail desc消耗到什麼位置了,但是當前dpdk vhost_user並沒有這個處理,因此當前如果打開event_idx,後端dpdk依然是無法收到中斷的。
發送隊列後端通知前端
我們再看發送隊列後端通知前端的過程。發送隊列爲什麼要通知前端呢?因爲後端將前端放入avail ring中的數據取出後需要告訴前端對應的數據已經被取走了,你可以把相關數據buffer釋放了。而究竟釋放那些buffer是取決於uesd ring的,所以通知前端本質上是爲了告訴前端uesd ring有更新了。
但是有一點要注意,我們知道uesd ring是前後端共享的,所以如果後端更新了uesd ring,即使不通知前端,前端應該也是可以感知到的。所以前端釋放buffer不一定要依賴後端kick。事實上也的確如此。
我們還看發送start_xmit,
start_xmit (kernel4.9 )
static netdev_tx_t start_xmit(struct sk_buff *skb, struct net_device *dev)
{
struct virtnet_info *vi = netdev_priv(dev);
int qnum = skb_get_queue_mapping(skb);
struct send_queue *sq = &vi->sq[qnum];
int err;
struct netdev_queue *txq = netdev_get_tx_queue(dev, qnum);
bool kick = !skb->xmit_more;
/* Free up any pending old buffers before queueing new ones. */
free_old_xmit_skbs(sq);
…….
}
其中開頭部分就首先調用free_old_xmit_skbs根據uesd ring將之前的buffer釋放掉。具體流程如下圖所示。
其中detach_buf負責更加uesd ring來將對應的desc釋放掉,並將對應地址dma unmmap。另外要注意的是virtqueue_get_buf函數的最後有如下調用:
virtqueue_get_buf (kernel4.9 )
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;
last_used = (vq->last_used_idx & (vq->vring.num - 1));
i = virtio32_to_cpu(_vq->vdev, vq->vring.used->ring[last_used].id);
*len = virtio32_to_cpu(_vq->vdev, vq->vring.used->ring[last_used].len);
……
/* detach_buf clears data, so grab it now. */
ret = vq->desc_state[i].data;
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->avail_flags_shadow & VRING_AVAIL_F_NO_INTERRUPT))
virtio_store_mb(vq->weak_barriers,
&vring_used_event(&vq->vring),
cpu_to_virtio16(_vq->vdev, vq->last_used_idx));
END_USE(vq);
return ret;
}
當前端沒有設置VRING_AVAIL_F_NO_INTERRUPT時,會更新avail->ring[(vr)->num],這也是後端開啓event_idx時kick前端的條件。VRING_AVAIL_F_NO_INTERRUPT是前端設置,後端只讀的。爲什麼要更新avail->ring[(vr)->num]呢?avail->ring[(vr)->num]中記錄的前端已經處理到那個uesd idx了,因爲可以及時告訴後端前端處理到什麼位置了,後端來根據情況決定是否需要kick前端。
那現在問題又回來了,對於txq既然前端不需要後端kick也能釋放buffer,那後端kick有什麼用呢?我們再回頭看一下start_xmit發送函數,有一下邏輯。
start_xmit (kernel4.9 )
static netdev_tx_t start_xmit(struct sk_buff *skb, struct net_device *dev)
{
struct virtnet_info *vi = netdev_priv(dev);
int qnum = skb_get_queue_mapping(skb);
struct send_queue *sq = &vi->sq[qnum];
int err;
struct netdev_queue *txq = netdev_get_tx_queue(dev, qnum);
bool kick = !skb->xmit_more;
……
/* If running out of space, stop queue to avoid getting packets that we
* are then unable to transmit.
* An alternative would be to force queuing layer to requeue the skb by
* returning NETDEV_TX_BUSY. However, NETDEV_TX_BUSY should not be
* returned in a normal path of operation: it means that driver is not
* maintaining the TX queue stop/start state properly, and causes
* the stack to do a non-trivial amount of useless work.
* Since most packets only take 1 or 2 ring slots, stopping the queue
* early means 16 slots are typically wasted.
*/
if (sq->vq->num_free < 2+MAX_SKB_FRAGS) {
netif_stop_subqueue(dev, qnum);
if (unlikely(!virtqueue_enable_cb_delayed(sq->vq))) {
/* More just got used, free them then recheck. */
free_old_xmit_skbs(sq);
if (sq->vq->num_free >= 2+MAX_SKB_FRAGS) {
netif_start_subqueue(dev, qnum);
virtqueue_disable_cb(sq->vq);
}
}
}
……
}
當前端發送速率過快,從而vq->num_free較少時會調用netif_stop_subqueue(將隊列狀態設置爲__QUEUE_STATE_DRV_XOFF),這樣隊列的start_xmit函數下次在__dev_queue_xmit中就不會被調用。要想打破這樣一個狀態,就需要後端的kick了。這裏還有一個十分關鍵的函數,就這在stop_queue之後調用的virtqueue_enable_cb_delayed函數。這個函數中有着至關重要的一個操作,如下:
virtqueue_enable_cb_delayed
bool virtqueue_enable_cb_delayed(struct virtqueue *_vq)
{
struct vring_virtqueue *vq = to_vvq(_vq);
u16 bufs;
START_USE(vq);
/* We optimistically turn back on interrupts, then check if there was
* more to do. */
/* Depending on the VIRTIO_RING_F_USED_EVENT_IDX feature, we need to
* either clear the flags bit or point the event index at the next
* entry. Always update the event index to keep code simple. */
/* 取消設置VRING_AVAIL_F_NO_INTERRUPT,使後端可以發送中斷上來 */
if (vq->avail_flags_shadow & VRING_AVAIL_F_NO_INTERRUPT) {
vq->avail_flags_shadow &= ~VRING_AVAIL_F_NO_INTERRUPT;
if (!vq->event)
vq->vring.avail->flags = cpu_to_virtio16(_vq->vdev, vq->avail_flags_shadow);
}
/* TODO: tune this threshold */
bufs = (u16)(vq->avail_idx_shadow - vq->last_used_idx) * 3 / 4;
/* 更新avail->ring[(vr)->num]以供event_idx使用 */
virtio_store_mb(vq->weak_barriers,
&vring_used_event(&vq->vring),
cpu_to_virtio16(_vq->vdev, vq->last_used_idx + bufs));
if (unlikely((u16)(virtio16_to_cpu(_vq->vdev, vq->vring.used->idx) - vq->last_used_idx) > bufs)) {
END_USE(vq);
return false;
}
END_USE(vq);
return true;
}
首先這個函數做了一個關鍵操作,就是取消設置VRING_AVAIL_F_NO_INTERRUPT,使後端可以發送中斷上來。那麼VRING_AVAIL_F_NO_INTERRUPT這個flag是什麼時候被置上的呢?這個我們後面回答。另外一點就是會更新更新avail->ring[(vr)->num]告訴前端uesd desc當前消耗到哪裏了,但注意這裏不是更新爲vq->last_used_idx,而是還加了一個bufs,爲的就是讓後端延時一會再kick前端。
下面我們看一下dpdk後端kick前端的時機,我們以split方式爲例說明。
virtio_dev_tx_split(dpdk18.11)
static __rte_always_inline uint16_t
virtio_dev_tx_split(struct virtio_net *dev, struct vhost_virtqueue *vq,
struct rte_mempool *mbuf_pool, struct rte_mbuf **pkts, uint16_t count)
{
uint16_t i;
uint16_t free_entries;
if (unlikely(dev->dequeue_zero_copy)) {
struct zcopy_mbuf *zmbuf, *next;
for (zmbuf = TAILQ_FIRST(&vq->zmbuf_list);
zmbuf != NULL; zmbuf = next) {
next = TAILQ_NEXT(zmbuf, next);
if (mbuf_is_consumed(zmbuf->mbuf)) {
update_shadow_used_ring_split(vq,
zmbuf->desc_idx, 0);
TAILQ_REMOVE(&vq->zmbuf_list, zmbuf, next);
restore_mbuf(zmbuf->mbuf);
rte_pktmbuf_free(zmbuf->mbuf);
put_zmbuf(zmbuf);
vq->nr_zmbuf -= 1;
}
}
if (likely(vq->shadow_used_idx)) {
/*如果是零拷貝方式,則每次接收前檢查之前已經dma完成的報文,更新uesd ring,kick前端*/
flush_shadow_used_ring_split(dev, vq);
vhost_vring_call_split(dev, vq);
}
}
......
for (i = 0; i < count; i++) {
......
err = copy_desc_to_mbuf(dev, vq, buf_vec, nr_vec, pkts[i],
mbuf_pool);
......
}
vq->last_avail_idx += i;
if (likely(dev->dequeue_zero_copy == 0)) {
do_data_copy_dequeue(vq);
if (unlikely(i < count))
vq->shadow_used_idx = i;
if (likely(vq->shadow_used_idx)) {
/* 更新used ring,kick前端 */
flush_shadow_used_ring_split(dev, vq);
vhost_vring_call_split(dev, vq);
}
}
return i;
}
如果是零拷貝,則在後端接受邏輯的開始,判斷上一次接受的報文是否dma完成,如果完成則更新uesd ring,調用vhost_vring_call_split kick前端。如果不是零拷貝模式,則在將報文從desc拷貝出來後,更新uesd ring,調用vhost_vring_call_split kick前端。關於vhost_vring_call_split的實現在前面已經介紹過了,這裏不再重複。
下面看前端收到後端的中斷是如何處理的。前面已經介紹過了,對於發送隊列,其註冊的中斷回調函數中會調用skb_xmit_done。
skb_xmit_done(kernel 4.9)
static void skb_xmit_done(struct virtqueue *vq)
{
struct virtnet_info *vi = vq->vdev->priv;
/* Suppress further interrupts. */
virtqueue_disable_cb(vq);
/* We were probably waiting for more output buffers. */
netif_wake_subqueue(vi->dev, vq2txq(vq));
}
其中virtqueue_disable_cb會將vring.avail->flags設置上VRING_AVAIL_F_NO_INTERRUPT,這樣後端就不會發生kick到前端了(不開啓event_idx的情況),然後調用netif_wake_subqueue喚醒被stop的queue(清除__QUEUE_STATE_DRV_XOFF 的state)。
virtqueue_disable_cb(kernel)
void virtqueue_disable_cb(struct virtqueue *_vq)
{
struct vring_virtqueue *vq = to_vvq(_vq);
if (!(vq->avail_flags_shadow & VRING_AVAIL_F_NO_INTERRUPT)) {
vq->avail_flags_shadow |= VRING_AVAIL_F_NO_INTERRUPT;
if (!vq->event)
vq->vring.avail->flags = cpu_to_virtio16(_vq->vdev, vq->avail_flags_shadow);
}
}
這裏又有個疑問,喚醒stop 狀態的queue容易理解,可以設置上了VRING_AVAIL_F_NO_INTERRUPT後,什麼時候取消呢?其實前面我們已經介紹過了,就是下次stop queue時調用virtqueue_enable_cb_delayed的處理中。所以我們可以看到,即使不開啓event_idx,也不是每次uesd 變化都會kick前端的,而是隻有queue stop後纔會kick,正常狀態前端是會設置VRING_AVAIL_F_NO_INTERRUPT不讓後端kick的。
接收隊列後端通知前端
對於接收隊列,後端會在將mbuf數據拷貝到avail desc中後,更新uesd ring,然後kick前端。kick前端的目的就是告訴前端,我有數據發送給你了(更新了uesd ring),你可以來取數據了。我們以split方式的接收方向爲例。其接受邏輯在virtio_dev_rx_split中實現。
virtio_dev_rx_split(dpdk 18.11)
static __rte_always_inline uint32_t
virtio_dev_rx_split(struct virtio_net *dev, struct vhost_virtqueue *vq,
struct rte_mbuf **pkts, uint32_t count)
{
uint32_t pkt_idx = 0;
uint16_t num_buffers;
struct buf_vector buf_vec[BUF_VECTOR_MAX];
uint16_t avail_head;
rte_prefetch0(&vq->avail->ring[vq->last_avail_idx & (vq->size - 1)]);
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;
uint16_t nr_vec = 0;
/* 爲拷貝當前mbuf後續預留avail desc */
if (unlikely(reserve_avail_buf_split(dev, vq,
pkt_len, buf_vec, &num_buffers,
avail_head, &nr_vec) < 0)) {
VHOST_LOG_DEBUG(VHOST_DATA,
"(%d) failed to get enough desc from vring\n",
dev->vid);
vq->shadow_used_idx -= num_buffers;
break;
}
rte_prefetch0((void *)(uintptr_t)buf_vec[0].buf_addr);
VHOST_LOG_DEBUG(VHOST_DATA, "(%d) current index %d | end index %d\n",
dev->vid, vq->last_avail_idx,
vq->last_avail_idx + num_buffers);
/* 拷貝mbuf到avail desc */
if (copy_mbuf_to_desc(dev, vq, pkts[pkt_idx],
buf_vec, nr_vec,
num_buffers) < 0) {
vq->shadow_used_idx -= num_buffers;
break;
}
/* 更新last_avail_idx */
vq->last_avail_idx += num_buffers;
}
/* 小包的批處理拷貝 */
do_data_copy_enqueue(dev, vq);
if (likely(vq->shadow_used_idx)) {
flush_shadow_used_ring_split(dev, vq); /* 更新used ring */
vhost_vring_call_split(dev, vq); /* 通知前端 */
}
return pkt_idx;
}
其中kick前端的處理在最後的vhost_vring_call_split函數中,這個我們在“後端vhost_user的kick方式”中已經介紹過,這裏就不再重複了。
然後我們看前端的通知處理。根據前面的介紹,接受隊列前端註冊的中斷處理函數最終會調用到skb_recv_done。
skb_recv_done(kernel 4.9)
static void skb_recv_done(struct virtqueue *rvq)
{
struct virtnet_info *vi = rvq->vdev->priv;
struct receive_queue *rq = &vi->rq[vq2rxq(rvq)];
/* Schedule NAPI, Suppress further interrupts if successful. */
if (napi_schedule_prep(&rq->napi)) {
virtqueue_disable_cb(rvq);
__napi_schedule(&rq->napi);
}
}
我們看到這個函數主要工作就是調用virtqueue_disable_cb給vring.avail->flags設置上VRING_AVAIL_F_NO_INTERRUPT從而禁止後端發送中斷(不開啓event_idx的情況),然後喚起NAPI處理。所以在NAPI的情況後端通知是被關閉的。那麼這個flag什麼時候會被打開呢?答案就是在virtio_net的NAPI處理邏輯中,即virtnet_poll函數。
virtnet_poll(kernel 4.9)
static int virtnet_poll(struct napi_struct *napi, int budget)
{
struct receive_queue *rq =
container_of(napi, struct receive_queue, napi);
unsigned int r, received;
received = virtnet_receive(rq, budget);
/* Out of packets? */
if (received < budget) {
r = virtqueue_enable_cb_prepare(rq->vq);
napi_complete_done(napi, received);
if (unlikely(virtqueue_poll(rq->vq, r)) &&
napi_schedule_prep(napi)) {
virtqueue_disable_cb(rq->vq);
__napi_schedule(napi);
}
}
return received;
}
在NAPI處理流程中如果received < budget,證明本輪數據接收已經比較少了,NAPI過程可能要退出了,這時調用virtqueue_enable_cb_prepare將之前的VRING_AVAIL_F_NO_INTERRUPT取消,從NAPI模式進入中斷模式。
virtqueue_enable_cb_prepare(kernel 4.9)
unsigned virtqueue_enable_cb_prepare(struct virtqueue *_vq)
{
struct vring_virtqueue *vq = to_vvq(_vq);
u16 last_used_idx;
START_USE(vq);
/* We optimistically turn back on interrupts, then check if there was
* more to do. */
/* Depending on the VIRTIO_RING_F_EVENT_IDX feature, we need to
* either clear the flags bit or point the event index at the next
* entry. Always do both to keep code simple. */
if (vq->avail_flags_shadow & VRING_AVAIL_F_NO_INTERRUPT) {
vq->avail_flags_shadow &= ~VRING_AVAIL_F_NO_INTERRUPT;
if (!vq->event)
vq->vring.avail->flags = cpu_to_virtio16(_vq->vdev, vq->avail_flags_shadow);
}
vring_used_event(&vq->vring) = cpu_to_virtio16(_vq->vdev, last_used_idx = vq->last_used_idx);
END_USE(vq);
return last_used_idx;
}
注意這個函數除了取消VRING_AVAIL_F_NO_INTERRUPT設置之外,還會更新avail->ring[(vr)->num],以供開啓event_idx時後端使用。
接收隊列前端通知後端
下面看接收方向前端通知的過程。首先要清除接收方向前端通知後端的目的,那就是告訴後端avail ring已經更新了(有了更多空buffer),你可以繼續放入更多數據了。從這裏我們也可以看出,前端通知後端,無論發送還是接收方向,都是告訴後端有了更多的avail desc,而後端通知前端,都是告訴前端有了更多的uesd 的desc。然後我們看前端通知後端的時機,要想知道前端再何時通知後端,我們需要對前端的數據接收流程有個清晰的認識。下面這個圖描述了前端vhost_net接收數據的過程。
而通知後端的時機就在try_fill_recv函數調用中。
try_fill_recv(kernel 4.9)
static bool try_fill_recv(struct virtnet_info *vi, struct receive_queue *rq,
gfp_t gfp)
{
int err;
bool oom;
gfp |= __GFP_COLD;
/* 針對三種情況分別給desc注入對應的buffer,並更新avail idx */
do {
if (vi->mergeable_rx_bufs)
err = add_recvbuf_mergeable(rq, gfp);
else if (vi->big_packets)
err = add_recvbuf_big(vi, rq, gfp);
else
err = add_recvbuf_small(vi, rq, gfp);
oom = err == -ENOMEM;
if (err)
break;
} while (rq->vq->num_free);
virtqueue_kick(rq->vq); /* kick 後端 */
return !oom;
}
這個函數首先會根據是否支持mergeable已經是否支持收大包來向avail desc注入對應的空buffer。每種情況下注入buffer產生的desc chain長度是不同的。這裏不是重點,我們就不展開了。下面看關鍵的virtqueue_kick函數。
virtqueue_kick(kernel 4.9)
bool virtqueue_kick(struct virtqueue *vq)
{
if (virtqueue_kick_prepare(vq))
return virtqueue_notify(vq);
return true;
}
其中真正向後端發送通知的是virtqueue_notify,但是首先要通過virtqueue_kick_prepare的判斷。virtqueue_kick_prepare這個函數我們之前已經介紹過了,如果不開啓event_idx時,會根據vring.used->flags是否設置VRING_USED_F_NO_NOTIFY來決定是否kick後端,而開啓event_idx時,則會根據後端填入(vr)->used->ring[(vr)->num]中的後端消耗位置來決定是否kick。
同樣如果後端使用的是dpdk vhost_user,那麼當前後端是不會寫(vr)->used->ring[(vr)->num]告訴前端自己的avail使用位置的,如果開啓了event_idx後端還是無法收到中斷的。那如果dpdk使用中斷模式,這裏收不到中斷是否會有問題呢?我們換位思考一下,對比一下“發送隊列後端通知前端”的場景,在“發送隊列後端通知前端”中,如果後端不通知前端,那麼前端一旦感覺到沒有可用的avail desc後就會stop queue,之後就無法被喚醒了。而這裏的場景,vhost_user後端沒有類似stop queue的操作,所以即使收不到前端的中斷也沒有問題。但是在“發送隊列前端通知後端”的場景中,如果dpdk採用中斷模式切開啓了event_idx,那麼vhost_user就會因爲收不到中斷而無法取出前端發送的報文。所以對應想使用中斷模式,且開啓event_idx的場景,vhost_user需要添加對(vr)->used->ring[(vr)->num]的處理。
原文鏈接:http://m.blog.chinaunix.net/uid-28541347-id-5819699.html