背景
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 Device和Virtio Driver都已經講完,本文將分析virtqueue;
- virtqueue用於前後端之間的資料交換,一看到這種資料佇列,首先想到的就是ring-buffer,實際的實現會是怎麼樣的呢?
2. 資料結構
先看一下核心的資料結構:
- 通常Virtio裝置操作Virtqueue時,都是通過
struct virtqueue
結構體,這個可以理解成對外的一個介面,而Virtqueue
機制的實現依賴於struct vring_virtqueue
結構體; Virtqueue
有三個核心的資料結構,由struct vring
負責組織:struct vring_desc
:描述符表,每一項描述符指向一片記憶體,記憶體型別可以分為out型別和in型別,分別代表輸出和輸入,而記憶體的管理都由驅動來負責。該結構體中的next欄位,可用於將多個描述符構成一個描述符鏈,而flag欄位用於描述屬性,比如只讀只寫等;struct vring_avail
:可用描述符區域,用於記錄裝置可用的描述符ID,它的主體是陣列ring,實際就是一個環形緩衝區;struct vring_used
:已用描述符區域,用於記錄裝置已經處理完的描述符ID,同樣,它的ring陣列也是環形緩衝區,與struct vring_avail
不同的是,它還記錄了裝置寫回的資料長度;
這麼看,當然是有點不太直觀,所以,下圖來了:
- 簡單來說,驅動會分配好記憶體(
scatterlist
),並通過virtqueue_add
新增到描述表中,這樣描述符表中的條目就都能對應到具體的實體地址了,其實可以把它理解成一個資源池子; - 驅動可以將可用的資源更新到
struct vring_avail
中,也就是將可用的描述符ID新增到ring陣列中,熟悉環形緩衝區的同學應該清楚它的機制,通過維護頭尾兩個指標來進行管理,Driver負責更新頭指標(idx),Device負責更新尾指標(Qemu中的Device負責維護一個last_avail_idx),頭尾指標,你追我趕,生生不息; - 當裝置使用完了後,將已用的描述符ID更新到
struct vring_used
中,vring_virtqueue
自身維護了last_used_idx,機制與struct vring_avail
一致;
3. 流程分析
3.1 傳送
當驅動需要把資料傳送給裝置時,流程如上圖所示:
- ①A表示分配一個Buffer並新增到Virtqueue中,①B表示從Used佇列中獲取一個Buffer,這兩種中選擇一種方式;
- ②表示將Data拷貝到Buffer中,用於傳送;
- ③表示更新Avail佇列中的描述符索引值,注意,驅動中需要執行memory barrier操作,確保Device能看到正確的值;
- ④與⑤表示Driver通知Device來取資料;
- ⑥表示Device從Avail佇列中獲取到描述符索引值;
- ⑦表示將描述符索引對應的地址中的資料取出來;
- ⑧表示Device更新Used佇列中的描述符索引;
- ⑨與⑩表示Device通知Driver資料已經取完了;
3.2 接收
當驅動從裝置接收資料時,流程如上圖所示:
- ①表示Device從Avail佇列中獲取可用描述符索引值;
- ②表示將資料拷貝至描述符索引對應的地址上;
- ③表示更新Used佇列中的描述符索引值;
- ④與⑤表示Device通知Driver來取資料;
- ⑥表示Driver從Used佇列中獲取已用描述符索引值;
- ⑦表示將描述符索引對應地址中的資料取出來;
- ⑧表示將Avail佇列中的描述符索引值進行更新;
- ⑨與⑩表示Driver通知Device有新的可用描述符;
3.3 程式碼分析
程式碼的分析將圍繞下邊這個圖來展開(Virtio-Net
),偷個懶,只分析單向資料傳送了:
3.3.1 virtqueue建立
- 之前的系列文章分析過virtio裝置和驅動,Virtio-Net是PCI網路卡裝置驅動,分別會在
virtnet-probe
和virtio_pci_probe
中完成所有的初始化; virtnet_probe
函式入口中,通過init_vqs
完成Virtqueue的初始化,這個逐級呼叫關係如圖所示,最終會呼叫到vring_create_virtqueue
來建立Virtqueue;- 這個建立的過程中,有些細節是忽略的,比如通過PCI去讀取裝置的配置空間,獲取建立Virtqueue所需要的資訊等;
- 最終就是圍繞
vring_virtqueue
資料結構的初始化展開,其中vring資料結構的記憶體分配也都是在驅動中完成,整個結構體都由驅動來管理與維護;
3.3.2 virtio-net驅動傳送
- 網路資料的傳輸在驅動中通過
start_xmit
函式來實現; xmit_skb
函式中,sg_init_table
初始化sg列表,sg_set_buf
將sg指向特定的buffer,skb_to_sgvec
將socket buffer中的資料填充sg;- 通過
virtqueue_add_outbuf
將sg新增到Virtqueue中,並更新Avail佇列中描述符的索引值; virtqueue_notify
通知Device,可以過來取資料了;
3.3.3 Qemu virtio-net裝置接收
- Guest驅動寫暫存器操作時,陷入到KVM中,最終Qemu會捕獲到進行處理,入口函式為
kvm_handle_io
; - Qemu中會針對IO記憶體區域設定讀寫的操作函式,當Guest進行IO操作時,最終觸發操作函式的呼叫,針對Virtio-Net,由於它是PCI裝置,操作函式為
virtio_pci_config_write
; virtio_pci_config_write
函式中,對Guest的寫操作進行判斷並處理,比如在VIRTIO_PCI_QUEUE_NOTIFY
時,呼叫virtio_queue_notify
,用於處理Guest驅動的通知,並最終回撥handle_output
函式;- 針對Virtio-Net裝置,傳送的回撥函式為
virtio_net_handle_tx_bh
,並在virtio_net_flush_tx
中完成操作; - 通用的操作模型:通過
virtqueue_pop
從Avail佇列中獲取地址,將資料進行處理,通過virtqueue_push
將處理完後的描述符索引更新到Used佇列中,通過virtio_notify
通知Guest驅動;
Virtqueue這種設計思想比較巧妙,不僅用在virtio中,在AMP系統中處理器之間的通訊也能看到它的身影。
草草收場了,下回見。
參考
https://www.redhat.com/en/blog/virtqueues-and-virtio-ring-how-data-travels
Virtual I/O Device Version 1.1
歡迎關注個人公眾號,不定期更新技術文章。