StratoVirt 中的虛擬網卡是如何實現的?

語言: CN / TW / HK

StratoVirt 當前支持 Virtio-net/Vhost-net/Vhost-user-net 三種虛擬網卡,這三種虛擬網卡都基於 virtio 協議實現數據面。Virtio-net 數據面存在一層用户態到內核態的切換,Vhost-net 通過將數據面卸載到內核態解決了該問題,但是仍然需要 Guest 陷出來通知後端。Vhost-user net 將數據面卸載到用户態進程中,並綁定固定的核,不停的對共享環進行輪訓操作,解決了 Vhost-net 存在的問題。接下來分別介紹每種虛擬網卡是如何實現的。

Virtio-net

Virtio-net 是一種虛擬的以太網卡,通過 tap 設備基於 virtio 協議的半虛擬化框架來實現前後端通信。Virtio 協議是一種在半虛擬化場景中使用的 I/O 傳輸協議,它的出現解決了全虛擬化場景中模擬指令導致的性能開銷問題。整體架構如下圖所示:

Guest 中需要支持 virtio-net 驅動, Guest 和 StratoVirt 之間基於 virtio 協議通過共享內存實現 I/O 請求的處理。

「發包流程:」1) Guest 通過 virtio-net 驅動將 I/O 請求放入發送隊列,並觸發陷出通知後端;2) 陷出後由 KVM 通過 eventfd 通知 StratoVirt,共享環中有數據需要處理;3) StratoVirt 將數據從環中取出併發送給 tap 設備,後由 tap 設備自動發給物理網卡;

「收包流程:」1) 物理網卡發送數據到 tap 設備時,StratoVirt 會監聽到;2) StratoVirt 將 I/O 請求從 tap 設備中取出,放入到共享環的接收隊列中;3) StratoVirt 通過 irqfd 通知 KVM,由 KVM 注入中斷通知 Guest 接收數據;

virto-net 實現

使用 NetIoHandler 結構體作為處理 virtio-net 虛擬網卡事件的主體。其中包含收/發包結構 RxVirtio(rx)和 TxVirtio(tx)、tap 設備及其對應的文件描述符。RxVirtio/TxVirtio 中都包含隊列 queue 和事件描述符 queue_evt,隊列用 Mutex 鎖保護,可以保證多線程共享時的數據安全。代碼路徑:virtio/src/net.rs

struct TxVirtio {
    queue: Arc<Mutex<Queue>>,
    queue_evt: EventFd,
}

struct RxVirtio {
    queue: Arc<Mutex<Queue>>,
    queue_evt: EventFd,
    ...
}

struct NetIoHandler {
    // 收報結構
    rx: RxVirtio,
    // 發包結構
    tx: TxVirtio,
    // tap設備
    tap: Option<Tap>,
    // tap設備對應的文件描述符
    tap_fd: RawFd,
    ...
}

收/發包實現

虛擬機收包時,StratoVirt 從 tap 設備讀取數據到 avail ring 中。然後將索引加入到 used ring,再發送中斷給虛擬機,通知虛擬機接收數據。虛擬機發包流程和收包流程相似,不再單獨介紹。收包操作核心代碼(virtio/src/net.rs)實現如下:

fn handle_rx(&mut self) -> Result<()> {
    let mut queue = self.rx.queue.lock().unwrap();
    while let Some(tap) = self.tap.as_mut() {
        ...
        // 獲取avail ring中的elem,用於保存發給Guest的包
        let elem = queue
            .vring
            .pop_avail(&self.mem_space, self.driver_features)
            .chain_err(|| "Failed to pop avail ring for net rx")?;
        let mut iovecs = Vec::new();
        for elem_iov in elem.in_iovec.iter() {
            // Guest地址轉換為HVA
            let host_addr = queue
                .vring
                .get_host_address_from_cache(elem_iov.addr, &self.mem_space);
            if host_addr != 0 {
                let iovec = libc::iovec {
                    iov_base: host_addr as *mut libc::c_void,
                    iov_len: elem_iov.len as libc::size_t,
                };
                iovecs.push(iovec);
            } else {
                error!("Failed to get host address for {}", elem_iov.addr.0);
            }
        }
        // 從tap設備讀取數據
        let write_count = unsafe {
            libc::readv(
                tap.as_raw_fd() as libc::c_int,
                iovecs.as_ptr() as *const libc::iovec,
                iovecs.len() as libc::c_int,
            )
        };
        ...
        queue
            .vring
            .add_used(&self.mem_space, elem.index, write_count as u32)
            .chain_err(|| {
                format!(
                    "Failed to add used ring for net rx, index: {}, len: {}",
                    elem.index, write_count
                )
            })?;
        self.rx.need_irqs = true;
    }

    if self.rx.need_irqs {
        self.rx.need_irqs = false;
        // 中斷通知Guest
        (self.interrupt_cb)(&VirtioInterruptType::Vring, Some(&queue))
            .chain_err(|| ErrorKind::InterruptTrigger("net", VirtioInterruptType::Vring))?;
    }

    Ok(())
}

Vhost-net

Vhost-net 將 Vritio-net 中的數據面卸載到了內核中,內核中會啟動一個線程來處理 I/O 請求,繞過了 StratoVirt,可以減少用户態和內核態之間的切換,提高網絡性能。整體框架如下圖所示:

Vhost-net 的控制面基於 vhost 協議將 vring、eventfd 等信息發給 vhost-net 驅動,vhost-net 驅動在內核中可以訪問 vring 信息,完成收/發包操作,用户態和內核態之間無需切換,有效的提升網絡性能。

