簡單分析Linux虛擬化KVM-Qemu之vhost-net
說明:
KVM版本:5.9.1
QEMU版本:5.0.0
工具:Source Insight 3.5, Visio
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é)議棧等的交互,路徑長并且涉及的切換多,帶來了性能的損耗;
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ū)動,針對數(shù)據(jù)傳遵循virtio協(xié)議,因此vhost-net中需要去實現(xiàn)virtqueue的相關(guān)機制;
本文將分析vhost-net的原理,只說重點,進(jìn)入主題。
2. 數(shù)據(jù)結(jié)構(gòu)
vhost-net內(nèi)核模塊的層次結(jié)構(gòu)如下圖:

struct vhost_net
:用于描述Vhost-Net設(shè)備。它包含幾個關(guān)鍵字段:1)struct vhost_dev
,通用的vhost設(shè)備,可以類比struct device
結(jié)構(gòu)體內(nèi)嵌在其他特定設(shè)備的結(jié)構(gòu)體中;2)struct vhost_net_virtqueue
,實際上對struct vhost_virtqueue
進(jìn)行了封裝,用于網(wǎng)絡(luò)包的數(shù)據(jù)傳輸;3)struct vhost_poll
,用于socket的poll,以便在數(shù)據(jù)包接收與發(fā)送時進(jìn)行任務(wù)調(diào)度;struct vhost_dev
:描述通用的vhost設(shè)備,可內(nèi)嵌在基于vhost機制的其他設(shè)備結(jié)構(gòu)體中,比如struct vhost_net
,struct vhost_scsi
等。關(guān)鍵字段如下:1)vqs
指針,指向已經(jīng)分配好的struct vhost_virtqueue
,對應(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è)備對應(yīng)的virtqueue,這部分內(nèi)容可以參考之前virtqueue機制分析,本質(zhì)上是將Qemu中virtqueue處理機制下沉到了Kernel中。關(guān)鍵字段如下:1)struct vhost_poll
,用于poll eventfd對應(yīng)的文件,當(dāng)不滿足處理請求時會添加到eventfd對應(yīng)的等待隊列中,而一旦被喚醒,該結(jié)構(gòu)體中的struct vhost_work
(執(zhí)行函數(shù)被初始化為handle_tx_kick
,以發(fā)送為例)將被放置到內(nèi)核線程中去執(zhí)行;
? 結(jié)構(gòu)體的核心圍繞著數(shù)據(jù)和通知機制,其中數(shù)據(jù)在vhost_virtqueue
中體現(xiàn),而通知主要是通過vhost_poll
來實現(xiàn),具體的細(xì)節(jié)下文將進(jìn)一步描述。
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【749907784】整理了一些個人覺得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦!?。。ê曨l教程、電子書、實戰(zhàn)項目及代碼)??


3. 流程分析
3.1 初始化
vhost-net為內(nèi)核模塊,注冊為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)實現(xiàn)的virtio后端,一種是在用戶態(tài)中實現(xiàn)的virtio后端;kernel_ops
:vhost的內(nèi)核操作函數(shù)集,都是一些回調(diào)函數(shù)的實現(xiàn),最終會通過vhost_kernel_call-->ioctl-->vhost-net.ko
路徑,進(jìn)行配置;
ioctl
系統(tǒng)調(diào)用,與驅(qū)動交互簡單來說可以分為三大類,下邊分別介紹幾個關(guān)鍵的設(shè)置:

vhost net設(shè)置
VHOST_SET_OWNER
:底層會為調(diào)用者創(chuàng)建一個內(nèi)核線程,對應(yīng)到前文中數(shù)據(jù)結(jié)構(gòu)中的vhost_worker
,同時在vhost_dev
結(jié)構(gòu)體中還會保存調(diào)用者線程的內(nèi)存空間數(shù)據(jù)結(jié)構(gòu);VHOST_NET_SET_BACKEND
:設(shè)置vhost-net的后端設(shè)備,比如Qemu往內(nèi)核態(tài)傳遞的tap設(shè)備對應(yīng)的fd,從而讓vhost-net直接與tap設(shè)備進(jìn)行通信;vhost dev設(shè)置
從Guest OS中的虛擬地址到最終的Host上的物理地址映射關(guān)系如上圖所示,如果在Guest OS中要將數(shù)據(jù)發(fā)送出去,實際上只需要將Qemu中關(guān)于Guest OS的物理地址布局信息傳遞下去,此外再結(jié)合
VHOST_SET_OWNER
時傳遞的內(nèi)存空間信息,就可以根據(jù)映射關(guān)系找到Guest OS中的數(shù)據(jù)對應(yīng)到Host之上的物理地址,完成最后搬運即可;VHOST_SET_MEM_TABLE
:將Qemu中的虛擬機物理地址布局信息傳遞給內(nèi)核,為了解釋清楚這個問題,可以回顧一下之前內(nèi)存虛擬化中的一張圖:

