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 傳輸協定,它的出現解決了全虛拟化場景中模拟指令導緻的性能開銷問題。整體架構如下圖所示:
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsQTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5iM2UzMzYDZ5MTZ3IjZiFjZyYzX5UzN1kDM5AzLchDMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.png)
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 虛拟網卡。