DPDK-22.11.2 [四] Virtio_user as Exception Path

2023-10-11 18:00:55

因為dpdk是把網路卡操作全部拿到使用者層,與原生系統驅動不再相容,所以被dpdk接管的網路卡從系統層面(ip a/ifconfig)無法看到,同樣資料也不再經過系統核心。

如果想把資料再傳送到系統,就要用到virtio user。這種把資料從dpdk再傳送到核心的步驟,就叫做exception path。

有關virtio user,又有一系列的相關知識,這裡系統的介紹一下。

hypervisor

hypervisor是一個軟體,用來建立執行虛擬機器器(virtual machines/VMs)。hypervisor又叫做虛擬機器器監視器(virtual machine monitor/VMM)。執行hypervisor的機器叫做宿主機(host machine),在執行在hypervisor上的虛擬機器器叫做訪客機(guest machine)。

hypervisor有兩種型別,一種是直接執行在硬體上(Type 1-native or bare-metal hypervisors),hypervisor相當於作業系統;另一種是hypervisor執行在作業系統上(Type 2-hosted hypervisors)。

常見的hypervisor

hypervisor只是一種解決思路,目的就是為了更大化利用硬體資源。比如有一臺計算機,沒有虛擬化之前,只能給一個使用者使用,然而這個使用者不可能24小時線上,空閒時間,系統資源就浪費了。有了虛擬化,就可以把計算機虛擬出多個作業系統,給多個使用者使用,更大化的利用系統資源。並且可以根據使用者的重要性(付費情況)控制硬體資源的使用佔比和優先順序。現在的雲就是虛擬化的進一步延伸。

VMware hypervisors

VMware hypervisors有兩類產品,一種是Type 1,直接執行在硬體上:

  • ESXi hypervisor/VMware ESXi (Elastic Sky X Integrated)
  • VSphere hypervisor

另一種是Type 2,執行在作業系統上:

  • VMware Fusion
  • Workstation
  • VirtualBox

Hyper-V hypervisor

Hyper-V hypervisor是微軟的產品,用在Windows上,是Type 1型別的,直接執行在硬體上。

Citrix hypervisors

XenServer是Citrix Hypervisor比較有名的產品,是Type 1型別,並且XenServer衍生出了Xen open source project。

Open source hypervisors

主要有KVM和Xen

Hypervisor KVM

Linux直接把kernel-based virtual machine (KVM)加到了系統中,並且對QEMU進行了補充。

Red Hat hypervisor

Red Hat hypervisor是基於KVM hypervisor開發的,同樣可以在很多其他Linux版本執行,比如Ubuntu。

虛擬化型別

全虛擬化

由虛擬程式提供全部的虛擬化指令,比如我們用的virtualbox/vmware workstation等桌面虛擬機器器。好處就是與硬體完全隔離,遷移方便,壞處就是犧牲了效能。

硬體虛擬化

由於全虛擬化效能受到影響,所以又提出了硬體虛擬化,由硬體提供虛擬化方案,虛擬機器器直接存取硬體,雖然效能得到了提升,但是也產生了弊端:不方便遷移,必須依賴特定硬體,硬體提供的功能不完善,很多操作無法執行。

半虛擬化

為了解決上面的兩個問題,又提出了半虛擬化,就是消耗效能的操作交給硬體(比如特定的解碼器)或者作業系統,而其他的操作還是在虛擬機器器中完成。半虛擬化中使用最廣泛的標準就是VirtIO。

VirtIO相當於是半虛擬化(paravirtualized hypervisor)的抽象層,有前端和後端,定義了一系列介面用於中間通訊。後端相當於硬體或者作業系統層,具體實現可以不同,只要給定相應的介面操作即可;前端通過呼叫這些介面達到作業系統資源的目的。

這樣的話,前端就可以放到虛擬機器器中,當需要更高效能操作時,通過前端存取後端資源,後端獲得資料後傳送到前端。

VirtIO Offload 就是通過VirtIO協定把操作解除安裝到硬體或者作業系統,也就是把一些消耗效能的操作從虛擬機器器中釋放出來,由硬體或者作業系統實現,最後把結果返回虛擬機器器(比如網路流量處理)。

Deep dive into Virtio-networking

基礎知識

網路

NIC (Network Interface Card) - 網路卡,就是專門用來offload(解除安裝)CPU工作的,把一些網路處理交由網路卡進行操作。

