虛擬化篇之前後端驅動分析
前後端驅動是虛擬化的重要組成部分,在我們平時的排查過程中,經常會涉及到這部分的資料,特別是與效能相關的問題型別。舉個例子,我們經常會碰到網路抖動的問題,此時我們會在例項內部和後端vif口抓包,如果發現兩者之間存在延遲,經常我們就會懷疑到前後端的問題。因此我們需要對其工作原理和排查方法需要有一個全面的瞭解,其中也涉及到一些除錯技巧,如為了確定問題是否與前後端佇列有關,需要在例項系統的core dump內解析出記憶體中的佇列資料。
何為前後端:
說到前後端就要提到virtIO,virtIO是IBM提出的實現虛擬機器內部和宿主機之前資料交換的一種方式,與之前所謂全虛擬化方式比較即通過qemu在模擬裝置的方式,效能有了較大的提升。我們在本文中僅侷限於網路卡裝置,這也是因為在例項案例中網路部分佔了主導地位。簡單來講,在virtIO體系中分為前端驅動和後端驅動兩個部分,前端驅動我們一般可以理解為虛擬機器內部的虛擬網路卡的驅動,當然Windows和Linux的驅動是不同的,後端驅動virtIO是宿主機上的部分 的實現可能會有不同的方式,我們常見的是vhost-net,核心模式的vhost,至於其他模式如使用者態vhost、qemu等等又有不同,但是本質的功能是類似的,就是將前端驅動發出的報文轉發到NC虛擬交換機上,同理將收到的報文傳入例項內的前端驅動。
上面這張圖表示了前面驅動和後端驅動的關係。簡單來講前端驅動就是虛擬機器內的虛擬網路卡驅動,而後端驅動是主機上的vhost程式負責將報文轉發出來,或者將物理機上接受到的報文轉發進虛擬機器。這兩者其實就是負責了虛擬機器內外的資料交換。
前後端之間如何交換資料
總的來說兩者是通過vring、或者說virt queue即前後端環形佇列進行資料交換。一共存在三個佇列:
crash> struct vring
struct vring {
unsigned int num;
struct vring_desc *desc;
struct vring_avail *avail;
struct vring_used *used;
}
- desc佇列 – 放置了所有真正的報文資料。
- avail佇列與used佇列 – 在傳送報文的時候,前端驅動將報文在desc中的索引放在avail佇列中,後端驅動從這個佇列裡獲取報文進行轉發,處理完之後將這些報文放入used佇列。在接受報文的時候前端驅動將空白的記憶體塊放入avail佇列中(當然也只是報文在desc佇列中的索引而已),後端接受報文將內容填充後,將這些含資料的報文放入used佇列。
這三個佇列都是固定長度的環形佇列,當然實現僅僅是對相應索引號對最大長度去餘而已。下面這張圖形象地表明三個佇列和前後端驅動的關係:
主要的資料結構
我們以前端的傳送佇列為例,注意所有的結構資訊都是在虛擬機器內部可見的,可以通過core dump檢視:
struct vring_virtqueue {
vq = {
list = {
next = 0xffff881027e3d800,
prev = 0xffff881026d9b000
},
callback = 0xffffffffa0149450,
name = 0xffff881027e3ee88 “output.0”, ->>表明是傳送佇列
vdev = 0xffff881023776800,
priv = 0xffff8810237d03c0
},
vring = {
num = 256, ->>所有的佇列長度
desc = 0xffff881026d9c000, ->> desc佇列
avail = 0xffff881026d9d000, ->> avail佇列
used = 0xffff881026d9e000 ->> used佇列
},
broken = false,
indirect = true,
event = true,
num_free = 0, ->> 佇列目前有多少空閒元素了,如果已經為0表明佇列已經阻塞,前端將無法傳送報文給後端
free_head = 0, ->> 指向下一個空閒的desc元素
num_added = 0, ->>是最近一次操作向佇列中新增報文的數量
last_used_idx = 52143, 這是前端記錄他看到最新的被後端用過的索引(idx),是前端已經處理到的used佇列的idx。前端會把這個值寫到avail佇列的最後一個元素,這樣後端就可以得知前端已經處理到used佇列的哪一個元素了。
<> ->> last_avail_idx 前端不會碰,而且前端的virtqueue結構裡就沒有這個值,這個代表後端已經處理到avail佇列的哪個元素了,前端靠這個資訊來做限速,後端是把這個值寫在used佇列的最後一個元素,這樣前端就可以讀到了。
notify = 0xffffffffa005a350,
queue_index = 1,
data = 0xffff881026d9f078
}
crash> struct vring_avail 0xffff881026d9d000
struct vring_avail {
flags = 0,
idx = 52399, ->> avail佇列的下個可用元素的索引
ring = 0xffff881026d9d004 ->> 佇列陣列
}
crash> struct vring_used
struct vring_used {
__u16 flags;
__u16 idx; ->> used佇列的下個可用元素的索引
struct vring_used_elem ring[]; ->> 佇列陣列
}
報文傳送的具體流程
相比接受,報文傳送是我們處理案例中主要遇到問題的部分,所以我們將其流程單獨拿出來詳細分析一下。
主要以前端驅動(Linux版本)的行為為主,後端行為設計到阿里雲原始碼實現暫不做分析,但是從前端行為基本可以判斷後端的大致行為:
-
儲存head = vq->free_head。
-
首先為報文分配desc,即報文的描述塊,包含對映到記憶體的報文內容。
-
判斷佇列的num_free是否小於要傳送desc元素個數,如果是的話,說明佇列已經阻塞了,後端驅動無法及時處理,所以此時需要通知(notify)後端驅動,前端通知後端的方法就是寫入notification register暫存器。
-
調整num_free減去已經分配的desc元素數量。
-
調整free_head指向下一個空閒的desc元素。
-
計算本次應該用avail ring中的哪個元素(即得出元素索引),avail佇列是個環形陣列,這裡通過(vq->vring.avail->idx) & (vq->vring.num – 1)達到取餘的目的。
-
記錄本次邏輯buf的起始desc索引號,即將根據剛才得出的元素索引找到相應在avail中的元素,將該元素的值指向本次分配desc的元素。這樣處理之後avail佇列中就已經包含了要處理的報文了(當然只是指向desc的索引而已)
-
調整avail->idx指向了下一次操作使用avail佇列的哪個元素。
-
調整num_added記錄增加了幾個可用的avail ring元素。
-
根據skb->xmit_more的值來決定是否”kick”即通知(notify)後端驅動。xmit_more值代表是否後續還有更多的報文需要傳送,如果沒有,前端驅動就會決定kick,如果有前端驅動會繼續等待其他報文進入佇列後再一起”kick”。
-
在決定是否要notify後端驅動時,這裡有一個限速邏輯:
- 首先提取used佇列中最後一個元素,這是後端填入的資訊,表示後端驅動處理到哪個avail佇列的元素了,將值儲存到event_idx。
- 記錄上一次avail佇列idx索引的值到old。
- 記錄這一次報文進入佇列之後avail佇列idx索引的值到new_idx。
-
於是這裡有一個公式來最後決定是否要notify後端驅動,即所謂的限速邏輯:
(new_idx – event_idx – 1) < (new_idx – old)
用一張圖來表示這個限速邏輯:
第一種情況,當前後端處理速度很快,前端應當notify後端驅動:
第二種情況,後端處理速度跟不上前端傳送報文速度,暫時不要notify後端:
前端佇列的狀態分析
這裡介紹的主要是通過core dump分析前端佇列的方法,後端由於涉及到線上物理機,我們往往無法進行有效的分析。
Linux Core Dump
由於後端缺乏詳盡的日誌,我們往往需要依賴前端進行分析,而前後端佇列的狀態是在核心態,因此core dump是成了比較重要的分析手段了。以下介紹怎樣通過Linux Core Dump對前後端佇列進行分析:
首先通過net命令可以直接獲取所有net_device的地址:
crash> net
NET_DEVICE NAME IP ADDRESS(ES)
ffff881028c66020 lo 127.0.0.1
ffff8810225f5020 eth0 172.20.1.13
獲取其中的device地址:
crash> struct net_device ffff8810225f5020 -o | grep device
struct net_device {
[ffff8810225f50a0] struct net_device_stats stats;
[ffff8810225f5168] const struct net_device_ops *netdev_ops;
[ffff8810225f5198] struct net_device *master;
[ffff8810225f5408] struct net_device *link_watch_next;
[ffff8810225f5418] void (*destructor)(struct net_device *);
[ffff8810225f5450] struct device dev; —>> device地址
獲取其中的parent指標:
crash> struct device ffff8810225f5450 | grep parent
parent = 0xffff881023776810,
將結果減去10就是virtio_device結構:
crash> struct virtio_device ffff881023776800 -o
struct virtio_device {
[ffff881023776800] int index;
[ffff881023776804] bool config_enabled;
[ffff881023776805] bool config_change_pending;
[ffff881023776808] spinlock_t config_lock;
[ffff881023776810] struct device dev;
[ffff881023776a30] struct virtio_device_id id;
[ffff881023776a38] struct virtio_config_ops *config;
[ffff881023776a40] struct list_head vqs; —–>> 所有佇列的地址
[ffff881023776a50] unsigned long features[1];
[ffff881023776a58] void *priv;
列出所有佇列的地址:
crash> list ffff881023776a40
ffff881023776a40
ffff881026d9b000 ->> input.0 接受佇列
ffff881026d9f000 ->> output.0 傳送佇列
ffff881027e3d800 ->> control控制指令佇列
我們一般比較多關注傳送佇列,因此挑選傳送佇列來看:
crash> struct vring_virtqueue ffff881026d9f000
struct vring_virtqueue {
vq = {
list = {
next = 0xffff881027e3d800,
prev = 0xffff881026d9b000
},
callback = 0xffffffffa0149450,
name = 0xffff881027e3ee88 “output.0”,
vdev = 0xffff881023776800,
priv = 0xffff8810237d03c0
},
vring = {
num = 256,
desc = 0xffff881026d9c000,
avail = 0xffff881026d9d000,
used = 0xffff881026d9e000
},
broken = false,
indirect = true,
event = true,
num_free = 0, —–>> 表明佇列已滿
free_head = 0,
num_added = 0,
last_used_idx = 52143,
notify = 0xffffffffa005a350,
queue_index = 1,
data = 0xffff881026d9f078
}
當然還可以打出desc、avail和used每個陣列的情況。
相關文章
- 【原創】Linux虛擬化KVM-Qemu分析(十)之virtio驅動Linux
- 雲端自動化虛擬機器虛擬機
- 2022愛分析·虛擬化活動實踐報告
- 後端技術雜談6:白話虛擬化技術後端
- 【原創】Linux虛擬化KVM-Qemu分析(六)之中斷虛擬化Linux
- 【原創】Linux虛擬化KVM-Qemu分析(七)之timer虛擬化Linux
- 【原創】Linux虛擬化KVM-Qemu分析(四)之CPU虛擬化(2)Linux
- vmware虛擬網路卡驅動無法安裝
- 【原創】Linux虛擬化KVM-Qemu分析(二)之ARMv8虛擬化Linux
- 【原創】Linux虛擬化KVM-Qemu分析(五)之記憶體虛擬化Linux記憶體
- win10虛擬印表機驅動怎麼安裝_win10系統安裝虛擬印表機驅動教程Win10
- 2022愛分析· 虛擬化活動廠商全景報告 | 愛分析報告
- 虛擬化四、KVM虛擬化技術
- MySQL 驅動中虛引用 GC 耗時最佳化與原始碼分析MySqlGC原始碼
- 雲端計算導論 # 4 虛擬化
- 雲端計算與虛擬化之後:網路威脅成新挑戰
- 【原創】Linux虛擬化KVM-Qemu分析(一)Linux
- 資本襲來之前,中國的虛擬偶像之路
- 啥是伺服器虛擬化,虛擬化的優勢伺服器
- 伺服器虛擬化基礎知識:如何虛擬化?伺服器
- 淺談GPU虛擬化技術(四)- GPU分片虛擬化GPU
- 淺談GPU虛擬化技術(四)-GPU分片虛擬化GPU
- 恆訊科技分析伺服器虛擬化技術是啥?常見虛擬化架構有幾個?伺服器架構
- 【北京】虛擬幣企業招聘go高階後端工程師Go後端工程師
- 玩玩虛擬化-KVM
- 虛擬化-總覽
- 雲端計算虛擬化的優劣勢有哪些?
- 什麼是伺服器虛擬化,虛擬化的優勢!伺服器
- 淺談GPU虛擬化技術:GPU圖形渲染虛擬化GPU
- 使用驅動器中的光碟之前需要將其格式化怎麼辦?
- 網路虛擬化之linux虛擬網路基礎Linux
- SpringBoot系列之前後端介面安全技術JWTSpring Boot後端JWT
- 詳解Java 虛擬機器(第②篇)——HotSpot 虛擬機器物件Java虛擬機HotSpot物件
- KVM虛擬化新型漏洞CVE-2015-6815技術分析
- 【原創】Linux虛擬化KVM-Qemu分析(十一)之virtqueueLinux
- linux核心匯流排驅動模型-驅動篇Linux模型
- 硬碟使用驅動器中的光碟之前需要將其格式化修復方法?硬碟
- 虛擬機器去虛擬化過魯大師教程虛擬機