DPDK vhost-user之前後端通知機制場景分析(十)

所謂前後端通知,必然涉及兩個方向:前端通知後端,後端通知前端。而我們知道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函數完成隊列中斷處理函數的初始化,根據設備對中斷的支持,分爲以下三種情況:

  1. 所有txq,rxq以及ctrlq都共享一箇中斷處理;
  2. ctrlq單獨使用一箇中斷處理,其他txq和rxq共享一箇中斷處理;
  3. 可以每個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

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