tun/tap - virtual point-to-point network devices that the userspace applications can use to exchange packets. The device is called a tap device when the data exchanged is layer 2 (ethernet frames), and a tun device if the data exchanged is layer 3 (IP packets).
When the tun kernel module is loaded it creates a special device /dev/net/tun. A process can create a tap device opening it and sending special ioctl commands to it. The new tap device has a name in the /dev filesystem and another process can open it, send and receive Ethernet frames.

IPC Inter-Process Communication

socket、eventfd和共用記憶體都是IPC的方式

實現方案

virtio-net/Networking with virtio: qemu implementation 基於QEMU的實現

從圖上可以看到,qemu中處於guest kernel層的virtio net與qemu的virtio net通訊,qemu的virtio net最後與系統kernel層的tap通訊。中間經歷了多次user space和kernel space的切換,並且使用的是系統預設的驅動,還有大量的中斷處理,所以效能不高。

Vhost protocol

由於上面方案的侷限性,vhost提出了改進,就是把消耗效能的模組,offload到另一個模組執行。換句話說,虛擬機器器不適合做的工作,就交給其他模組做,通過一些通訊手段互動資料即可。

Vhost-net

Vhost-net就是對vhost協定的一種實現。這個功能已經整合到linux核心中。如果相關的核心模組載入後,可以在系統路徑下看到/dev/vhost-net目錄。

從這張圖上我們可以看到,原來通訊流程是qemu guest kernel中的virtio-net->qemu virtio-net->host kernel中的tap。現在中間少了一步,通過IPC(Inner-process communication)直接到host kernel的vhost-net,提高了效能。

vhost-user

上面的方案是通過共用記憶體的方式,對映到核心,但是還是有上下文切換。vhost-user把操作完全放到使用者層,使用socket的方式與核心通訊,沒有了上下文切換,也降低了開發難度。

上面這種圖可以看到,操作都被移動到使用者層,使用DPDK避免了上下文切換和中斷,大大提高了效能。

virtio-user

按照官方檔案所述,virtio-user是與vhost-user一起引入的。vhost-user作為後端,virtio-user作為前端。virtio-user除了可以用在容器,與vhost-user一起使用,還可以與vhost-kernel使用,把封包傳送回作業系統。

硬體加速

HW vDPA(Hardware vhost Data Path Acceleration)是SR-IOV VF Passthrough的一種實現。

最快的肯定是直接使用硬體作為後端,把操作直接交給硬體。但是基於硬體的侷限性比較大,功能也不如其他方式豐富,並且成本昂貴,所以除非在對效能要求非常高的場合,一般不會直接使用專有硬體作為後端。

Exception Path的方案介紹

TAP/TUN方案

這個是最早的方案,通過系統的TAP/TUN進行通訊,呼叫的系統標準的api,缺點就是上下文切換和中斷影響了效能。

KNI Kernel NIC Interface


KNI比TAP/TUN的好處就是減少了資料拷貝,可以支援linux系統管理工具(ethtool等)。

但是缺點就是,已經過時了,不安全,功能不全。

virtio user

virtio user用來代替kni,其優點是:

  • 被linux加入核心,不需要額外維護
  • 功能更完善
  • 效能更高

如下圖是virtio user的基本流程示意圖

使用Testpmd測試virtio-user

build/app/dpdk-testpmd -l 12-15 -a 0000:84:00.0 \
    --vdev=virtio_user0,path=/dev/vhost-net,queues=1,queue_size=1024 -- --numa

-l 12-15 表示使用cpu core12到15
-a 0000:84:00.0 表示使用指定的網口,該網口必須有流量進來。
--vdev=virtio_user0,path=/dev/vhost-net,queues=1,queue_size=1024 表示建立一個虛擬裝置,裝置名是virtio_user0,路徑是/dev/host-net(這樣就可以把資料傳送給系統了),queues=1表示通訊佇列有1個,queue_size=1024表示佇列大小是1024。

啟動後,通過ip a,可以看到多了一個tap0的裝置。上面指定的virtio_user0表示是使用的時候的名稱,至於系統顯示的名稱沒有指定,就會預設為tapx。

ip a
...
69: tap0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 1a:e0:f5:1f:21:5f brd ff:ff:ff:ff:ff:ff

