天天看點

StratoVirt 中的虛拟網卡是如何實作的?

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 傳輸協定,它的出現解決了全虛拟化場景中模拟指令導緻的性能開銷問題。整體架構如下圖所示:

StratoVirt 中的虛拟網卡是如何實作的?

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,可以減少使用者态和核心态之間的切換,提高網絡性能。整體架構如下圖所示:

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 減少了陷出開銷,進一步提高網絡性能。整體架構如下圖所示:

StratoVirt 中的虛拟網卡是如何實作的?

類似于 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 虛拟網卡。

繼續閱讀