【原創】Linux虛擬化KVM-Qemu分析(十一)之virtqueue

背景

  • Read the fucking source code! --By 魯迅
  • A picture is worth a thousand words. --By 高爾基

說明:

  1. KVM版本:5.9.1
  2. QEMU版本:5.0.0
  3. 工具:Source Insight 3.5, Visio
  4. 文章同步在博客園: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負責組織:
    1. struct vring_desc:描述符表,每一項描述符指向一片內存,內存類型可以分爲out類型和in類型,分別代表輸出和輸入,而內存的管理都由驅動來負責。該結構體中的next字段,可用於將多個描述符構成一個描述符鏈,而flag字段用於描述屬性,比如只讀只寫等;
    2. struct vring_avail:可用描述符區域,用於記錄設備可用的描述符ID,它的主體是數組ring,實際就是一個環形緩衝區;
    3. 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 發送

當驅動需要把數據發送給設備時,流程如上圖所示:

  1. ①A表示分配一個Buffer並添加到Virtqueue中,①B表示從Used隊列中獲取一個Buffer,這兩種中選擇一種方式;
  2. ②表示將Data拷貝到Buffer中,用於傳送;
  3. ③表示更新Avail隊列中的描述符索引值,注意,驅動中需要執行memory barrier操作,確保Device能看到正確的值;
  4. ④與⑤表示Driver通知Device來取數據;
  5. ⑥表示Device從Avail隊列中獲取到描述符索引值;
  6. ⑦表示將描述符索引對應的地址中的數據取出來;
  7. ⑧表示Device更新Used隊列中的描述符索引;
  8. ⑨與⑩表示Device通知Driver數據已經取完了;

3.2 接收

當驅動從設備接收數據時,流程如上圖所示:

  1. ①表示Device從Avail隊列中獲取可用描述符索引值;
  2. ②表示將數據拷貝至描述符索引對應的地址上;
  3. ③表示更新Used隊列中的描述符索引值;
  4. ④與⑤表示Device通知Driver來取數據;
  5. ⑥表示Driver從Used隊列中獲取已用描述符索引值;
  6. ⑦表示將描述符索引對應地址中的數據取出來;
  7. ⑧表示將Avail隊列中的描述符索引值進行更新;
  8. ⑨與⑩表示Driver通知Device有新的可用描述符;

3.3 代碼分析

代碼的分析將圍繞下邊這個圖來展開(Virtio-Net),偷個懶,只分析單向數據發送了:

3.3.1 virtqueue創建

  • 之前的系列文章分析過virtio設備和驅動,Virtio-Net是PCI網卡設備驅動,分別會在virtnet-probevirtio_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

歡迎關注個人公衆號,不定期更新技術文章。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章