裝置建立出來後是down狀態,需要up起來。官方範例指定了ip,實際上如果只是檢視是否有接收資料,可以不用指定ip。

ip link set dev tap0 up

在通過ifconfig檢視詳細資訊

ifconfig tap0
tap0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet6 fe80::18e0:f5ff:fe1f:215f  prefixlen 64  scopeid 0x20<link>
        ether 1a:e0:f5:1f:21:5f  txqueuelen 1000  (Ethernet)
        RX packets 1175788  bytes 947947134 (904.0 MiB)
        RX errors 0  dropped 1  overruns 0  frame 0
        TX packets 6  bytes 516 (516.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

可以看到有資料傳遞進來。

如果有多個網口可以指定多個,這樣就會有兩個虛擬裝置tap0和tap1。

build/app/dpdk-testpmd -l 12-15 -a 0000:84:00.0,0000:84:00.1 \
    --vdev=virtio_user0,path=/dev/vhost-net --vdev=virtio_user1,path=/dev/vhost-net -- --numa

另起一個程序,指定tap0為接收裝置,就可以接收到資料。

build/app/dpdk-testpmd -l 2-5 --vdev=net_af_packet0,iface=tap0 --in-memory --no-pci

使用basicfwd修改一個手動建立虛擬裝置的範例

#include <stdint.h>
#include <stdlib.h>
#include <inttypes.h>
#include <rte_eal.h>
#include <rte_ethdev.h>
#include <rte_cycles.h>
#include <rte_lcore.h>
#include <rte_mbuf.h>
#include <rte_config.h>
#include <rte_ethdev.h>
#include <unistd.h>
#define RX_RING_SIZE 1024
#define TX_RING_SIZE 1024

#define NUM_MBUFS 8191
#define MBUF_CACHE_SIZE 250
#define BURST_SIZE 32
uint16_t virport[64];
int virportnum = 0;
struct lcore_conf
{
    unsigned n_rx_port;
    unsigned rx_port_list[16];
    int pkts;
} __rte_cache_aligned;

static struct lcore_conf lcore_conf_info[RTE_MAX_LCORE];

static inline int port_init(uint16_t port, struct rte_mempool *mbuf_pool)
{
    uint16_t portid = port;
    struct rte_eth_conf port_conf;
    uint16_t nb_rxd = RX_RING_SIZE;
    uint16_t nb_txd = TX_RING_SIZE;
    int retval;
    uint16_t q;
    struct rte_eth_dev_info dev_info;
    int istx=0;

    if (!rte_eth_dev_is_valid_port(port))
        return -1;
    // 需要判斷是否是虛擬網路卡
    // 因為動態建立的網路卡也會遍歷進來,需要額外處理
    for (int i = 0; i < virportnum; i++)
    {
        if (port == virport[i])
        {
            istx=1;
            break;
        }
    }
    uint16_t rx_rings = 0, tx_rings = 0;
    if (istx == 1)
    {
        tx_rings = 1;
    }
    else
    {
        rx_rings = 1;
    }

    memset(&port_conf, 0, sizeof(struct rte_eth_conf));

    retval = rte_eth_dev_info_get(port, &dev_info);
    if (retval != 0)
    {
        printf("Error during getting device (port %u) info: %s\n",
               port, strerror(-retval));
        return retval;
    }

    if (dev_info.tx_offload_capa & RTE_ETH_TX_OFFLOAD_MBUF_FAST_FREE)
        port_conf.txmode.offloads |=
                RTE_ETH_TX_OFFLOAD_MBUF_FAST_FREE;

    retval = rte_eth_dev_configure(port, rx_rings, tx_rings, &port_conf);
    if (retval != 0)
        return retval;

    retval = rte_eth_dev_adjust_nb_rx_tx_desc(port, &nb_rxd, &nb_txd);
    if (retval != 0)
        return retval;
    // 建立的虛擬裝置與物理裝置沒有區別,都需要初始化
    // 如果是物理裝置,就是接收資料;如果是虛擬裝置,就是傳送資料
    if (istx == 0)
    {
        for (q = 0; q < rx_rings; q++)
        {
            retval = rte_eth_rx_queue_setup(port, q, nb_rxd, rte_eth_dev_socket_id(port), NULL, mbuf_pool);
            if (retval < 0)
                return retval;
            retval = rte_eth_dev_set_ptypes(port, RTE_PTYPE_UNKNOWN, NULL, 0);
            if (retval < 0)
            {
                    printf("Port %u, Failed to disable Ptype parsing\n", port);
                    return retval;
            }
        }
    }
    else
    {
        for (q = 0; q < tx_rings; q++)
        {
            retval = rte_eth_tx_queue_setup(port, q, nb_txd, rte_eth_dev_socket_id(port), NULL);
            if (retval < 0)
                return retval;
        }

    }

    retval = rte_eth_dev_start(port);
    if (retval < 0)
        return retval;

    char portname[32];
    char portargs[256];

    struct rte_ether_addr addr;
    retval = rte_eth_macaddr_get(port, &addr);
    if (retval != 0)
        return retval;

    printf("Port %u MAC: %02" PRIx8 " %02" PRIx8 " %02" PRIx8 " %02" PRIx8 " %02" PRIx8 " %02" PRIx8 "\n", port, RTE_ETHER_ADDR_BYTES(&addr));

    // 如果是物理裝置,就建立一個對應的虛擬裝置
    if(istx==0)
    {
        snprintf(portname, sizeof(portname), "virtio_user%u", port);
        // 修改一下mac,避免與物理裝置一致
        addr.addr_bytes[5]=1;
        // 建立虛擬裝置引數,指定路徑,裝置名稱,mac地址等
        snprintf(portargs, sizeof(portargs), "path=/dev/vhost-net,queues=1,queue_size=%u,iface=%s,mac=" RTE_ETHER_ADDR_PRT_FMT, RX_RING_SIZE, portname, RTE_ETHER_ADDR_BYTES(&addr));
        
        // 把裝置加入到系統
        if (rte_eal_hotplug_add("vdev", portname, portargs) < 0)
            rte_exit(EXIT_FAILURE, "Cannot create paired port for port %u\n", port);

        uint16_t virportid = -1;
        // 通過裝置名稱獲取裝置id
        if (rte_eth_dev_get_port_by_name(portname, &virportid) != 0)
        {
            rte_eal_hotplug_remove("vdev", portname);
                rte_exit(EXIT_FAILURE, "cannot find added vdev %s:%s:%d\n", portname, __func__, __LINE__);
        }
        // 記錄下虛擬裝置id
        virport[virportnum] = virportid;
        virportnum++;
    }
    
    // 虛擬裝置不可以開啟混雜模式
    if(istx==0)
    {
        retval = rte_eth_promiscuous_enable(port);
        if (retval != 0)
            return retval;
        for (int i = 0; i < RTE_MAX_LCORE; i++)
        {
            if (rte_lcore_is_enabled(i) == 0)
            {
                continue;
            }

            if (i == rte_get_main_lcore())
            {
                continue;
            }

            if (lcore_conf_info[i].n_rx_port > 0)
            {
                continue;
            }

            struct lcore_conf *qconf = &lcore_conf_info[i];
            qconf->rx_port_list[qconf->n_rx_port] = port;
            qconf->n_rx_port++;
            break;
        }
    }

    return 0;
}

static int lcore_main(void *param)
{
    int ret;
    int lcore_id = rte_lcore_id();
    struct lcore_conf *qconf = &lcore_conf_info[lcore_id];

    int master_coreid = rte_get_main_lcore();
    uint16_t port;
    if (qconf->n_rx_port == 0)
    {
        printf("lcore %u has nothing to do\n", lcore_id);
        return 0;
    }

    if (lcore_id == rte_get_main_lcore())
    {
        printf("do not receive data in main core\n");
        return 0;
    }

    RTE_ETH_FOREACH_DEV(port)
    if (rte_eth_dev_socket_id(port) >= 0 &&
        rte_eth_dev_socket_id(port) !=
        (int) rte_socket_id())
        printf("WARNING, port %u is on remote NUMA node to "
               "polling thread.\n\tPerformance will "
               "not be optimal.\n", port);

    printf("\nCore %u forwarding packets. [Ctrl+C to quit]\n", rte_lcore_id());
    uint16_t portid;
    for (;;)
    {
        for (int i = 0; i < qconf->n_rx_port; i++)
        {
            int port = qconf->rx_port_list[i];
            portid = port;
            struct rte_mbuf *bufs[BURST_SIZE];
            uint16_t nb_rx = rte_eth_rx_burst(port, 0, bufs, BURST_SIZE);

            if (unlikely(nb_rx == 0))
                continue;
            uint16_t nb_tx = 0;
            for (int i = 0; i < virportnum; i++)
            {
                // 找一個虛擬網路卡傳送出去
                // 這裡只有一個裝置,可以這樣
                // 如果有多個,需要設定好一一對應關係再傳送
                nb_tx = rte_eth_tx_burst(virport[i], 0, bufs, nb_rx);
                break;
            }

            for (int j = nb_tx; j < nb_rx; j++)
            {
                // 資料傳送完後,會自動釋放,沒有傳送的資料,需要手動釋放
                rte_pktmbuf_free(bufs[j]);
            }
        }
    }

    return 0;
}

int main(int argc, char *argv[])
{
    struct rte_mempool *mbuf_pool;
    unsigned nb_ports;
    uint16_t portid;
    memset(lcore_conf_info, 0, sizeof(lcore_conf_info));
    memset(virport, -1, sizeof(virport));

    int ret = rte_eal_init(argc, argv);
    if (ret < 0)
        rte_exit(EXIT_FAILURE, "Error with EAL initialization\n");

    nb_ports = rte_eth_dev_count_avail();
    mbuf_pool = rte_pktmbuf_pool_create("MBUF_POOL", NUM_MBUFS * nb_ports, MBUF_CACHE_SIZE, 0, RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id());

    if (mbuf_pool == NULL)
        rte_exit(EXIT_FAILURE, "Cannot create mbuf pool\n");

    // 這裡遍歷需要注意,遍歷期間動態建立的虛擬裝置也會被遍歷到
    RTE_ETH_FOREACH_DEV(portid)
    if (port_init(portid, mbuf_pool) != 0)
        rte_exit(EXIT_FAILURE, "Cannot init port %" PRIu16 "\n", portid);

    rte_eal_mp_remote_launch(lcore_main, NULL, SKIP_MAIN);
    int lcore_id;
    RTE_LCORE_FOREACH_WORKER(lcore_id)
    {
        if (rte_eal_wait_lcore(lcore_id) < 0)
        {
            ret = -1;
            break;
        }
    }

    rte_eal_cleanup();

    return 0;
}

編譯執行,通過ip a檢視

ip a
...
70: virtio_user0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 1a:e0:f5:1f:21:01 brd ff:ff:ff:ff:ff:ff

可以看到該裝置,因為指定了名稱,則不再是tap0,而是我們指定的virtio_user0。mac地址也是我們指定的。

開啟裝置,再次檢視資訊

ip link set dev virtio_user0 up

ip a
70: virtio_user0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UNKNOWN group default qlen 1000
    link/ether 1a:e0:f5:1f:21:01 brd ff:ff:ff:ff:ff:ff
    inet6 fe80::92e2:baff:fe85:3d01/64 scope link tentative 
       valid_lft forever preferred_lft forever

檢視網路卡接收封包資訊

ifconfig virtio_user0
virtio_user0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet6 fe80::92e2:baff:fe85:3d01  prefixlen 64  scopeid 0x20<link>
        ether 1a:e0:f5:1f:21:01  txqueuelen 1000  (Ethernet)
        RX packets 2899366  bytes 2334954577 (2.1 GiB)
        RX errors 0  dropped 1  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

http://doc.dpdk.org/guides-22.11/howto/virtio_user_as_exception_path.html
https://www.redhat.com/en/topics/virtualization/what-is-a-hypervisor
https://en.wikipedia.org/wiki/Hypervisor
https://www.ibm.com/topics/hypervisors
https://aws.amazon.com/cn/what-is/hypervisor/
https://developer.ibm.com/articles/l-virtio/
https://docs.oasis-open.org/virtio/virtio/v1.2/virtio-v1.2.html
https://www.redhat.com/en/blog/deep-dive-virtio-networking-and-vhost-net
https://qemu-project.gitlab.io/qemu/interop/vhost-user.html
https://www.redhat.com/en/blog/journey-vhost-users-realm
https://mp.weixin.qq.com/s/q3qAaMBGyQ5E2_2Dd-IvdA
https://www.cnblogs.com/bakari/p/8971710.html
https://doc.dpdk.org/guides-18.08/sample_app_ug/exception_path.html
https://doc.dpdk.org/guides/prog_guide/kernel_nic_interface.html