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源創計劃”,歡迎正在閱讀的你也加入,一起分享。