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

LoyenWang發表於2021-03-28

背景

  • 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

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

相關文章