vhost vring設(shè)置
VHOST_SET_VRING_KICK
:設(shè)置vhost-net模塊前端virtio驅(qū)動發(fā)送通知時觸發(fā)的eventfd,通知機制,最終觸發(fā)handle_kick函數(shù)的執(zhí)行;VHOST_SET_VRING_CALL
:設(shè)置vhost-net后端到虛擬機virtio前端的中斷通知,參考之前文章中的irqfd機制;此外關(guān)于vring的設(shè)備還包括vring的大小,地址信息等;
上述的這些設(shè)置的流程路徑如下,只畫出了關(guān)鍵路徑:

當(dāng)Guest OS中的virtio-net驅(qū)動完成初始化后,會通過
vp_set_status
來設(shè)置狀態(tài),以通知后端驅(qū)動已經(jīng)ready,此時會觸發(fā)VM的退出并進(jìn)入KVM進(jìn)行異常處理,最終路由給Qemu;Qemu中的vcpu線程監(jiān)測異常,當(dāng)檢測到
KVM_EXIT_MMIO
時,去回調(diào)注冊該IO區(qū)域的讀寫函數(shù),比如virtio_pci_common_write
函數(shù),在該函數(shù)中逐級往下最終調(diào)用到vhost_net_start
函數(shù);在
vhost_net_start
中最終去通過kernel_ops
函數(shù)集去設(shè)置底層并交互;
初始化完成后,接下來讓我們看看數(shù)據(jù)的發(fā)送與接收,為了能將整個流程表達(dá)清楚,我會將完整的圖拆分成幾個步驟來講述。
3.2 數(shù)據(jù)發(fā)送
1)
發(fā)送前的框圖如下:

Guest OS中的virtio-net驅(qū)動中維護(hù)兩個virtqueue,分別用于發(fā)送和接收;
圖中的
datagram
表示的是需要發(fā)送的數(shù)據(jù);KVM模塊提供了
ioeventfd
和irqfd
用于通知機制;vhost-net
模塊中創(chuàng)建好了vhost_worker內(nèi)核線程,用于處理任務(wù);
2)

當(dāng)數(shù)據(jù)包準(zhǔn)備好之后,通過往kick fd上觸發(fā)信號,從而喚醒vhost_worker內(nèi)核線程來調(diào)用
handle_tx_kick
進(jìn)行數(shù)據(jù)的發(fā)送;當(dāng)Tap/Tun不具備發(fā)送條件時,vhost_worker會poll在socket上,等待Tap/Tun的喚醒,一旦被喚醒后可以調(diào)用
handle_tx_net
發(fā)送;最終的
handle_tx
完成具體的發(fā)送;
3)
vhost_get_vq_desc
函數(shù)在vritqueue中查找可用的buffer,并將信息存儲到iov
中,以便更好的訪問;sock->ops->sendmsg()
函數(shù),實際調(diào)用的是tun_sendmsg
函數(shù),在該函數(shù)中分配了skb
結(jié)構(gòu)體,并將iov[]
中的信息傳遞過來,最終如圖中所示完成數(shù)據(jù)的拷貝和發(fā)送,通過NIC發(fā)送出去;
4)

數(shù)據(jù)發(fā)送完畢后,通過irqfd機制通知vcpu;
3.3 數(shù)據(jù)接收
數(shù)據(jù)的接收是發(fā)送的逆過程,流程一致:
1)

初始化部分與發(fā)送過程一致;
Tap/Tun驅(qū)動從NIC接收到數(shù)據(jù)包,準(zhǔn)備發(fā)送給vhost-net;
2)

vhost-net中的vhost_worker線程也poll在兩個fd之上,與發(fā)送端類似;
kick fd上觸發(fā)信號時最終調(diào)用
handle_rx_kick
函數(shù),Tap/Tun對應(yīng)的socket上觸發(fā)信號時,調(diào)用handle_rx_net
函數(shù);最終通過
handle_rx
來完成實際的接收;
3)

接收過程中,
vhost_get_vq_desc
獲取virtqueue中的可用buffer,并將信息存儲到iov[]
中;sock->ops->recvmsg()
函數(shù)實際指向tun_recvmsg
函數(shù),在該函數(shù)中最終完成數(shù)據(jù)的傳遞;
4)

數(shù)據(jù)接收完成后,通過irqfd機制通過vcpu,從而在Guest OS中進(jìn)行處理;
原文作者:LoyenWang
