背景
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
歡迎關注個人公眾號,不定期更新技術文章。