本來這是在前端驅(qū)動后期分析的,但是這部分內(nèi)容比較多,且分析了后端notify前端的機(jī)制,所以還是單獨拿出一節(jié)分析比較好!
還是拿網(wǎng)絡(luò)驅(qū)動部分做案例,網(wǎng)絡(luò)驅(qū)動部分有兩個隊列,(忽略控制隊列):接收隊列和發(fā)送隊列;每個隊列都對應(yīng)一個virtqueue,兩個隊列之間是互不影響的。
前后端利用virtqueue的方式如下圖所示:
這里再詳細(xì)的描述下,當(dāng)兩個queue都需要客戶機(jī)填充buffer,ReceiveQueue需要客戶機(jī) driver提前填充分配好的空buffer,然后記錄到availRing,并在恰當(dāng)?shù)臅r機(jī)通知后端,當(dāng)外部網(wǎng)絡(luò)有數(shù)據(jù)包到達(dá)時,qemu后端就從availRing 中獲取一個buffer,然后填充數(shù)據(jù),完事后記錄buffer head index到usedRing.最后在恰當(dāng)?shù)臅r機(jī)通知客戶機(jī)(向客戶機(jī)注入中斷),客戶機(jī)接收到信號便知道有數(shù)據(jù)包到達(dá),這里只需要從usedRing 中獲取到index,然后取data數(shù)組的第i個元素即可。因為在客戶機(jī)填充buffer的時候把邏輯buffer的指針保存在data數(shù)組中。
而SendQueue同樣需要客戶機(jī)去填充,只不過這里是當(dāng)客戶機(jī)需要發(fā)送數(shù)據(jù)包時,把數(shù)據(jù)包構(gòu)造成邏輯buffer,然后填充到send Queue,并在恰當(dāng)?shù)臅r機(jī)通知后端,qemu后端收到通知就知道那個隊列有請求到達(dá),如果當(dāng)前沒有處理其他數(shù)據(jù)包就著手處理這個數(shù)據(jù)包。具體就同樣是從AvailRing中取出buffer head index,然后從描述符表中g(shù)et到buffer,這時就需要從buffer中copy數(shù)據(jù)了,因為要把數(shù)據(jù)包從host發(fā)送出去,然后更新usedRing。最后同樣要在恰當(dāng)?shù)臅r機(jī)通知客戶機(jī)。注意這里客戶機(jī)同樣需要從usedRing 中g(shù)et index,但是這里主要是用于delay notify,因為數(shù)據(jù)包由客戶機(jī)構(gòu)造,其占用的buffer并不能重復(fù)使用,只是每次有數(shù)據(jù)包就把其構(gòu)造成buffer而已。
以上便是基本的使用sendqueue和receive的原理,但是還有一點上面我沒有提到,就是通知的那個恰當(dāng)?shù)臅r機(jī),那么這個恰當(dāng)?shù)膶嶋H究竟是什么時候呢??在virtIO中有兩種方式控制前后端的notify.
1、flags字段
2、事件觸發(fā)
1、在vring_avail和vring_used的flags字段,控制前后端的通信。vring_used中的flags用于通知driver端,當(dāng)add一個buffer的時候不用notify后端。而vring_avail中的flags用于通知qemu端,當(dāng)消費一個buffer的時候不用interrupt 客戶機(jī)。
2、在virtIO中又加入了另一種機(jī)制,需要由driver和qemu自己判斷是否需要通知,也就是設(shè)置一個限額,當(dāng)一端添加buffer或者消費buffer的數(shù)量達(dá)到指定數(shù)目,就觸發(fā)事件,從而發(fā)生notify或者interrupt。在有這種機(jī)制的情況下就忽略了前面所說的flags。
這里我們以receiveQueue為例,分析下前后端的delay notify機(jī)制。
在front driver端:
客戶機(jī)driver通過NAPI接收數(shù)據(jù)時,會在可用buffer不足的時候調(diào)用函數(shù)添加,具體就是try_fill_recv:
至于添加的是哪種類型的buffer,我們這里并不關(guān)心,循環(huán)結(jié)束就調(diào)用virtqueue_kick(rq->vq)函數(shù),此時參數(shù)是接收隊列的virtqueue,
接下來就調(diào)用到了virtqueue_kick_prepare函數(shù),該函數(shù)判斷當(dāng)前應(yīng)不應(yīng)該通知后端。先看下函數(shù)的代碼:
這里面涉及到幾個變量,old是add_sg之前的avail.idx,而new是當(dāng)前的avail.idx,還有一個是vring_avail_event(&vq->vring),看具體的實現(xiàn):
可以看到這里是VRingUsed中的ring數(shù)組最后一項的值,該值在后端驅(qū)動從virtqueue中pop一個elem之前設(shè)置成相應(yīng)隊列的下一個將要使用的index,即last_avail_index。
看下vring_need_event函數(shù):
前后端通過對比(__u16)(new_idx - event_idx - 1) < (__u16)(new_idx - old)來判斷是否需要notify后端,這在數(shù)據(jù)量比較大的時候顯得很實用。在初始狀態(tài)下,即在qemu一個buffer還沒有使用的情況下,event_idx必然是0,那么此時這里的判斷肯定為真,所以notify后端。后端收到通知就從virtqueue中pop buffer,同時在此之前需要設(shè)置event_idx,代碼見qemu virtio.c的virtqueue_pop函數(shù):
如果是初始化狀態(tài),即當(dāng)前是首次執(zhí)行virtqueue_pop函數(shù),last_avail_idx=0,在++后就成了1,然后設(shè)置此值到UsedRing.ring[]數(shù)組的最后一項:
設(shè)置成功后就執(zhí)行pop之后的處理,寫入數(shù)據(jù)完成后,調(diào)用后端的virtio_notify(vdev, q->rx_vq)函數(shù)。該函數(shù)執(zhí)行前同樣需要判斷是否需要notify,具體函數(shù)為virtio_should_notify
該函數(shù)邏輯和前端driver總的判斷函數(shù)大致類似,但是還是有些不同,首先,如果隊列為空即當(dāng)前沒有可用buffer了,那么必然會notify前端;
接著判斷是否支持這樣事件觸發(fā)式的方式即VIRTIO_RING_F_EVENT_IDX,如果不支持,就通過flags字段來判斷。而如果支持,就通過事件觸發(fā)來通知。
這里有兩個條件:第一個是v = vq->signalled_used_valid和vring_need_event(vring_get_used_event(vq), new, old)
v = vq->signalled_used_valid在初始化的時候被設(shè)置成false,表示還沒有向前端做任何通知,而后再每次的virtio_should_notify中就會設(shè)置成true,并更新vq->signalled_used = vq->used_idx;所以如果是首次嘗試通知前端,則總能成功,否則需要判斷vring_need_event(vring_get_used_event(vq), new, old),該函數(shù)具體是根前面邏輯是一樣的,正如前面所說,這是第一次嘗試通知,所以總能成功。而vring_get_used_event(vq)是VRingAvail.ring[]數(shù)組的最后一項的值,該值在客戶機(jī)driver中被設(shè)置
在次回到linux driver中,就會從usedRing中取buffer,同樣每取出一個buffer就會設(shè)置used_event,代碼見virtio_ring.c的virtqueue_get_buf函數(shù),設(shè)置的值是vq->last_used_idx,記錄客戶機(jī)處理位置。
到目前為止,基本一次完整的交互已經(jīng)完成了,但是由于是初次交互,前后端的delay機(jī)制都沒起作用,判斷條件中使用到的event_idx已經(jīng)更新了,假如說首次add 8個buffer,然后通知了后端,并且后端使用了三個buffer并首次notify前端,此時 后端向第4個buffer中寫數(shù)據(jù),last_avail_idx=4(從0開始),那么used_event=4,此時前端發(fā)現(xiàn)可用buffer不足,需要添加,那么本次添加了5個,即new=8+5=13,old=8,new-old=5,而此時new-used_event-1=8,條件不滿足,所以此時前端driver添加的buffer就不用notify后端。而話說這段時間后端又處理好了第二個數(shù)據(jù)包,使用了3個buffer。但不幸,前端還在處理第二個buffer,即last_used_idx=2,則used_event=2;對于后端來講new-old=3,new-used_event-1=3,條件不滿足,所以也不用通知。這樣delay notify的機(jī)制便顯示出效果了。筆者認(rèn)為這其實本質(zhì)上就是一場速度的對決,為了保證公平,即使一方處理快,也不能任意向另一端發(fā)送數(shù)據(jù),只能待對方處理的差不多了你才能發(fā),這樣發(fā)送一方可以歇歇,而接受一方也不會因為處理不及而丟棄,從而造成浪費!哈哈,真是無規(guī)矩不成方圓!
具體通知方式:
前面已經(jīng)提到前端或者后端完成某個操作需要通知另一端的時候需要某種notify機(jī)制。這個notify機(jī)制是啥呢?這里分為兩個方向
1、guest->host
前面也已經(jīng)介紹,當(dāng)前端想通知后端時,會調(diào)用virtqueue_kick函數(shù),繼而調(diào)用virtqueue_notify,對應(yīng)virtqueue結(jié)構(gòu)中的notify函數(shù),在初始化的時候被初始化成vp_notify(virtio_pci.c中),看下該函數(shù)的實現(xiàn)
可以看到這里僅僅是吧vq的index編號寫入到設(shè)備的IO地址空間中,實際上就是設(shè)備對應(yīng)的PCI配置空間中VIRTIO_PCI_QUEUE_NOTIFY位置。這里執(zhí)行IO操作會引發(fā)VM-exit,繼而退出到KVM->qemu中處理。看下后端驅(qū)動的處理方式。在qemu代碼中virtio-pci.c文件中有函數(shù)virtio_ioport_write專門處理前端驅(qū)動的IO寫操作,看
這里首先判斷隊列號是否在合法范圍內(nèi),然后調(diào)用virtio_queue_notify函數(shù),而最終會調(diào)用到virtio_queue_notify_vq,該函數(shù)其實僅僅調(diào)用了VirtQueue結(jié)構(gòu)中綁定的處理函數(shù)handle_output,該函數(shù)根據(jù)不同的設(shè)備有不同的實現(xiàn),比如網(wǎng)卡有網(wǎng)卡的實現(xiàn),而塊設(shè)備有塊設(shè)備的實現(xiàn)。以網(wǎng)卡為例看看創(chuàng)建VirtQueue的時候給綁定的是哪個函數(shù)。在virtio-net,c中的virtio_net_init,可以看到這里給接收隊列綁定的是virtio_net_handle_rx,而給發(fā)送隊列綁定的是virtio_net_handle_tx_bh或者virtio_net_handle_tx_timer。而對于塊設(shè)備則對應(yīng)的是virtio_blk_handle_output函數(shù)。
2、host->guest
host通知guest當(dāng)然是通過注入中斷的方式,首先調(diào)用的是virtio_notify,繼而調(diào)用virtio_notify_vector并把中斷向量作為參數(shù)傳遞進(jìn)去。這里就調(diào)用了設(shè)備關(guān)聯(lián)的notify函數(shù),具體實現(xiàn)為virtio_pci_notify函數(shù),常規(guī)中斷(非MSI)會調(diào)用qemu_set_irq,在8259a中斷控制器的情況下回調(diào)用kvm_pic_set_irq,然后到了kvm_set_irq,這里就會通過kvm_vm_ioctl和KVM交互,接口為KVM_IRQ_LINE,通知KVM對guest進(jìn)行中斷的注入。KVm里的kvm_vm_ioctl函數(shù)會對此調(diào)用進(jìn)行處理,具體就是調(diào)用kvm_vm_ioctl_irq_line,之后就調(diào)用kvm_set_irq函數(shù)進(jìn)行注入了。之后的流程參看中斷虛擬化部分。
共享內(nèi)存
前面提到,在guest通知host的時候,是把隊列的索引寫入到了配置空間的VIRTIO_PCI_QUEUE_NOTIFY字段,但是僅僅一個索引是怎么找到指定的隊列,且數(shù)據(jù)時什么時候到達(dá)后端的呢?這就用到了共享內(nèi)存。我們知道的是前后端的確通過共享內(nèi)存的方式傳遞數(shù)據(jù),但是數(shù)據(jù)的地址是怎么傳遞到后端的,這是個問題。本小節(jié)主要分析下這個問題。
為了便于理解我們先闡述其原理,然后結(jié)合代碼看具體的實現(xiàn)。實際上前后端在初始化后就共享了一段連續(xù)的內(nèi)存區(qū),注意這里是物理上連續(xù)的內(nèi)存區(qū)(GPA),由客戶機(jī)內(nèi)部初始化隊列的時候分配,所以這里就是需要和伙伴系統(tǒng)交互。這段內(nèi)存區(qū)的結(jié)構(gòu)如下圖所示
對于vring了解的朋友應(yīng)該很熟悉這個結(jié)構(gòu),沒錯,這就是通過vring管理的結(jié)構(gòu),換句話說,前后端直接共享的其實是vring。也就是說針對同一個隊列(比如網(wǎng)卡的發(fā)送隊列),前后端已經(jīng)形成一種協(xié)議,通過這段內(nèi)存區(qū)交換數(shù)據(jù)的地址信息。在把數(shù)據(jù)的地址信息寫入到desc數(shù)組中后,僅僅需要通知另一端,另一端就知道從哪里取出數(shù)據(jù)。當(dāng)然還是通過desc數(shù)組。具體數(shù)據(jù)的傳遞過程參見其他小結(jié)。因此在初始化階段,前端分配好內(nèi)存區(qū),并初始化好前端的vring后,就把內(nèi)存區(qū)的信息傳遞到后端,后端也利用這個內(nèi)存區(qū)的信息初始化隊列相關(guān)的vring。這樣vring就在前后端保持了一致。原理就是如此,下面看具體初始化代碼:
前端:
virtnet_probe->init_vqs->virtnet_find_vqs->vi->vdev->config->find_vqs(vp_find_vqs)->vp_try_to_find_vqs->setup_vq,在setup_vp中通過IO端口和后端交互完成前面我們說的協(xié)議。看下該函數(shù)
注意協(xié)商的步驟,首先通過VIRTIO_PCI_QUEUE_SEL標(biāo)記本次操作的隊列索引,因為每個隊列都有自己的vring,即需要自己的共享內(nèi)存區(qū)。然后檢查隊列是否可用,這是通過VIRTIO_PCI_QUEUE_NUM,如果返回的結(jié)果是0,則表示沒有隊列可用,則返回錯誤。接著通過VIRTIO_PCI_QUEUE_PFN檢查是否已經(jīng)激活,如果已經(jīng)激活,同樣返回錯誤。這些檢查通過就可以予以初始化了,具體先分配一個中間結(jié)構(gòu)virtio_pci_vq_info,這不是重點,后面通過alloc_pages_exact向伙伴系統(tǒng)分配了不小于size的連續(xù)物理內(nèi)存,等會我們再說size的問題,然后把這塊物理頁框號(GPA>>VIRTIO_PCI_QUEUE_ADDR_SHIFT)寫入到VIRTIO_PCI_QUEUE_PFN,這樣后端就會得到這塊內(nèi)存區(qū)的信息。然后我們先看下前端利用這塊內(nèi)存區(qū)做了什么?看下面的vring_new_virtqueue函數(shù),該函數(shù)中調(diào)用vring_init來初始化vring
這個函數(shù)正好體現(xiàn)了我們前面那個結(jié)構(gòu)圖。這樣前端vring就初始化好了。對隊列填充數(shù)據(jù)時就是根據(jù)這個vring填充信息。
后端(qemu端)
主要操作都在virtio_ioport_write中,我們只關(guān)注三個case
可以看到在VIRTIO_PCI_QUEUE_SEL時候,僅僅是標(biāo)記了下設(shè)備中的queue_sel表示當(dāng)前操作的隊列索引。下面在通過VIRTIO_PCI_QUEUE_PFN傳遞地址的時候,調(diào)用virtio_queue_set_addr設(shè)置后端相關(guān)隊列的vring該函數(shù)實現(xiàn)較簡單
看到這里有么有很面熟,沒錯,這個函數(shù)和前端初始化vring的函數(shù)很是類似,這樣前后端的vring就同步起來了……
而在guest通知后端的時候,通過VIRTIO_PCI_QUEUE_NOTIFY接口,該函數(shù)調(diào)用了virtio_queue_notify_vq繼而調(diào)用 vq->handle_output……就這樣,后端就得到通知著手處理了!
后記:
到此,virtIO部分已經(jīng)分析的差不多了,分析期間真實感覺到了自己知識的匱乏,其間多次向開發(fā)者求助,并均得到認(rèn)真回復(fù),在此在此感謝這些優(yōu)秀的開發(fā)者。有時候看內(nèi)核代碼就感覺工程師和硬件在干仗,站在工程師的角度,需要盡其所能榨取硬件的性能。大到實現(xiàn)算法的優(yōu)化,小到分析程序執(zhí)行流的概率,從而針對編譯做優(yōu)化。站在硬件的角度,你處理不好,我就不給你工作。而從這方面,工程師自然是完勝,并且還在不遺余力的朝著勝利的另一個境界挺近,即征服硬件!哈哈,不過誰都知道,這是一場沒有勝負(fù)的戰(zhàn)爭,工程師自然優(yōu)秀,但是,因為工程師內(nèi)部的競爭,這樣戰(zhàn)斗將永無休止!!唉,瞎扯淡了,各位朋友,下篇文章見!
-
信號
+關(guān)注
關(guān)注
11文章
2841瀏覽量
77869 -
網(wǎng)絡(luò)
+關(guān)注
關(guān)注
14文章
7759瀏覽量
90310 -
數(shù)據(jù)包
+關(guān)注
關(guān)注
0文章
269瀏覽量
24866
原文標(biāo)題:virtIO前后端notify機(jī)制詳解
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
virtio I/O通信流程及設(shè)備框架的實現(xiàn)
請問小車轉(zhuǎn)向兩種方式有什么優(yōu)缺點?
SQL語言的兩種使用方式
電力操作電源兩種控制方式的比較

單片機(jī)常用的兩種延時控制方式

在MATLAB/simulink中建模時的兩種不同實現(xiàn)方式
MATLAB/simulink中兩種實現(xiàn)建模方式的優(yōu)勢
詳解PMSM中常用的兩種坐標(biāo)變換

springboot前后端交互流程
Linux應(yīng)用層控制外設(shè)的兩種不同的方式

eBPF技術(shù)實踐之virtio-net網(wǎng)卡隊列可觀測

評論