「發包流程:」1) Guest 通過 virtio-net 驅動將 I/O 請求放入發送隊列,並觸發陷出通知後端;2) 陷出後由 KVM 通過 eventfd 通知 vhost-net,共享環中有數據需要處理;3) Vhost-net 將數據從環中取出併發送給 tap 設備,後由 tap 設備自動發給物理網卡;

「收包流程:」1) 物理網卡發送數據到 tap 設備時,會通知 vhost-net;2) vhost-net 將 I/O 請求從 tap 設備中取出,放入到共享環的接收隊列中;3) vhost-net 通過 irqfd 通知 KVM,由 KVM 注入中斷通知 Guest 接收數據;

Vhost-net 實現

虛擬機啟動時,當虛擬機中 virtio-net 驅動準備好後,StratoVirt 中調用 activate 函數使能 virtio 設備。該函數基於 vhost 協議將前後端協商的特性、虛擬機的內存信息、vring 的相關信息、tap 的信息等發送給 vhost-net 驅動,將 virtio 數據面卸載到單獨的進程中進行處理,來提升網絡性能。使能設備核心代碼(virtio/src/vhost/kernel/net.rs)實現如下:

fn activate(
    &mut self,
    _mem_space: Arc<AddressSpace>,
    interrupt_cb: Arc<VirtioInterrupt>,
    queues: &[Arc<Mutex<Queue>>],
    queue_evts: Vec<EventFd>,
) -> Result<()> {
    let backend = match &self.backend {
        None => return Err("Failed to get backend for vhost net".into()),
        Some(backend_) => backend_,
    };

    // 設置前後端協商的特性給vhost-net
    backend
        .set_features(self.vhost_features)
        .chain_err(|| "Failed to set features for vhost net")?;

    // 設置虛擬機的內存信息給vhost-net
    backend
        .set_mem_table()
        .chain_err(|| "Failed to set mem table for vhost net")?;

    for (queue_index, queue_mutex) in queues.iter().enumerate() {
        let queue = queue_mutex.lock().unwrap();
        let actual_size = queue.vring.actual_size();
        let queue_config = queue.vring.get_queue_config();

        // 設置vring的大小給vhost-net
        backend
            .set_vring_num(queue_index, actual_size)
            .chain_err(...)?;
        // 將vring的地址給vhost-net
        backend
            .set_vring_addr(&queue_config, queue_index, 0)
            .chain_err(...)?;
        // 設置vring的起始位置給vhost-net
        backend.set_vring_base(queue_index, 0).chain_err(...)?;
        // 設置輪詢vring使用的eventfd給vhost-net
        backend
            .set_vring_kick(queue_index, &queue_evts[queue_index])
            .chain_err(...)?;
        ...
        // 設置callfd給vhost-net,處理完請求後通知KVM時使用
        backend
            .set_vring_call(queue_index, &host_notify.notify_evt)
            .chain_err(...)?;

        let tap = match &self.tap {
            None => bail!("Failed to get tap for vhost net"),
            Some(tap_) => tap_,
        };
        // 設置tap信息給vhost-net
        backend.set_backend(queue_index, &tap.file).chain_err(...)?;
    }
    ...
}

Vhost-user net

Vhost-user net 在用户態基於 vhost 協議將 Vritio-net 的數據面卸載到了用户態進程 Ovs-dpdk 中,數據面由 Ovs-dpdk 接管,該進程會綁定到固定的核,不停的對共享環進行輪訓操作,來確認 vring 環中是否有數據需要處理。該輪訓機制使虛擬機在發送數據時不再需要陷出,相對於 Vhost-net 減少了陷出開銷,進一步提高網絡性能。整體框架如下圖所示:

類似於 Vhost-net,Vhost-user net 的控制面基於用户態實現的 vhost 協議,在 StratoVirt 中調用 activate 函數激活 virtio 設備時,將虛擬機的內存信息、Vring 的相關信息、eventfd 等發送給 Ovs-dpdk,供其進行收/發包使用。

「發包流程:」1) Guest 通過 virtio-net 驅動將 I/O 請求放入發送隊列;2) Ovs-dpdk 一直在輪訓共享環,此時會輪訓到 1)中的請求;3) Ovs-dpdk 將 I/O 請求取出併發送給網卡;

「收包流程:」1) Ovs-dpdk 從網卡接收 I/O 請求;2) Ovs-dpdk 將 I/O 請求放入到共享環的接收隊列中;3) Ovs-dpdk 通過 irqfd 通知 KVM,由 KVM 注入中斷通知 Guest 接收數據;

該部分的代碼實現類似於 vhost-net,不再單獨介紹。

總結

Virtio-net/Vhost-net/Vhost-user-net 三種虛擬網卡各有優缺點,針對不同的場景可以選擇使用不同的虛擬網卡。最通用的是 Virtio-net 虛擬網卡。對性能有一定要求且 Host 側支持 vhost 時,可以使用 Vhost-net 虛擬網卡。對性能要求較高,並且 Host 側有充足的 CPU 資源時,可以使用 Vhost-user net 虛擬網卡。

關注我們

StratoVirt 當前已經在 openEuler 社區開源。後續我們將開展一系列技術分享,讓大家更加詳細地瞭解 StratoVirt。如果您對虛擬化技術或者 StratoVirt 感興趣,歡迎掃描文末小助手二維碼,回覆 StratoVirt 加入 SIG 交流羣。

項目地址:https://gitee.com/openeuler/stratovirt

項目交流:https://gitee.com/openeuler/stratovirt/issues


本文分享自微信公眾號 - openEuler(openEulercommunity)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閲讀的你也加入,一起分享。