Linux虛擬化KVM-Qemu分析之Vhost-Net
本文轉(zhuǎn)載自微信公眾號(hào)「LoyenWang」,作者LoyenWang。轉(zhuǎn)載本文請(qǐng)聯(lián)系LoyenWang公眾號(hào)。
背景
- Read the fucking source code! --By 魯迅
- A picture is worth a thousand words. --By 高爾基
說明:
- KVM版本:5.9.1
- QEMU版本:5.0.0
- 工具:Source Insight 3.5, Visio
- 文章同步在博客園:https://www.cnblogs.com/LoyenWang/
1. 概述
讓我們先來看看問題的引入,在之前的virtio系列文章中,網(wǎng)絡(luò)虛擬化的框架如下圖所示:
- Qemu中的virtio-net設(shè)備數(shù)據(jù)包收發(fā),通過用戶態(tài)訪問tap設(shè)備完成的;
- 收發(fā)過程涉及Guest OS,KVM,Qemu中的virtio-net設(shè)備,Host中的網(wǎng)絡(luò)協(xié)議棧等的交互,路徑長(zhǎng)并且涉及的切換多,帶來了性能的損耗;
- vhost-net的引入,就是將vitio-net后端設(shè)備的數(shù)據(jù)處理模塊下沉到Kernel中,從而提高整體的效率;
vhost-net的框架圖如下:
- 從圖中可以看出,Guest的網(wǎng)絡(luò)數(shù)據(jù)交互直接可以通過vhost-net內(nèi)核模塊進(jìn)行處理,而不再需要從內(nèi)核態(tài)切換回用戶態(tài)的Qemu進(jìn)程中進(jìn)行處理;
- 之前的文章分析過virtio設(shè)備與驅(qū)動(dòng),針對(duì)數(shù)據(jù)傳遵循virtio協(xié)議,因此vhost-net中需要去實(shí)現(xiàn)virtqueue的相關(guān)機(jī)制;
本文將分析vhost-net的原理,只說重點(diǎn),進(jìn)入主題。
2. 數(shù)據(jù)結(jié)構(gòu)
vhost-net內(nèi)核模塊的層次結(jié)構(gòu)如下圖:
- struct vhost_net:用于描述Vhost-Net設(shè)備。它包含幾個(gè)關(guān)鍵字段:1)struct vhost_dev,通用的vhost設(shè)備,可以類比struct device結(jié)構(gòu)體內(nèi)嵌在其他特定設(shè)備的結(jié)構(gòu)體中;2)struct vhost_net_virtqueue,實(shí)際上對(duì)struct vhost_virtqueue進(jìn)行了封裝,用于網(wǎng)絡(luò)包的數(shù)據(jù)傳輸;3)struct vhost_poll,用于socket的poll,以便在數(shù)據(jù)包接收與發(fā)送時(shí)進(jìn)行任務(wù)調(diào)度;
- struct vhost_dev:描述通用的vhost設(shè)備,可內(nèi)嵌在基于vhost機(jī)制的其他設(shè)備結(jié)構(gòu)體中,比如struct vhost_net,struct vhost_scsi等。關(guān)鍵字段如下:1)vqs指針,指向已經(jīng)分配好的struct vhost_virtqueue,對(duì)應(yīng)數(shù)據(jù)傳輸;2)work_list,任務(wù)鏈表,用于放置需要在vhost_worker內(nèi)核線程上執(zhí)行的任務(wù);3)worker,用于指向創(chuàng)建的內(nèi)核線程,執(zhí)行任務(wù)列表中的任務(wù);
- struct vhost_virtqueue:用于描述設(shè)備對(duì)應(yīng)的virtqueue,這部分內(nèi)容可以參考之前virtqueue機(jī)制分析,本質(zhì)上是將Qemu中virtqueue處理機(jī)制下沉到了Kernel中。關(guān)鍵字段如下:1)struct vhost_poll,用于poll eventfd對(duì)應(yīng)的文件,當(dāng)不滿足處理請(qǐng)求時(shí)會(huì)添加到eventfd對(duì)應(yīng)的等待隊(duì)列中,而一旦被喚醒,該結(jié)構(gòu)體中的struct vhost_work(執(zhí)行函數(shù)被初始化為handle_tx_kick,以發(fā)送為例)將被放置到內(nèi)核線程中去執(zhí)行;
結(jié)構(gòu)體的核心圍繞著數(shù)據(jù)和通知機(jī)制,其中數(shù)據(jù)在vhost_virtqueue中體現(xiàn),而通知主要是通過vhost_poll來實(shí)現(xiàn),具體的細(xì)節(jié)下文將進(jìn)一步描述。
3. 流程分析
3.1 初始化
vhost-net為內(nèi)核模塊,注冊(cè)為misc設(shè)備,Qemu通過系統(tǒng)調(diào)用接口與內(nèi)核交互,Qemu中的初始化如下圖:
- Qemu中tap設(shè)備初始化在net_init_tap中完成,其中net_init_tap_one打開vhost-net設(shè)備文件,用于與內(nèi)核的vhost-net交互;
- vhost_set_backend_type:設(shè)置vhost的后端類型,以及vhost的操作函數(shù)集。目前有兩種vhost后端,一種是在內(nèi)核態(tài)實(shí)現(xiàn)的virtio后端,一種是在用戶態(tài)中實(shí)現(xiàn)的virtio后端;
- kernel_ops:vhost的內(nèi)核操作函數(shù)集,都是一些回調(diào)函數(shù)的實(shí)現(xiàn),最終會(huì)通過vhost_kernel_call-->ioctl-->vhost-net.ko路徑,進(jìn)行配置;
ioctl系統(tǒng)調(diào)用,與驅(qū)動(dòng)交互簡(jiǎn)單來說可以分為三大類,下邊分別介紹幾個(gè)關(guān)鍵的設(shè)置:
vhost net設(shè)置
- VHOST_SET_OWNER:底層會(huì)為調(diào)用者創(chuàng)建一個(gè)內(nèi)核線程,對(duì)應(yīng)到前文中數(shù)據(jù)結(jié)構(gòu)中的vhost_worker,同時(shí)在vhost_dev結(jié)構(gòu)體中還會(huì)保存調(diào)用者線程的內(nèi)存空間數(shù)據(jù)結(jié)構(gòu);
- VHOST_NET_SET_BACKEND:設(shè)置vhost-net的后端設(shè)備,比如Qemu往內(nèi)核態(tài)傳遞的tap設(shè)備對(duì)應(yīng)的fd,從而讓vhost-net直接與tap設(shè)備進(jìn)行通信;
vhost dev設(shè)置
從Guest OS中的虛擬地址到最終的Host上的物理地址映射關(guān)系如上圖所示,如果在Guest OS中要將數(shù)據(jù)發(fā)送出去,實(shí)際上只需要將Qemu中關(guān)于Guest OS的物理地址布局信息傳遞下去,此外再結(jié)合VHOST_SET_OWNER時(shí)傳遞的內(nèi)存空間信息,就可以根據(jù)映射關(guān)系找到Guest OS中的數(shù)據(jù)對(duì)應(yīng)到Host之上的物理地址,完成最后搬運(yùn)即可;
- VHOST_SET_MEM_TABLE:將Qemu中的虛擬機(jī)物理地址布局信息傳遞給內(nèi)核,為了解釋清楚這個(gè)問題,可以回顧一下之前內(nèi)存虛擬化中的一張圖:
vhost vring設(shè)置
- VHOST_SET_VRING_KICK:設(shè)置vhost-net模塊前端virtio驅(qū)動(dòng)發(fā)送通知時(shí)觸發(fā)的eventfd,通知機(jī)制,最終觸發(fā)handle_kick函數(shù)的執(zhí)行;
- VHOST_SET_VRING_CALL:設(shè)置vhost-net后端到虛擬機(jī)virtio前端的中斷通知,參考之前文章中的irqfd機(jī)制;
- 此外關(guān)于vring的設(shè)備還包括vring的大小,地址信息等;
上述的這些設(shè)置的流程路徑如下,只畫出了關(guān)鍵路徑:
- 當(dāng)Guest OS中的virtio-net驅(qū)動(dòng)完成初始化后,會(huì)通過vp_set_status來設(shè)置狀態(tài),以通知后端驅(qū)動(dòng)已經(jīng)ready,此時(shí)會(huì)觸發(fā)VM的退出并進(jìn)入KVM進(jìn)行異常處理,最終路由給Qemu;
- Qemu中的vcpu線程監(jiān)測(cè)異常,當(dāng)檢測(cè)到KVM_EXIT_MMIO時(shí),去回調(diào)注冊(cè)該IO區(qū)域的讀寫函數(shù),比如virtio_pci_common_write函數(shù),在該函數(shù)中逐級(jí)往下最終調(diào)用到vhost_net_start函數(shù);
- 在vhost_net_start中最終去通過kernel_ops函數(shù)集去設(shè)置底層并交互;
初始化完成后,接下來讓我們看看數(shù)據(jù)的發(fā)送與接收,為了能將整個(gè)流程表達(dá)清楚,我會(huì)將完整的圖拆分成幾個(gè)步驟來講述。
3.2 數(shù)據(jù)發(fā)送
1)
發(fā)送前的框圖如下:
- Guest OS中的virtio-net驅(qū)動(dòng)中維護(hù)兩個(gè)virtqueue,分別用于發(fā)送和接收;
- 圖中的datagram表示的是需要發(fā)送的數(shù)據(jù);
- KVM模塊提供了ioeventfd和irqfd用于通知機(jī)制;
- vhost-net模塊中創(chuàng)建好了vhost_worker內(nèi)核線程,用于處理任務(wù);
2)
- 當(dāng)數(shù)據(jù)包準(zhǔn)備好之后,通過往kick fd上觸發(fā)信號(hào),從而喚醒vhost_worker內(nèi)核線程來調(diào)用handle_tx_kick進(jìn)行數(shù)據(jù)的發(fā)送;
- 當(dāng)Tap/Tun不具備發(fā)送條件時(shí),vhost_worker會(huì)poll在socket上,等待Tap/Tun的喚醒,一旦被喚醒后可以調(diào)用handle_tx_net發(fā)送;
- 最終的handle_tx完成具體的發(fā)送;
3)
- vhost_get_vq_desc函數(shù)在vritqueue中查找可用的buffer,并將信息存儲(chǔ)到iov中,以便更好的訪問;
- sock->ops->sendmsg()函數(shù),實(shí)際調(diào)用的是tun_sendmsg函數(shù),在該函數(shù)中分配了skb結(jié)構(gòu)體,并將iov[]中的信息傳遞過來,最終如圖中所示完成數(shù)據(jù)的拷貝和發(fā)送,通過NIC發(fā)送出去;
4)
- 數(shù)據(jù)發(fā)送完畢后,通過irqfd機(jī)制通知vcpu;
3.3 數(shù)據(jù)接收
數(shù)據(jù)的接收是發(fā)送的逆過程,流程一致:
1)
初始化部分與發(fā)送過程一致;
Tap/Tun驅(qū)動(dòng)從NIC接收到數(shù)據(jù)包,準(zhǔn)備發(fā)送給vhost-net;
2)
- vhost-net中的vhost_worker線程也poll在兩個(gè)fd之上,與發(fā)送端類似;
- kick fd上觸發(fā)信號(hào)時(shí)最終調(diào)用handle_rx_kick函數(shù),Tap/Tun對(duì)應(yīng)的socket上觸發(fā)信號(hào)時(shí),調(diào)用handle_rx_net函數(shù);
- 最終通過handle_rx來完成實(shí)際的接收;
3)
- 接收過程中,vhost_get_vq_desc獲取virtqueue中的可用buffer,并將信息存儲(chǔ)到iov[]中;
- sock->ops->recvmsg()函數(shù)實(shí)際指向tun_recvmsg函數(shù),在該函數(shù)中最終完成數(shù)據(jù)的傳遞;
4)
數(shù)據(jù)接收完成后,通過irqfd機(jī)制通過vcpu,從而在Guest OS中進(jìn)行處理;
vhost-net的整體內(nèi)容較多,從上到下涉及到的細(xì)節(jié)很繁瑣,短短的一篇文章難以涵蓋全部,權(quán)當(dāng)給個(gè)輪廓了。
暫且告一段落吧。
參考
Introduction to virtio-networking and vhost-net
Deep dive into Virtio-networking and vhost-net
Vhost-net Device IOTLB