精品国产人成在线_亚洲高清无码在线观看_国产在线视频国产永久2021_国产AV综合第一页一个的一区免费影院黑人_最近中文字幕MV高清在线视频

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

SPDK在虛擬化場(chǎng)景下的使用方法

科技綠洲 ? 來(lái)源:Linux開(kāi)發(fā)架構(gòu)之路 ? 作者:Linux開(kāi)發(fā)架構(gòu)之路 ? 2023-11-10 10:12 ? 次閱讀

一.概述

隨著越來(lái)越多公有云服務(wù)提供商采用SPDK技術(shù)作為其高性能云存儲(chǔ)的核心技術(shù)之一,intel推出的SPDK技術(shù)備受業(yè)界關(guān)注。本篇博文就和大家一起探索SPDK。

什么是SPDK?為什么需要它?

SPDK(全稱Storage Performance Development Kit),提供了一整套工具和庫(kù),以實(shí)現(xiàn)高性能、擴(kuò)展性強(qiáng)、全用戶態(tài)的存儲(chǔ)應(yīng)用程序。它是繼DPDK之后,intel在存儲(chǔ)領(lǐng)域推出的又一項(xiàng)顛覆性技術(shù),旨在大幅縮減存儲(chǔ)IO棧的軟件開(kāi)銷,從而提升存儲(chǔ)性能,可以說(shuō)它就是為了存儲(chǔ)性能而生。

為便于大家理解,我們先介紹一下SPDK在虛擬化場(chǎng)景下的使用方法,以給大家一些直觀的認(rèn)識(shí)。

  1. DPDK的編譯與安裝

SPDK使用了DPDK中一些通用的功能和機(jī)制,因此首先需要下載DPDK的源碼并完成編譯和安裝:

[root@linux:~/DPDK]# make config T=x86_64-native-linuxapp-gcc
[root@linux:~/DPDK]# make
[root@linux:~/DPDK]# make install (默認(rèn)安裝到/usr/local,包括.a庫(kù)文件和頭文件)
  1. SPDK的編譯
[root@linux:~/SPDK]# ./configure –with-dpdk=/usr/local
[root@linux:~/SPDK]# make

編譯成功后,我們?cè)趕pdk/app/vhost目錄下可以看到一個(gè)名為vhost的可執(zhí)行文件,它就是SPDK在虛擬化場(chǎng)景下為虛擬機(jī)模擬程序qemu提供的存儲(chǔ)轉(zhuǎn)發(fā)服務(wù),借此為虛擬機(jī)用戶帶來(lái)高性能的虛擬磁盤。

  1. 大頁(yè)內(nèi)存配置

SPDK vhost進(jìn)程和qemu進(jìn)程通過(guò)大頁(yè)共享虛擬機(jī)可見(jiàn)內(nèi)存,因此需要進(jìn)行一些大頁(yè)的配置和調(diào)整:

  • 可通過(guò)設(shè)置/sys/kernel/mm/hugepages/hugepages-xxx/nr_hugepages來(lái)調(diào)整大頁(yè)數(shù)量(xxx通常為2M或1G)
  • qemu使用掛載到/dev/hugepages目錄下的hugetlbfs來(lái)使用大頁(yè)內(nèi)存,可在掛載參數(shù)中指定大頁(yè)大小,如mount -t hugetlbfs -o pagesize=1G nodev /dev/hugepages
  1. vhost配置與啟動(dòng)
[root@linux:~/SPDK]# HUGEMEM=4096 scripts/setup.sh
[root@linux:~/SPDK]# app/vhost/vhost -S /var/tmp -m 0x3 -c etc/spdk/rootw.conf

vhost命令執(zhí)行過(guò)程中,是一個(gè)常駐的服務(wù)進(jìn)程;-S參數(shù)指定了socket文件的生成的目錄,每個(gè)虛擬磁盤(vhost-blk)或虛擬存儲(chǔ)控制器(vhost-scsi)都會(huì)在該目錄下產(chǎn)生一個(gè)socket文件,以便qemu程序與vhost進(jìn)程建立連接;-m參數(shù)指定了vhost進(jìn)程中的輪循線程所綁定的物理CPU核,例如0x3代表在0號(hào)和1號(hào)核上各綁定一個(gè)輪循線程;-c參數(shù)指定了vhost進(jìn)程所需的配置文件,例如這里我通過(guò)內(nèi)存設(shè)備(SPDK中稱之為Malloc設(shè)備)提供了一個(gè)vhost-blk磁盤:

[root@linux: /SPDK]# cat etc/spdk/rootw.conf
[root@linux:
/SPDK]#
[Malloc]NumberOfLuns 1 #創(chuàng)建一個(gè)內(nèi)存設(shè)備,默認(rèn)名稱為Malloc0
LunSizeInMB 128 #該內(nèi)存設(shè)備大小為128M
BlockSize 4096 #該內(nèi)存設(shè)備塊大小為4096字節(jié)
[VhostBlk0]Name vhost.2 #創(chuàng)建一個(gè)vhost-blk設(shè)備,名稱為vhost.2
Dev Malloc0 #該設(shè)備后端對(duì)應(yīng)的物理設(shè)備為Malloc0
Cpumask 0x1 #將該設(shè)備綁定到0號(hào)核的輪循線程上

  1. 虛擬機(jī)啟動(dòng)與驗(yàn)證

vhost進(jìn)程啟動(dòng)后,我們就可以拉起qemu進(jìn)程來(lái)啟動(dòng)一個(gè)新虛擬機(jī),qemu進(jìn)程的命令行參數(shù)如下(重點(diǎn)關(guān)注與SPDK vhost相關(guān)部分):

[root@linux:~/qemu]# ./x86_64-softmmu/qemu-system-x86_64 -name rootw-vm -machine pc-i440fx-2.6,accel=kvm
-m 1G -object memory-backend-file,id=mem,size=1G,mem-path=/dev/hugepages,share=on -numa node,memdev=mem
-drive file=/mnt/centos.qcow2,format=qcow2,id=virtio-disk0,cache=none,aio=native -device virtio-blk-pci,drive=virtio-disk0,id=blk0
-chardev socket,id=char_rootw,path=/var/tmp/vhost.2 -device vhost-user-blk-pci,id=blk_rootw,chardev=char_rootw
-vnc 0.0.0.0:0

通過(guò)上述啟動(dòng)參數(shù),我們可以看出:

  • vhost進(jìn)程和qemu進(jìn)程通過(guò)大頁(yè)方式共享虛擬機(jī)可見(jiàn)的所有內(nèi)存(原因我們將在深入分析時(shí)討論)
  • qemu在配置vhost-user-blk-pci設(shè)備時(shí),只需要指定vhost生成的socket文件即可(-S參數(shù)指定的路徑后拼接上設(shè)備名稱)

虛擬機(jī)啟動(dòng)成功后,我們通過(guò)vnc工具登陸虛擬機(jī),執(zhí)行l(wèi)sblk命令可以查看到vda和vdb兩個(gè)virtio-blk塊設(shè)備,表明vhost后端已成功生效。這里要說(shuō)明一下,qemu中配置的virtio-blk-pci設(shè)備、vhost-user-blk-pci設(shè)備或vhost-blk-pci設(shè)備,在虛擬機(jī)內(nèi)部均呈現(xiàn)為virtio-blk-pci設(shè)備,因此在虛擬機(jī)中采用相同的virtio-blk-pci和virtio-blk驅(qū)動(dòng)進(jìn)行使能,如此一來(lái)不同的后端實(shí)現(xiàn)技術(shù)在虛擬機(jī)內(nèi)部均采用一套驅(qū)動(dòng),可以減少驅(qū)動(dòng)的開(kāi)發(fā)和維護(hù)工作量。

如何實(shí)現(xiàn)SPDK?

SPDK能實(shí)現(xiàn)高性能,得益于以下三個(gè)關(guān)鍵技術(shù):

  • 全用戶態(tài),它把所有必要的驅(qū)動(dòng)全部移到了用戶態(tài),避免了系統(tǒng)調(diào)用的開(kāi)銷并真正實(shí)現(xiàn)內(nèi)存零拷貝
  • 輪循模式,針對(duì)高速物理存儲(chǔ)設(shè)備,采用輪循的方式而非中斷通知方式判斷請(qǐng)求完成,大大降低時(shí)延并減少性能波動(dòng)
  • 無(wú)鎖機(jī)制,在IO路徑上避免采用任何鎖機(jī)制進(jìn)行同步,降低時(shí)延并提升吞吐量

下面我們將深入到SPDK的實(shí)現(xiàn)細(xì)節(jié),去看看這些關(guān)鍵點(diǎn)分別是如何提升性能的。

  1. 整體架構(gòu)

首先,我們來(lái)了解一下SPDK內(nèi)部的整體組件架構(gòu):

圖片

SPDK整體分為三層:

  • 存儲(chǔ)協(xié)議層(Storage Protocols),指SPDK支持存儲(chǔ)應(yīng)用類型。iSCSI Target對(duì)外提供iSCSI服務(wù),用戶可以將運(yùn)行SPDK服務(wù)的主機(jī)當(dāng)前標(biāo)準(zhǔn)的iSCSI存儲(chǔ)設(shè)備來(lái)使用;vhost-scsi或vhost-blk對(duì)qemu提供后端存儲(chǔ)服務(wù),qemu可以基于SPDK提供的后端存儲(chǔ)為虛擬機(jī)掛載virtio-scsi或virtio-blk磁盤;NVMF對(duì)外提供基于NVMe協(xié)議的存儲(chǔ)服務(wù)端。注意,圖中vhost-blk在spdk-18.04版本中已實(shí)現(xiàn),后面我們主要基于此版本進(jìn)行代碼分析。
  • 存儲(chǔ)服務(wù)層(Storage Services),該層實(shí)現(xiàn)了對(duì)塊和文件的抽象。目前來(lái)說(shuō),SPDK主要在塊層實(shí)現(xiàn)了QoS特性,這一層整體上還是非常薄的。
  • 驅(qū)動(dòng)層(drivers),這一層實(shí)現(xiàn)了存儲(chǔ)服務(wù)層定義的抽象接口,以對(duì)接不同的存儲(chǔ)類型,如NVMe,RBD,virtio,aio等等。圖中把驅(qū)動(dòng)細(xì)分成兩層,和塊設(shè)備強(qiáng)相關(guān)的放到了存儲(chǔ)服務(wù)層,而把和硬件強(qiáng)相關(guān)部分放到了驅(qū)動(dòng)層。
  1. 深入數(shù)據(jù)面

接下來(lái)我們將以SPDK前端配置成vhost-blk、后端配置成NVMe SSD場(chǎng)景為例,來(lái)分析整個(gè)數(shù)據(jù)面流程。我們將分兩部分完成數(shù)據(jù)面的分析:

  • IO棧對(duì)比與線程模型
  • IO流程代碼解析
  1. 深入管理面

管理面流程比數(shù)據(jù)面要復(fù)雜得多,也無(wú)趣得多。因此我們?cè)诜治鐾陻?shù)據(jù)面流程之后,再回頭看看數(shù)據(jù)面中涉及的各個(gè)對(duì)象分別是如何被創(chuàng)建和初始化的,這樣更利于我們理解這樣做的目的,也不會(huì)一下子就被這些復(fù)雜的流程嚇住而無(wú)法堅(jiān)持往下分析。

整個(gè)管理面功能包含vhost啟動(dòng)初始化和通過(guò)rpc動(dòng)態(tài)管理兩個(gè)部分,這里我們主要討化啟動(dòng)初始化,根據(jù)啟動(dòng)時(shí)的先后順序,分為

  • reactor線程初始化
  • bdev子系統(tǒng)初始化
  • vhost子系統(tǒng)初始化
  • vhost客戶端(qemu)連接請(qǐng)求處理

【SPDK】二、IO棧對(duì)比與線程模型

這里我們以SPDK前端配置成vhost-blk、后端配置成NVMe SSD場(chǎng)景為例,來(lái)分析SPDK的IO棧和線程模型。

IO棧對(duì)比與時(shí)延分析

我們先來(lái)對(duì)比一下qemu使用普通內(nèi)核NVMe驅(qū)動(dòng)和使用SPDK vhost時(shí)IO棧的差別,如下圖所示:

圖片

編輯切換為居中

添加圖片注釋,不超過(guò) 140 字(可選)

無(wú)論使用傳統(tǒng)內(nèi)核NVMe驅(qū)動(dòng),還是使用vhost,虛擬機(jī)內(nèi)部的IO處理流程都是一樣的:IO請(qǐng)求下發(fā)時(shí)需要從用戶態(tài)應(yīng)用程序中切換到內(nèi)核態(tài),并穿過(guò)文件系統(tǒng)和virtio-blk驅(qū)動(dòng)后,才能借助IO環(huán)(IO Ring)將請(qǐng)求信息傳遞給虛擬設(shè)備進(jìn)行處理;虛擬設(shè)備處理完成后,以中斷方式通知虛擬機(jī),虛擬機(jī)內(nèi)進(jìn)過(guò)驅(qū)動(dòng)和文件系統(tǒng)的回調(diào)后,最終喚醒應(yīng)用程序返回用戶態(tài)繼續(xù)執(zhí)行業(yè)務(wù)邏輯。在intel Xeon E5620@2.4GHz服務(wù)器上的測(cè)試結(jié)果表明,虛擬機(jī)內(nèi)部的請(qǐng)求下發(fā)與響應(yīng)處理總時(shí)延約15us。

針對(duì)傳統(tǒng)內(nèi)核NVMe驅(qū)動(dòng),qemu進(jìn)程中io線程負(fù)責(zé)處理虛擬機(jī)下發(fā)的IO請(qǐng)求:它通過(guò)virtio backend從IO環(huán)中取出請(qǐng)求,并將請(qǐng)求通過(guò)系統(tǒng)調(diào)用傳遞給內(nèi)核塊層和NVMe驅(qū)動(dòng)層進(jìn)行處理,最后由NVMe驅(qū)動(dòng)將請(qǐng)求通過(guò)Queue Pair(類似IO環(huán))交由物理NVMe控制器進(jìn)行處理;NVMe控制器處理完成后以物理中斷方式通知qemu io線程,由它將響應(yīng)放入虛擬機(jī)IO環(huán)中并以虛擬中斷通知虛擬機(jī)請(qǐng)求完成。在此我們看到,qemu中總共的處理時(shí)延約15us,而NVMe硬件(華為ES3000 NVMe SSD)上的處理時(shí)延才10us(讀請(qǐng)求)。

針對(duì)SPDK vhost,qemu進(jìn)程不參與IO請(qǐng)求的處理(僅在初始化時(shí)起作用),所有虛擬機(jī)下發(fā)的IO請(qǐng)求均由vhost進(jìn)程處理。vhost進(jìn)程以輪循的方式不斷從IO環(huán)中取出請(qǐng)求(意味著虛擬機(jī)下發(fā)IO請(qǐng)求時(shí),不用通知虛擬設(shè)備),對(duì)于取出的每個(gè)請(qǐng)求,vhost將其以任務(wù)方式交給bdev抽象層進(jìn)行處理;bdev根據(jù)后端設(shè)備的類型來(lái)選擇不同的驅(qū)動(dòng)進(jìn)行處理,例如對(duì)于NVMe設(shè)備,將使用用戶態(tài)的NVMe驅(qū)動(dòng)在用戶空間完成對(duì)Queue Pair的操作。vhost進(jìn)程同樣會(huì)輪循物理NVMe設(shè)備的Queue Pair,如果有響應(yīng)例會(huì)立刻進(jìn)行處理,而無(wú)須等待物理中斷。vhost在處理NVMe響應(yīng)過(guò)程中,會(huì)向虛擬機(jī)IO環(huán)中添加響應(yīng),并以虛擬中斷方式通知虛擬機(jī)。我們可以看到,vhost中絕大部分操作都是在用戶態(tài)完成的(中斷通知虛擬機(jī)時(shí)會(huì)進(jìn)入內(nèi)核態(tài)通過(guò)KVM模塊完成),各層時(shí)延均非常短,app和bdev抽象層約2us,NVMe用戶態(tài)驅(qū)動(dòng)約2us。

因此,端到端時(shí)延對(duì)比來(lái)看,我們可以發(fā)現(xiàn)傳統(tǒng)NVMe IO棧的總時(shí)延約40us,而SPDK用戶態(tài)NVMe IO棧時(shí)延不到30us,時(shí)延上有25%以上的優(yōu)化。另一方面,在吞吐量(IOPS)方面,如果我們給virtio-blk設(shè)備配置多隊(duì)列(確保虛擬機(jī)IO壓力足夠),并在后端NVMe設(shè)備不成為瓶頸的前提下,傳統(tǒng)NVMe IO棧在單個(gè)qemu io線程處理時(shí),最多能達(dá)到20萬(wàn)IOPS,而SPDK vhost在單線程處理時(shí)可達(dá)100萬(wàn)IOPS,同等CPU開(kāi)銷下,吞吐量上有5倍以上的性能提升。傳統(tǒng)NVMe IO棧在處理多隊(duì)列模型時(shí),相比單隊(duì)列模型,減少了線程間通知開(kāi)銷,一次通知可以處理多個(gè)IO請(qǐng)求,因此多隊(duì)列相比單隊(duì)列模型會(huì)有較大的IOPS提升;而vhost得益于全用戶態(tài)及輪循模式,進(jìn)一步減少了內(nèi)核切換和通知開(kāi)銷,帶來(lái)了吞吐量的大幅提升。

線程模型分析

在了解了SPDK的IO棧之后,我們進(jìn)一步來(lái)分析一下vhost進(jìn)程的線程模型,如下圖所示。圖中示例場(chǎng)景為,一臺(tái)服務(wù)器上插了一張NVMe SSD卡,卡上劃分了三個(gè)namespace;三個(gè)namespace分別配給了三臺(tái)虛擬機(jī)的vhost-user-blk-pci設(shè)備。

圖片

vhost進(jìn)程啟動(dòng)時(shí)可以配置多個(gè)輪循線程(reactor),每個(gè)線程綁定一個(gè)物理CPU。在示例場(chǎng)景下,我們假設(shè)配置了兩個(gè)輪循線程reactor_0和reactor_1,分別對(duì)應(yīng)物理CPU0和物理CPU1。每配置一個(gè)vhost-blk設(shè)備時(shí),同樣要為該設(shè)備綁定物理核,并且只能綁定到一個(gè)物理核上,例如這里我們假設(shè)vm1的vhost-blk設(shè)備綁定到CPU0,vm2和vm3綁定到CPU1。那么reactor_0將輪循vm1中vhost-blk的IO環(huán),reactor_1將依次輪循vm2和vm3的IO環(huán)。

vhost線程在操作相同NVMe控制器下的namespace時(shí),不同的vhost線程會(huì)申請(qǐng)不同的IO Channel(實(shí)際對(duì)應(yīng)NVMe Queue Pair,作用類似虛擬機(jī)IO環(huán)),并且每個(gè)線程都會(huì)輪循各自申請(qǐng)的IO Channel中的響應(yīng)消息。例如圖中reactor_0會(huì)向NVMe控制器申請(qǐng)QueuePair1,并在輪循過(guò)程中注冊(cè)對(duì)該QueuePair的poller函數(shù)(負(fù)責(zé)從中取響應(yīng));reactor_1則會(huì)向NVMe控制器申請(qǐng)QueuePair2并輪循該QueuePair。如此一來(lái),就能提升對(duì)后端NVMe設(shè)備的并發(fā)訪問(wèn)度,充分發(fā)揮物理設(shè)備的吞吐量?jī)?yōu)勢(shì)。

綜上所述,

  • 每個(gè)vhost線程都會(huì)輪循若干個(gè)vhost設(shè)備的IO環(huán)(一個(gè)vhost設(shè)備無(wú)論有多少個(gè)環(huán),都只會(huì)在一個(gè)線程中處理),并且會(huì)向有操作述求的物理存儲(chǔ)控制器(例如NVMe控制器、virtio-blk控制器、virtio-scsi控制器等)申請(qǐng)一個(gè)獨(dú)立的IO Channel(IO環(huán)可以理解為對(duì)前端虛擬機(jī)呈現(xiàn)的一個(gè)IO Channel)并對(duì)其進(jìn)行輪循。
  • 無(wú)論是前端虛擬機(jī)IO環(huán),還是后端IO Channel,都只會(huì)在一個(gè)vhost線程中被輪循,因此這就避免了多線程并發(fā)操作同一個(gè)對(duì)象,可以通過(guò)無(wú)鎖的方式操作IO環(huán)或IO Channel。
  • 針對(duì)前端虛擬機(jī)來(lái)說(shuō),一個(gè)vhost設(shè)備無(wú)論有多少個(gè)環(huán),都只會(huì)在一個(gè)vhost線程中處理。這種設(shè)計(jì)上的約束雖說(shuō)可以簡(jiǎn)化實(shí)現(xiàn),但也帶來(lái)了吞吐量性能擴(kuò)展上的限制,即一個(gè)vhost設(shè)備在后端物理存儲(chǔ)非瓶頸的前提下,最高的IOPS為100萬(wàn)。因此我們可以考慮將vhost的多個(gè)IO環(huán)拆分到多個(gè)vhost線程中處理,進(jìn)一步提升吞吐量。

【SPDK】三、IO流程代碼解析

在分析SPDK數(shù)據(jù)面代碼之前,需要我們對(duì)qemu中實(shí)現(xiàn)的IO環(huán)以及virtio前后端驅(qū)動(dòng)的實(shí)現(xiàn)有所了解(后續(xù)我計(jì)劃出專門的博文來(lái)介紹qemu)。這里我們?nèi)砸許PDK前端配置vhost-blk,后端對(duì)接NVMe SSD為例(有關(guān)NVMe驅(qū)動(dòng)涉及較多規(guī)范細(xì)節(jié),這里也不作過(guò)于深入的討論,感興趣的讀者可以結(jié)合NVMe規(guī)范展開(kāi)閱讀)進(jìn)行分析。

總流程

前文在分析SPDK IO棧時(shí)已經(jīng)大致分析了IO處理的調(diào)用層次,在此我們進(jìn)一步打開(kāi)內(nèi)部實(shí)現(xiàn)細(xì)節(jié),更細(xì)致地分析一下IO處理流程:

圖片

首先,從虛擬機(jī)視角來(lái)說(shuō),它看到的是一個(gè)virtio-blk-pci設(shè)備,該pci設(shè)備內(nèi)部包含一條virtio總線,其上又連接了virtio-blk設(shè)備。qemu在對(duì)虛擬機(jī)用戶呈現(xiàn)這個(gè)virtio-blk-pci設(shè)備時(shí),采用的具體設(shè)備類型是vhost-user-blk-pci(這是virtio-blk-pci設(shè)備的一種后端實(shí)現(xiàn)方式。另外兩種是:vhost-blk-pci,由內(nèi)核實(shí)現(xiàn)后端;普通virtio-blk-pci,由qemu實(shí)現(xiàn)后端處理),這樣便可與用戶態(tài)的SPDK vhost進(jìn)程建立連接。SPDK vhost進(jìn)程內(nèi)部對(duì)于虛擬機(jī)所見(jiàn)的virtio-blk-pci設(shè)備也有一個(gè)對(duì)象來(lái)表示它,這就是spdk_vhost_blk_dev。該對(duì)象指向一個(gè)bdev對(duì)象和一個(gè)io channel對(duì)象,bdev對(duì)象代表真正的后端塊存儲(chǔ)(這里對(duì)應(yīng)NVMe SSD上的一個(gè)namespace),io channel代表當(dāng)前線程訪問(wèn)存儲(chǔ)的獨(dú)立通道(對(duì)應(yīng)NVMe SSD的一個(gè)Queue Pair)。這兩個(gè)對(duì)象在驅(qū)動(dòng)層會(huì)進(jìn)一步擴(kuò)展新的成員變量,用來(lái)表示驅(qū)動(dòng)層可見(jiàn)的一些詳細(xì)信息。

其次,當(dāng)虛擬機(jī)往IO環(huán)中放入IO請(qǐng)求后,便立刻被vhost進(jìn)程中的某個(gè)reactor線程輪循到該請(qǐng)求(輪循過(guò)種中執(zhí)行函數(shù)為vdev_worker)。reactor線程取出請(qǐng)求后,會(huì)將其映成一個(gè)任務(wù)(spdk_vhost_blk_task)。對(duì)于讀寫請(qǐng)求,會(huì)進(jìn)一步走到bdev層,將任務(wù)封狀成一個(gè)bdev_io對(duì)象(類似內(nèi)核的bio)。bdev_io繼續(xù)往驅(qū)動(dòng)層遞交,它會(huì)擴(kuò)展為適配具體驅(qū)動(dòng)的io對(duì)象,例如針對(duì)NVMe驅(qū)動(dòng),bdev_io將擴(kuò)展成nvme_bdev_io對(duì)象。NVMe驅(qū)動(dòng)會(huì)根據(jù)nvme_bdev_io對(duì)象中的請(qǐng)求內(nèi)容在當(dāng)前reactor線程對(duì)應(yīng)的QueuePair中生成一個(gè)新的請(qǐng)求項(xiàng),并通知NVMe控制器有新的請(qǐng)求產(chǎn)生。

最后,當(dāng)物理NVMe控制器完成IO請(qǐng)求后,會(huì)往QueuePair中添加IO響應(yīng)。該響應(yīng)信息也會(huì)很快被reactor線程輪循到(輪循執(zhí)行函數(shù)為bdev_nvme_poll)。reactor取出響應(yīng)后,根據(jù)其id找到對(duì)應(yīng)的nvme_bdev_io,進(jìn)一步關(guān)聯(lián)到對(duì)應(yīng)的bdev_io,再調(diào)用bdev_io中的記錄的回調(diào)函數(shù)。vhost-blk下發(fā)請(qǐng)求時(shí)注冊(cè)的回調(diào)函數(shù)為blk_request_complete_cb,回調(diào)參數(shù)為當(dāng)前的spdk_vhost_blk_task對(duì)象。在blk_request_complete_cb中會(huì)往虛擬機(jī)IO環(huán)中放入IO響應(yīng),并通過(guò)虛擬中斷通知虛擬機(jī)IO完成。

IO請(qǐng)求下發(fā)流程代碼解析

vhost進(jìn)程通過(guò)vdev_worker函數(shù)以輪循方式處理虛擬機(jī)下發(fā)的IO請(qǐng)求,調(diào)用棧如下:

1 vdev_worker()
2 -process_vq()
3 |-spdk_vhost_vq_avail_ring_get()
4 -process_blk_request()
5 |-blk_iovs_setup()
6 -spdk_bdev_readv()/spdk_bdev_writev()
7 -spdk_bdev_io_submit()
8 -bdev->fn_table->submit_request()

下面我們先來(lái)分析一下vhost-blk層的具體代碼實(shí)現(xiàn):

spdk/lib/vhost/vhost-blk.c:

1 /* reactor線程會(huì)采用輪循方式周期性地調(diào)用vdev_worker函數(shù)來(lái)處理虛擬機(jī)下發(fā)的請(qǐng)求 */
2 static int
3 vdev_worker(void *arg)
4 {
5 /* arg在注冊(cè)輪循函數(shù)時(shí)指定,代表當(dāng)前操作的vhost-blk對(duì)象 */
6 struct spdk_vhost_blk_dev *bvdev = arg;
7 uint16_t q_idx;
8
9 /* vhost-blk對(duì)象bvdev中含有一個(gè)抽象的spdk_vhost_dev對(duì)象,其內(nèi)部記錄所有vhost_dev類別對(duì)象
10 均含有的公共內(nèi)容,max_queues代表當(dāng)前vhost_dev對(duì)象共有多少個(gè)IO環(huán),virtqueue[]數(shù)組記錄了
11 所有的IO環(huán)信息 */
12 for (q_idx = 0; q_idx < bvdev->vdev.max_queues; q_idx++) {
13 /* 根據(jù)IO環(huán)的個(gè)數(shù),依次處理每個(gè)環(huán)中的請(qǐng)求 */
14 process_vq(bvdev, &bvdev->vdev.virtqueue[q_idx]);
15 }
16
17 ...
18
19 }
20
21 /* 處理IO環(huán)中的所有請(qǐng)求 */
22 static void
23 process_vq(struct spdk_vhost_blk_dev *bvdev, struct spdk_vhost_virtqueue *vq)
24 {
25 struct spdk_vhost_blk_task *task;
26 int rc;
27 uint16_t reqs[32];
28 uint16_t reqs_cnt, i;
29
30 /* 先給出一些關(guān)于IO環(huán)的知識(shí):
31 (1) 簡(jiǎn)單來(lái)說(shuō),每個(gè)IO環(huán)分成descriptor數(shù)組、avail數(shù)組和used數(shù)組三個(gè)部分,數(shù)組元素個(gè)數(shù)均為環(huán)的最大請(qǐng)求個(gè)數(shù)。
32 (2) descriptor數(shù)組元素代表一段虛擬機(jī)內(nèi)存,每個(gè)IO請(qǐng)求至少包含三段,請(qǐng)求頭部段、數(shù)據(jù)段(至少一個(gè))和響應(yīng)段。
33 請(qǐng)求頭部包含請(qǐng)求類型(讀或?qū)?、訪問(wèn)偏移,數(shù)據(jù)段代表實(shí)際的數(shù)據(jù)存放位置,響應(yīng)段記錄請(qǐng)求處理結(jié)果。一般來(lái)說(shuō),
34 每個(gè)IO請(qǐng)求在descriptor中至少要占據(jù)三個(gè)元素;不過(guò)當(dāng)配置了indirect特性后,一個(gè)IO請(qǐng)求只占用一項(xiàng),只不過(guò)
35 該項(xiàng)指向的內(nèi)存段又是一個(gè)descriptor數(shù)組,該數(shù)組元素個(gè)數(shù)為IO請(qǐng)求實(shí)際所需內(nèi)存段。
36 (3) avail數(shù)組用來(lái)記錄已下發(fā)的IO請(qǐng)求,數(shù)組元素內(nèi)容為IO請(qǐng)求在descriptor數(shù)組中的下標(biāo),該下標(biāo)可作為請(qǐng)求的id。
37 (4) used數(shù)組用來(lái)記錄已完成的IO響應(yīng),數(shù)組元素內(nèi)容同樣為IO在descritpror數(shù)組中的下標(biāo)。
38 */
39
40 /* 從IO環(huán)的avail數(shù)組中中取出一批請(qǐng)求,將請(qǐng)求id放入reqs數(shù)組中;每次將環(huán)取空或者最多取32個(gè)請(qǐng)求 */
41 reqs_cnt = spdk_vhost_vq_avail_ring_get(vq, reqs, SPDK_COUNTOF(reqs));
42 ...
43
44 /* 依次對(duì)reqs數(shù)組中的請(qǐng)求進(jìn)行處理 */
45 for (i = 0; i < reqs_cnt; i++) {
46 ...
47
48 /* 以請(qǐng)求id作為下標(biāo),找到對(duì)應(yīng)的task對(duì)象。注,初始化時(shí),會(huì)按IO環(huán)的最大請(qǐng)求個(gè)數(shù)來(lái)申請(qǐng)tasks數(shù)組 */
49 task = &((struct spdk_vhost_blk_task *)vq->tasks)[reqs[i]];
50 ...
51
52 bvdev->vdev.task_cnt++; /* 作統(tǒng)計(jì)計(jì)數(shù) */
53
54 task->used = true; /* 代表tasks數(shù)組中該項(xiàng)正在被使用 */
55 task->iovcnt = SPDK_COUNTOF(task->iovs); /* iovs數(shù)組將來(lái)會(huì)記錄IO請(qǐng)求中數(shù)據(jù)段的內(nèi)存映射信息 */
56 task->status = NULL; /* 將來(lái)指向IO響應(yīng)段,用來(lái)給虛擬機(jī)返回IO處理結(jié)果 */
57 task->used_len = 0;
58
59 /* 將IO環(huán)中請(qǐng)求的詳細(xì)信息記錄到task中,并遞交給bdev層處理 */
60 rc = process_blk_request(task, bvdev, vq);
61 ...
62 }
63 }
64
65 static int
66 process_blk_request(struct spdk_vhost_blk_task *task, struct spdk_vhost_blk_dev *bvdev,
67 struct spdk_vhost_virtqueue *vq)
68 {
69 const struct virtio_blk_outhdr *req;
70 struct iovec *iov;
71 uint32_t type;
72 uint32_t payload_len;
73 int rc;
74
75 /* 將IO環(huán)descriptor數(shù)組中記錄的請(qǐng)求內(nèi)存段(以gpa表示,即Guest Physical Address)映成vhost進(jìn)程中的
76 虛擬地址(vva, vhost virtual address),并保存到task的iovs數(shù)組中 */
77 if (blk_iovs_setup(&bvdev->vdev, vq, task->req_idx, task->iovs, &task->iovcnt, &payload_len)) {
78 ...
79 }
80
81 /* 第一個(gè)請(qǐng)求內(nèi)存段為請(qǐng)求頭部,即struct virtio_blk_outhdr,記錄請(qǐng)求類型、訪問(wèn)位置信息 */
82 iov = &task->iovs[0];
83 ...
84 req = iov->iov_base;
85
86 /* 最后一個(gè)請(qǐng)求內(nèi)存段用來(lái)保存請(qǐng)求處理結(jié)果 */
87 iov = &task->iovs[task->iovcnt - 1];
88 ...
89 task->status = iov->iov_base;
90
91 /* 除去一頭一尾,中間的請(qǐng)求內(nèi)存段為數(shù)據(jù)段 */
92 payload_len -= sizeof(*req) + sizeof(*task->status);
93 task->iovcnt -= 2;
94
95 type = req->type;
96
97 switch (type) {
98 case VIRTIO_BLK_T_IN:
99 case VIRTIO_BLK_T_OUT:
100
101 /* 對(duì)于讀寫請(qǐng)求,調(diào)用bdev讀寫接口,并注冊(cè)請(qǐng)求完成后的回調(diào)函數(shù)為blk_request_complete_cb */
102 if (type == VIRTIO_BLK_T_IN) {
103 task->used_len = payload_len + sizeof(*task->status);
104 rc = spdk_bdev_readv(bvdev->bdev_desc, bvdev->bdev_io_channel,
105 &task->iovs[1], task->iovcnt, req->sector * 512,
106 payload_len, blk_request_complete_cb, task);
107 } else if (!bvdev->readonly) {
108 task->used_len = sizeof(*task->status);
109 rc = spdk_bdev_writev(bvdev->bdev_desc, bvdev->bdev_io_channel,
110 &task->iovs[1], task->iovcnt, req->sector * 512,
111 payload_len, blk_request_complete_cb, task);
112 } else {
113 SPDK_DEBUGLOG(SPDK_LOG_VHOST_BLK, "Device is in read-only mode!n");
114 rc = -1;
115 }
116 break;
117 case VIRTIO_BLK_T_GET_ID:
118 ...
119 break;
120 default:
121 ...
122 return -1;
123 }
124
125 return 0;
126 }
127
128 static int
129 blk_iovs_setup(struct spdk_vhost_dev *vdev, struct spdk_vhost_virtqueue *vq, uint16_t req_idx,
130 struct iovec *iovs, uint16_t *iovs_cnt, uint32_t *length)
131 {
132 struct vring_desc *desc, *desc_table;
133 uint16_t out_cnt = 0, cnt = 0;
134 uint32_t desc_table_size, len = 0;
135 int rc;
136
137 /* 從IO環(huán)descriptor數(shù)組中獲取請(qǐng)求對(duì)應(yīng)的所有內(nèi)存段信息,并映射成vva地址 */
138 rc = spdk_vhost_vq_get_desc(vdev, vq, req_idx, &desc, &desc_table, &desc_table_size);
139 ...
140
141 while (1) {
142 ...
143 len += desc->len;
144
145 out_cnt += spdk_vhost_vring_desc_is_wr(desc);
146
147 rc = spdk_vhost_vring_desc_get_next(&desc, desc_table, desc_table_size);
148 if (rc != 0) {
149 ...
150 return -1;
151 } else if (desc == NULL) {
152 break;
153 }
154 }
155
156 ...
157
158 *length = len;
159 *iovs_cnt = cnt;
160 return 0;
161 }
162
163 int
164 spdk_vhost_vq_get_desc(struct spdk_vhost_dev *vdev, struct spdk_vhost_virtqueue *virtqueue,
165 uint16_t req_idx, struct vring_desc **desc, struct vring_desc **desc_table,
166 uint32_t *desc_table_size)
167 {
168
169 *desc = &virtqueue->vring.desc[req_idx];
170
171 if (spdk_vhost_vring_desc_is_indirect(*desc)) {
172 assert(spdk_vhost_dev_has_feature(vdev, VIRTIO_RING_F_INDIRECT_DESC));
173 *desc_table_size = (*desc)->len / sizeof(**desc);
174
175 /* 將IO環(huán)中記錄的gpa地址轉(zhuǎn)換成vhost的虛擬地址,qemu和vhost之間的內(nèi)存映射關(guān)系管理我們將在管理面分析時(shí)討論 */
176 *desc_table = spdk_vhost_gpa_to_vva(vdev, (*desc)->addr, sizeof(**desc) * *desc_table_size);
177 *desc = *desc_table;
178 if (*desc == NULL) {
179 return -1;
180 }
181
182 return 0;
183 }
184
185 *desc_table = virtqueue->vring.desc;
186 *desc_table_size = virtqueue->vring.size;
187
188 return 0;
189 }

接著,我們看一下bdev層對(duì)IO請(qǐng)求的處理,以讀請(qǐng)求為例:

spdk/lib/bdev/bdev.c:

1 int
2 spdk_bdev_readv(struct spdk_bdev_desc *desc, struct spdk_io_channel *ch,
3 struct iovec *iov, int iovcnt,
4 uint64_t offset, uint64_t nbytes,
5 spdk_bdev_io_completion_cb cb, void *cb_arg)
6 {
7 uint64_t offset_blocks, num_blocks;
8
9 ...
10
11 /* 將字節(jié)轉(zhuǎn)換成塊進(jìn)行實(shí)際的IO操作 */
12 return spdk_bdev_readv_blocks(desc, ch, iov, iovcnt, offset_blocks, num_blocks, cb, cb_arg);
13 }
14
15 int spdk_bdev_readv_blocks(struct spdk_bdev_desc *desc, struct spdk_io_channel *ch,
16 struct iovec *iov, int iovcnt,
17 uint64_t offset_blocks, uint64_t num_blocks,
18 spdk_bdev_io_completion_cb cb, void *cb_arg)
19 {
20 struct spdk_bdev *bdev = desc->bdev;
21 struct spdk_bdev_io *bdev_io;
22 struct spdk_bdev_channel *channel = spdk_io_channel_get_ctx(ch);
23
24 /* io channel是一個(gè)線程強(qiáng)相關(guān)對(duì)象,不同的線程對(duì)應(yīng)不同的channel,
25 這里spdk_bdev_channel包含一個(gè)線程獨(dú)立的緩存池,先從中申請(qǐng)bdev_io內(nèi)存(免鎖),
26 如果申請(qǐng)不到,再到全局的mempool中申請(qǐng)內(nèi)存 */
27 bdev_io = spdk_bdev_get_io(channel);
28 ...
29
30 /* 將接口參數(shù)記錄到bdev_io中,并繼續(xù)遞交 */
31 bdev_io->ch = channel;
32 bdev_io->type = SPDK_BDEV_IO_TYPE_READ;
33 bdev_io->u.bdev.iovs = iov;
34 bdev_io->u.bdev.iovcnt = iovcnt;
35 bdev_io->u.bdev.num_blocks = num_blocks;
36 bdev_io->u.bdev.offset_blocks = offset_blocks;
37 spdk_bdev_io_init(bdev_io, bdev, cb_arg, cb);
38
39 spdk_bdev_io_submit(bdev_io);
40 return 0;
41 }
42
43 static void
44 spdk_bdev_io_submit(struct spdk_bdev_io *bdev_io)
45 {
46 struct spdk_bdev *bdev = bdev_io->bdev;
47
48 if (bdev_io->ch->flags & BDEV_CH_QOS_ENABLED) { /* 開(kāi)啟了bdev的qos特性時(shí)走該流程 */
49 ...
50 } else {
51 _spdk_bdev_io_submit(bdev_io); /* 直接遞交 */
52 }
53 }
54
55 static void
56 _spdk_bdev_io_submit(void *ctx)
57 {
58 struct spdk_bdev_io *bdev_io = ctx;
59 struct spdk_bdev *bdev = bdev_io->bdev;
60 struct spdk_bdev_channel *bdev_ch = bdev_io->ch;
61 struct spdk_io_channel *ch = bdev_ch->channel; /* 底層驅(qū)動(dòng)對(duì)應(yīng)的io channel */
62 struct spdk_bdev_module_channel *module_ch = bdev_ch->module_ch;
63
64 bdev_io->submit_tsc = spdk_get_ticks();
65 bdev_ch->io_outstanding++;
66 module_ch->io_outstanding++;
67 bdev_io->in_submit_request = true;
68 if (spdk_likely(bdev_ch->flags == 0)) {
69 if (spdk_likely(TAILQ_EMPTY(&module_ch->nomem_io))) {
70 /* 不同的驅(qū)動(dòng)在生成bdev對(duì)象時(shí)會(huì)注冊(cè)不同的fn_table,這里將調(diào)用驅(qū)動(dòng)注冊(cè)的submit_request函數(shù) */
71 bdev->fn_table->submit_request(ch, bdev_io);
72 } else {
73 bdev_ch->io_outstanding--;
74 module_ch->io_outstanding--;
75 TAILQ_INSERT_TAIL(&module_ch->nomem_io, bdev_io, link);
76 }
77 } else if (bdev_ch->flags & BDEV_CH_RESET_IN_PROGRESS) {
78 ...
79 } else if (bdev_ch->flags & BDEV_CH_QOS_ENABLED) {
80 ...
81 } else {
82 ...
83 }
84 bdev_io->in_submit_request = false;
85 }

最后,我們來(lái)看一下bdev的NVMe驅(qū)動(dòng)的處理邏輯:

spdk/lib/bdev/bdev_nvme.c:

1 static const struct spdk_bdev_fn_table nvmelib_fn_table = {
2 .destruct = bdev_nvme_destruct,
3 .submit_request = bdev_nvme_submit_request,
4 .io_type_supported = bdev_nvme_io_type_supported,
5 .get_io_channel = bdev_nvme_get_io_channel,
6 .dump_info_json = bdev_nvme_dump_info_json,
7 .write_config_json = bdev_nvme_write_config_json,
8 .get_spin_time = bdev_nvme_get_spin_time,
9 };
10
11 static void
12 bdev_nvme_submit_request(struct spdk_io_channel *ch, struct spdk_bdev_io *bdev_io)
13 {
14 int rc = _bdev_nvme_submit_request(ch, bdev_io);
15
16 if (spdk_unlikely(rc != 0)) {
17 if (rc == -ENOMEM) {
18 spdk_bdev_io_complete(bdev_io, SPDK_BDEV_IO_STATUS_NOMEM);
19 } else {
20 spdk_bdev_io_complete(bdev_io, SPDK_BDEV_IO_STATUS_FAILED);
21 }
22 }
23 }
24
25 static int
26 _bdev_nvme_submit_request(struct spdk_io_channel *ch, struct spdk_bdev_io *bdev_io)
27 {
28 /* 將ch擴(kuò)展成具體的nvme_io_channel,其對(duì)應(yīng)一個(gè)queue parir */
29 struct nvme_io_channel *nvme_ch = spdk_io_channel_get_ctx(ch);
30 if (nvme_ch->qpair == NULL) {
31 /* The device is currently resetting */
32 return -1;
33 }
34
35 switch (bdev_io->type) {
36
37 /* 針對(duì)讀寫請(qǐng)求,會(huì)將bdev_io擴(kuò)展成nvme_bdev_io請(qǐng)求后,再將請(qǐng)求內(nèi)容填入io channel
38 對(duì)應(yīng)的queue pair中,并通知物理硬件處理 */
39 case SPDK_BDEV_IO_TYPE_READ:
40 spdk_bdev_io_get_buf(bdev_io, bdev_nvme_get_buf_cb,
41 bdev_io->u.bdev.num_blocks * bdev_io->bdev->blocklen);
42 return 0;
43
44 case SPDK_BDEV_IO_TYPE_WRITE:
45 return bdev_nvme_writev((struct nvme_bdev *)bdev_io->bdev->ctxt,
46 ch,
47 (struct nvme_bdev_io *)bdev_io->driver_ctx,
48 bdev_io->u.bdev.iovs,
49 bdev_io->u.bdev.iovcnt,
50 bdev_io->u.bdev.num_blocks,
51 bdev_io->u.bdev.offset_blocks);
52 ...
53 default:
54 return -EINVAL;
55 }
56
57 return 0;
58 }

詳細(xì)的NVMe請(qǐng)求處理不在本文的討論范圍內(nèi),感興趣的讀者可以自行深入分析。

IO響應(yīng)返回流程代碼解析

reactor線程通過(guò)bdev_nvme_poll函數(shù)獲知已完成的NVMe響應(yīng),最終會(huì)調(diào)用bdev層的spdk_bdev_io_complete來(lái)處理響應(yīng):

spdk/lib/bdev/bdev.c:

1 void
2 spdk_bdev_io_complete(struct spdk_bdev_io *bdev_io, enum spdk_bdev_io_status status)
3 {
4 ...
5 bdev_io->status = status;
6
7 ...
8 _spdk_bdev_io_complete(bdev_io);
9 }
10
11 static inline void
12 _spdk_bdev_io_complete(void *ctx)
13 {
14 struct spdk_bdev_io *bdev_io = ctx;
15
16 ...
17
18 /* 如果請(qǐng)求執(zhí)行成功,則更新一些統(tǒng)計(jì)信息 */
19 if (bdev_io->status == SPDK_BDEV_IO_STATUS_SUCCESS) {
20 switch (bdev_io->type) {
21 case SPDK_BDEV_IO_TYPE_READ:
22 bdev_io->ch->stat.bytes_read += bdev_io->u.bdev.num_blocks * bdev_io->bdev->blocklen;
23 bdev_io->ch->stat.num_read_ops++;
24 bdev_io->ch->stat.read_latency_ticks += (spdk_get_ticks() - bdev_io->submit_tsc);
25 break;
26 case SPDK_BDEV_IO_TYPE_WRITE:
27 bdev_io->ch->stat.bytes_written += bdev_io->u.bdev.num_blocks * bdev_io->bdev->blocklen;
28 bdev_io->ch->stat.num_write_ops++;
29 bdev_io->ch->stat.write_latency_ticks += (spdk_get_ticks() - bdev_io->submit_tsc);
30 break;
31 default:
32 break;
33 }
34 }
35
36 /* 調(diào)用上層注冊(cè)回調(diào),這里將回到vhost-blk的blk_request_complete_cb */
37 bdev_io->cb(bdev_io, bdev_io->status == SPDK_BDEV_IO_STATUS_SUCCESS, bdev_io->caller_ctx);
38 }
39 spdk/lib/vhost/vhost_blk.c:
40
41 static void
42 blk_request_complete_cb(struct spdk_bdev_io *bdev_io, bool success, void *cb_arg)
43 {
44 struct spdk_vhost_blk_task *task = cb_arg;
45
46 spdk_bdev_free_io(bdev_io); /* 釋放bdev_io */
47 blk_request_finish(success, task);
48 }
49
50 static void
51 blk_request_finish(bool success, struct spdk_vhost_blk_task *task)
52 {
53 *task->status = success ? VIRTIO_BLK_S_OK : VIRTIO_BLK_S_IOERR;
54
55 /* 往虛擬機(jī)中放入響應(yīng)并以虛擬中斷方式通知虛擬機(jī)IO完成 */
56 spdk_vhost_vq_used_ring_enqueue(&task->bvdev->vdev, task->vq, task->req_idx,
57 task->used_len);
58
59 /* 釋放當(dāng)前task,實(shí)際就是將task->used置為false */
60 blk_task_finish(task);
61 }

至此,整個(gè)IO流程已經(jīng)分析完畢,可見(jiàn)SPDK對(duì)IO的處理還是非常簡(jiǎn)潔的,這便是高性能的基石。

【SPDK】四、reactor線程

reactor線程是SPDK中負(fù)責(zé)實(shí)際業(yè)務(wù)處理邏輯的單元,它們?cè)趘hsot服務(wù)啟動(dòng)時(shí)創(chuàng)建,直到服務(wù)停止。目前還不支持reactor線程的動(dòng)態(tài)增減。

reactor線程總流程

我們順著vhost進(jìn)程的代碼執(zhí)行順序來(lái)看看總體流程:

1 spdk/app/vhost/vhost.c:
2
3 int
4 main(int argc, char *argv[])
5 {
6 struct spdk_app_opts opts = {};
7 int rc;
8
9 /* 首先進(jìn)行參數(shù)解析,解析后的結(jié)果保存于opts中 */
10
11 vhost_app_opts_init(&opts);
12
13 if ((rc = spdk_app_parse_args(argc, argv, &opts, "f:S:",
14 vhost_parse_arg, vhost_usage)) !=
15 SPDK_APP_PARSE_ARGS_SUCCESS) {
16 exit(rc);
17 }
18
19 ...
20
21 /* 接著根據(jù)配置文件指明的物理核啟動(dòng)reactors線程(主線程最終也成為一個(gè)reactor)。
22 這些reactors線程會(huì)執(zhí)行輪循函數(shù),直到外部將服務(wù)狀態(tài)置為退出 */
23
24 /* Blocks until the application is exiting */
25 rc = spdk_app_start(&opts, vhost_started, NULL, NULL);
26
27 /* 所有reactor線程退出后,進(jìn)行資源清理 */
28 spdk_app_fini();
29
30 return rc;
31 }

上述整體流程中最為重要的便是spdk_app_start函數(shù),該函數(shù)內(nèi)部調(diào)用了DPDK關(guān)于系統(tǒng)CPU、內(nèi)存、PCI設(shè)備管理等通用性服務(wù)代碼,這里我們盡可能以理解其功能為主而不做深入的代碼分析:

1 spdk/lib/event/app.c:
2
3 int
4 spdk_app_start(struct spdk_app_opts *opts, spdk_event_fn start_fn,
5 void *arg1, void *arg2)
6 {
7 struct spdk_conf *config = NULL;
8 int rc;
9 struct spdk_event *app_start_event;
10
11 ...
12
13 /* 將配置文件中的內(nèi)容導(dǎo)入到config對(duì)象中 */
14 config = spdk_app_setup_conf(opts->config_file);
15 ...
16 spdk_app_read_config_file_global_params(opts);
17
18 ...
19
20 /* 調(diào)用DPDK系統(tǒng)服務(wù):
21 (1)通過(guò)內(nèi)核sysfs獲取物理CPU信息,并通過(guò)配置文件指定的運(yùn)行核,在各個(gè)核上啟動(dòng)服務(wù)線程;
22 各服務(wù)線程啟動(dòng)后因?yàn)樵诘却骶€程給它們發(fā)送需要執(zhí)行的任務(wù)而處于睡眠狀態(tài);
23 (2)基于大頁(yè)內(nèi)存創(chuàng)建內(nèi)存池以供其它模塊使用;
24 (3)初始化PCI設(shè)備枚舉服務(wù),可以實(shí)現(xiàn)類似內(nèi)核的設(shè)備發(fā)現(xiàn)及驅(qū)動(dòng)初始化流程。SPDK基于此并借
25 助內(nèi)核uio或vfio驅(qū)動(dòng)實(shí)現(xiàn)全用戶態(tài)的PCI驅(qū)動(dòng) */
26 /* 完成DPDK的初始化后,SPDK會(huì)建立一張由vva(vhost virtual address)到pa(physical address)
27 的內(nèi)存映射表g_vtophys_map。每當(dāng)有新的內(nèi)存映射到vhost中時(shí),都需要調(diào)用spdk_mem_register在該
28 表中注冊(cè)新的映射關(guān)系。設(shè)計(jì)該表的原因是當(dāng)SPDK向物理設(shè)備發(fā)送DMA請(qǐng)求時(shí),需要向設(shè)備提供pa而非vva */
29 if (spdk_app_setup_env(opts) < 0) {
30 ...
31 }
32
33 /* 這里為reactors分配相應(yīng)的內(nèi)存 */
34 /*
35 * If mask not specified on command line or in configuration file,
36 * reactor_mask will be 0x1 which will enable core 0 to run one
37 * reactor.
38 */
39 if ((rc = spdk_reactors_init(opts->max_delay_us)) != 0) {
40 ...
41 }
42
43 ...
44
45 /* 設(shè)置一些全局變量 */
46 memset(&g_spdk_app, 0, sizeof(g_spdk_app));
47 g_spdk_app.config = config;
48 g_spdk_app.shm_id = opts->shm_id;
49 g_spdk_app.shutdown_cb = opts->shutdown_cb;
50 g_spdk_app.rc = 0;
51 g_init_lcore = spdk_env_get_current_core();
52 g_app_start_fn = start_fn;
53 g_app_start_arg1 = arg1;
54 g_app_start_arg2 = arg2;
55 app_start_event = spdk_event_allocate(g_init_lcore, start_rpc, (void *)opts->rpc_addr, NULL);
56
57 /* 初始化SPDK的各個(gè)子系統(tǒng),如bdev、vhost均為子系統(tǒng)。但這里需注意一點(diǎn),此處僅是產(chǎn)生了一個(gè)初始化事件,事件的處理要在
58 reactor線程正式進(jìn)入輪循函數(shù)后才開(kāi)始 */
59 spdk_subsystem_init(app_start_event);
60
61 /* 從此處開(kāi)始,各個(gè)線程(包括主線程)開(kāi)始執(zhí)行_spdk_reactor_run,線程名也正式變更為reactor_X;
62 直到所有線程均退出_spdk_reactor_run后,主線程才會(huì)返回 */
63 /* This blocks until spdk_app_stop is called */
64 spdk_reactors_start();
65
66 return g_spdk_app.rc;
67 ...
68 }

再看一下spdk_reactors_start:

1 spdk/lib/event/reactor.c:
2
3 void
4 spdk_reactors_start(void)
5 {
6 struct spdk_reactor *reactor;
7 uint32_t i, current_core;
8 int rc;
9
10 g_reactor_state = SPDK_REACTOR_STATE_RUNNING;
11 g_spdk_app_core_mask = spdk_cpuset_alloc();
12
13 /* 針對(duì)主線程之外的其它核上的線程,通過(guò)發(fā)送通知使它們開(kāi)始執(zhí)行_spdk_reactor_run */
14 current_core = spdk_env_get_current_core();
15 SPDK_ENV_FOREACH_CORE(i) {
16 if (i != current_core) {
17 reactor = spdk_reactor_get(i);
18 rc = spdk_env_thread_launch_pinned(reactor->lcore, _spdk_reactor_run, reactor);
19 ...
20 }
21 spdk_cpuset_set_cpu(g_spdk_app_core_mask, i, true);
22 }
23
24 /* 主線程也會(huì)執(zhí)行_spdk_reactor_run */
25 /* Start the master reactor */
26 reactor = spdk_reactor_get(current_core);
27 _spdk_reactor_run(reactor);
28
29 /* 主線程退出后會(huì)等待其它核上的線程均退出 */
30 spdk_env_thread_wait_all();
31
32 /* 執(zhí)行到此處,說(shuō)明vhost服務(wù)進(jìn)程即將退出 */
33 g_reactor_state = SPDK_REACTOR_STATE_SHUTDOWN;
34 spdk_cpuset_free(g_spdk_app_core_mask);
35 g_spdk_app_core_mask = NULL;
36 }

輪循函數(shù)_spdk_reactor_run

通過(guò)對(duì)vhost代碼流程的分析,我們看到vhost中所有線程最終都會(huì)調(diào)用_spdk_reactor_run,該函數(shù)是一個(gè)死循環(huán),由此實(shí)現(xiàn)輪循邏輯:

spdk/lib/event/reactor.c:

1 static int
2 _spdk_reactor_run(void *arg)
3 {
4 struct spdk_reactor *reactor = arg;
5 struct spdk_poller *poller;
6 uint32_t event_count;
7 uint64_t idle_started, now;
8 uint64_t spin_cycles, sleep_cycles;
9 uint32_t sleep_us;
10 uint32_t timer_poll_count;
11 char thread_name[32];
12
13 /* 重新命名線程名,reactor_[核號(hào)] */
14 snprintf(thread_name, sizeof(thread_name), "reactor_%u", reactor->lcore);
15
16 /* 創(chuàng)建SPDK線程對(duì)象:
17 (1)線程間通過(guò)_spdk_reactor_send_msg發(fā)送消息,本質(zhì)是向接收方的event隊(duì)列中添加事件;
18 (2)線程通過(guò)_spdk_reactor_start_poller和_spdk_reactor_stop_poller啟動(dòng)和停止poller;
19 (3)IO Channel等線程相關(guān)對(duì)象也會(huì)記錄到線程對(duì)象中 */
20 if (spdk_allocate_thread(_spdk_reactor_send_msg,
21 _spdk_reactor_start_poller,
22 _spdk_reactor_stop_poller,
23 reactor, thread_name) == NULL) {
24 return -1;
25 }
26
27 /* spin_cycles代表最短輪循時(shí)間 */
28 spin_cycles = SPDK_REACTOR_SPIN_TIME_USEC * spdk_get_ticks_hz() / SPDK_SEC_TO_USEC;
29 /* sleep_cycles代表最長(zhǎng)睡眠時(shí)間 */
30 sleep_cycles = reactor->max_delay_us * spdk_get_ticks_hz() / SPDK_SEC_TO_USEC;
31 idle_started = 0;
32 timer_poll_count = 0;
33
34 /* 輪循的死循環(huán)正式開(kāi)始 */
35 while (1) {
36 bool took_action = false;
37
38 /* 首先,每個(gè)reactor線程通過(guò)DPDK的無(wú)鎖隊(duì)列實(shí)現(xiàn)了一個(gè)事件隊(duì)列;這里從事件隊(duì)列中取出事件并調(diào)用事件
39 的處理函數(shù)。例如,vhost的子系統(tǒng)的初始化即是在spdk_subsystem_init中產(chǎn)生了一個(gè)verify事件并
40 添加到主線程reactor的事件隊(duì)列中,該事件處理函數(shù)為spdk_subsystem_verify */
41 event_count = _spdk_event_queue_run_batch(reactor);
42 if (event_count > 0) {
43 took_action = true;
44 }
45
46 /* 接著,每個(gè)reactor線程從active_pollers鏈表頭部取出一個(gè)poller并調(diào)用其fn函數(shù)。poller代表一次
47 具體的處理動(dòng)作,例如處理某個(gè)vhost_blk設(shè)備的所有IO環(huán)中的請(qǐng)求,又或者處理后端NVMe某個(gè)queue
48 pair中的所有響應(yīng) */
49 poller = TAILQ_FIRST(&reactor->active_pollers);
50 if (poller) {
51 TAILQ_REMOVE(&reactor->active_pollers, poller, tailq);
52 poller->state = SPDK_POLLER_STATE_RUNNING;
53 poller->fn(poller->arg);
54 if (poller->state == SPDK_POLLER_STATE_UNREGISTERED) {
55 free(poller);
56 } else {
57 poller->state = SPDK_POLLER_STATE_WAITING;
58 TAILQ_INSERT_TAIL(&reactor->active_pollers, poller, tailq);
59 }
60 took_action = true;
61 }
62
63 /* 最后,reactor線程還實(shí)現(xiàn)了定時(shí)器邏輯,這里判斷是否有定時(shí)器到期;如果確有定時(shí)器到期則執(zhí)行其回調(diào)并將
64 其放到定時(shí)器隊(duì)列尾部 */
65 if (timer_poll_count >= SPDK_TIMER_POLL_ITERATIONS) {
66 poller = TAILQ_FIRST(&reactor->timer_pollers);
67 if (poller) {
68 now = spdk_get_ticks();
69
70 if (now >= poller->next_run_tick) {
71 TAILQ_REMOVE(&reactor->timer_pollers, poller, tailq);
72 poller->state = SPDK_POLLER_STATE_RUNNING;
73 poller->fn(poller->arg);
74 if (poller->state == SPDK_POLLER_STATE_UNREGISTERED) {
75 free(poller);
76 } else {
77 poller->state = SPDK_POLLER_STATE_WAITING;
78 _spdk_poller_insert_timer(reactor, poller, now);
79 }
80 took_action = true;
81 }
82 }
83 timer_poll_count = 0;
84 } else {
85 timer_poll_count++;
86 }
87
88 /* 下面的邏輯主要用來(lái)決定輪循線程是否可以睡眠一會(huì) */
89
90 if (took_action) {
91 /* We were busy this loop iteration. Reset the idle timer. */
92 idle_started = 0;
93 } else if (idle_started == 0) {
94 /* We were previously busy, but this loop we took no actions. */
95 idle_started = spdk_get_ticks();
96 }
97
98 /* Determine if the thread can sleep */
99 if (sleep_cycles && idle_started) {
100 now = spdk_get_ticks();
101 if (now >= (idle_started + spin_cycles)) { /* 保證輪循線程最少已執(zhí)行了spin_cycles */
102 sleep_us = reactor->max_delay_us;
103
104 poller = TAILQ_FIRST(&reactor->timer_pollers);
105 if (poller) {
106 /* There are timers registered, so don't sleep beyond
107 * when the next timer should fire */
108 if (poller->next_run_tick < (now + sleep_cycles)) {
109 if (poller->next_run_tick <= now) {
110 sleep_us = 0;
111 } else {
112 sleep_us = ((poller->next_run_tick - now) *
113 SPDK_SEC_TO_USEC) / spdk_get_ticks_hz();
114 }
115 }
116 }
117
118 if (sleep_us > 0) {
119 usleep(sleep_us);
120 }
121
122 /* After sleeping, always poll for timers */
123 timer_poll_count = SPDK_TIMER_POLL_ITERATIONS;
124 }
125 }
126
127 if (g_reactor_state != SPDK_REACTOR_STATE_RUNNING) {
128 break;
129 }
130 } /* 死循環(huán)結(jié)束 */
131
132 ...
133 spdk_free_thread();
134 return 0;
135 }

至此,reactor線程整體執(zhí)行邏輯已分析完成,后續(xù)我們將以verify_event為線索開(kāi)始分析各個(gè)子系統(tǒng)的初始化過(guò)程。

【SPDK】五、bdev子系統(tǒng)

SPDK從功能角度將各個(gè)獨(dú)立的部分劃分為“子系統(tǒng)“。例如對(duì)各種后端存儲(chǔ)的訪問(wèn)屬于bdev子系統(tǒng),又例如對(duì)虛擬機(jī)呈現(xiàn)各種設(shè)備屬于vhost子系統(tǒng)。不同場(chǎng)景下,各種工具可以通過(guò)組合不同的子系統(tǒng)來(lái)實(shí)現(xiàn)各種不同的功能。例如虛擬化場(chǎng)景下,vhost主要集成了bdev、vhost、scsi等子系統(tǒng)。這些子系統(tǒng)存在一定依賴關(guān)系,例如vhost子系統(tǒng)依賴bdev,這就需要將被依賴的子系統(tǒng)先初始化完成,才能執(zhí)行其它子系統(tǒng)的初始化。

本篇博文我們先整體介紹一下SPDK子系統(tǒng)的初始化流程,然后再深入分析一下bdev子系統(tǒng)。vhost子系統(tǒng)我們將在獨(dú)立的博文中展開(kāi)分析。

SPDK子系統(tǒng)

通過(guò)前文的分析,我們知道主線程在執(zhí)行_spdk_reactor_run時(shí),首先處理的事件便是verify事件,該事件處理函數(shù)為spdk_subsystem_verify:

spdk/lib/event/subsystem.c:

1 static void
2 spdk_subsystem_verify(void *arg1, void *arg2)
3 {
4 struct spdk_subsystem_depend *dep;
5
6 /* 檢查當(dāng)前應(yīng)用中所有需要的子系統(tǒng)及其依賴系統(tǒng)是否均已成功注冊(cè) */
7 /* Verify that all dependency name and depends_on subsystems are registered */
8 TAILQ_FOREACH(dep, &g_subsystems_deps, tailq) {
9 if (!spdk_subsystem_find(&g_subsystems, dep->name)) {
10 SPDK_ERRLOG("subsystem %s is missingn", dep->name);
11 spdk_app_stop(-1);
12 return;
13 }
14 if (!spdk_subsystem_find(&g_subsystems, dep->depends_on)) {
15 SPDK_ERRLOG("subsystem %s dependency %s is missingn",
16 dep->name, dep->depends_on);
17 spdk_app_stop(-1);
18 return;
19 }
20 }
21
22 /* 按依賴關(guān)系對(duì)所有子系統(tǒng)進(jìn)行排序 */
23 subsystem_sort();
24
25 /* 依據(jù)排序依次執(zhí)行各個(gè)子系統(tǒng)的init函數(shù) */
26 spdk_subsystem_init_next(0);
27 }

bdev子系統(tǒng)

bdev和vhost是虛擬化場(chǎng)景下兩個(gè)最為主要的子系統(tǒng),且vhost依賴bdev,因此我們先來(lái)分析一下bdev子系統(tǒng)。

我們可以看到bdev子系統(tǒng)的初始化函數(shù)為spdk_bdev_subsystem_initialize:

spdk/lib/event/subsystems/bdev/bdev.c:

1 static struct spdk_subsystem g_spdk_subsystem_bdev = {
2 .name = "bdev",
3 .init = spdk_bdev_subsystem_initialize,
4 .fini = spdk_bdev_subsystem_finish,
5 .config = spdk_bdev_config_text,
6 .write_config_json = _spdk_bdev_subsystem_config_json,
7 };

bdev子系統(tǒng)針對(duì)不同的后端存儲(chǔ)設(shè)備實(shí)現(xiàn)了不同的“模塊”,例如nvme模塊主要實(shí)現(xiàn)了用戶態(tài)對(duì)nvme設(shè)備的訪問(wèn)操作,virtio實(shí)現(xiàn)了用戶態(tài)對(duì)virtio設(shè)備的訪問(wèn)操作,又例如malloc模塊通過(guò)內(nèi)存實(shí)現(xiàn)了一個(gè)模擬的塊設(shè)備。因此bdev子系統(tǒng)在初始化時(shí)主要針對(duì)配置文件中已經(jīng)配置的后端存儲(chǔ)模塊進(jìn)行初始化操作。

另外,bdev借助IO Channel的概念也實(shí)現(xiàn)了系統(tǒng)級(jí)的management_channel和模塊級(jí)的module_channel。我們知道IO Channel是一個(gè)線程相關(guān)的概念,management_channel和module_channel也是如此:

  • management_channel是線程唯一的一個(gè)對(duì)象,不同線程具備不同的的management_channel,同一個(gè)線程只有一個(gè)。目前management_channel中實(shí)現(xiàn)了一個(gè)線程內(nèi)部獨(dú)立的內(nèi)存池,用來(lái)緩存bdev_io對(duì)象;
  • module_channel是線程內(nèi)部屬于同一個(gè)模塊的bdev所共享的一個(gè)對(duì)象,用來(lái)記錄同一線程中屬于同一模塊的所有對(duì)象。例如同一個(gè)線程如果操作兩個(gè)nvme的bdev對(duì)象且這兩個(gè)bdev屬于不同的nvme控制器,那么雖然這兩個(gè)bdev對(duì)應(yīng)不同的NVMe IO Channel,但是它們屬于同一個(gè)module_channel。目前module_channel只含有一個(gè)模塊級(jí)的引用計(jì)數(shù)和內(nèi)存不足時(shí)的bdev io臨時(shí)隊(duì)列(當(dāng)有內(nèi)存空間時(shí),實(shí)現(xiàn)IO重發(fā))。

每個(gè)模塊都會(huì)提供一個(gè)module_init函數(shù),當(dāng)bdev子系統(tǒng)初始化時(shí)會(huì)依次調(diào)用這些初始化函數(shù)。下面我們以NVMe和virtio兩個(gè)模塊為例,來(lái)簡(jiǎn)要看下模塊的初始化邏輯。

  1. nvme模塊初始化

nvme模塊描述如下:

spdk/lib/bdev/nvme/bdev_nvme.c:

1 static struct spdk_bdev_module nvme_if = {
2 .name = "nvme",
3 .module_init = bdev_nvme_library_init,
4 .module_fini = bdev_nvme_library_fini,
5 .config_text = bdev_nvme_get_spdk_running_config,
6 .config_json = bdev_nvme_config_json,
7 .get_ctx_size = bdev_nvme_get_ctx_size,
8
9 };

這里我們可以看到nvme模塊的初始化函數(shù)為bdev_nvme_library_init,另外bdev_nvme_get_ctx_size返回的context大小為nvme_bdev_io的大小。bdev子系統(tǒng)會(huì)以所有模塊最大的context大小來(lái)創(chuàng)建bdev_io內(nèi)存池,以此確保為所有模塊申請(qǐng)bdev_io時(shí)都能獲得足夠的擴(kuò)展內(nèi)存(nvme_bdev_io即是對(duì)bdev_io的擴(kuò)展)。

bdev_nvme_library_init函數(shù)從SPDK的配置文件中讀取“Nvme”字段開(kāi)始的相關(guān)信息,并通過(guò)這些信息創(chuàng)建一個(gè)NVMe控制器并獲取其下的namespace,最后將namespace表示成一個(gè)bdev對(duì)象。這里我們打開(kāi)看一下識(shí)別到對(duì)應(yīng)NVMe控制器后的回調(diào)處理邏輯:

1 static void
2 attach_cb(void *cb_ctx, const struct spdk_nvme_transport_id *trid,
3 struct spdk_nvme_ctrlr *ctrlr, const struct spdk_nvme_ctrlr_opts *opts)
4 {
5 struct nvme_ctrlr *nvme_ctrlr;
6 struct nvme_probe_ctx *ctx = cb_ctx;
7 char *name = NULL;
8 size_t i;
9
10 /* 首先根據(jù)DPDK中PCI驅(qū)動(dòng)框架識(shí)別到的NVMe控制器信息來(lái)創(chuàng)建一個(gè)nvme_ctrlr對(duì)象 */
11 if (ctx) {
12 for (i = 0; i < ctx->count; i++) {
13 if (spdk_nvme_transport_id_compare(trid, &ctx->trids[i]) == 0) {
14 name = strdup(ctx->names[i]);
15 break;
16 }
17 }
18 } else {
19 name = spdk_sprintf_alloc("HotInNvme%d", g_hot_insert_nvme_controller_index++);
20 }
21
22 nvme_ctrlr = calloc(1, sizeof(*nvme_ctrlr));
23 ...
24 nvme_ctrlr->adminq_timer_poller = NULL;
25 nvme_ctrlr->ctrlr = ctrlr;
26 nvme_ctrlr->ref = 0;
27 nvme_ctrlr->trid = *trid;
28 nvme_ctrlr->name = name;
29
30 /* 將該nvme控制器對(duì)象添加為一個(gè)io device;每個(gè)io device可申請(qǐng)獨(dú)立的IO Channel;
31 bdev_nvme_create_cb負(fù)責(zé)在IO Channel對(duì)象創(chuàng)建時(shí)初始化底層驅(qū)動(dòng)相關(guān)對(duì)象,這里
32 即是獲取一個(gè)新的queue pair */
33 spdk_io_device_register(ctrlr, bdev_nvme_create_cb, bdev_nvme_destroy_cb,
34 sizeof(struct nvme_io_channel));
35
36 /* 此處開(kāi)始枚舉nvme控制器下的所有namespace,并將其建為bdev對(duì)象。注意一點(diǎn),此時(shí)并不會(huì)為
37 bdev申請(qǐng)IO channel,它是vhost子系統(tǒng)初始時(shí),完成線程綁定后才創(chuàng)建的 */
38 if (nvme_ctrlr_create_bdevs(nvme_ctrlr) != 0) {
39 ...
40 }
41
42 nvme_ctrlr->adminq_timer_poller = spdk_poller_register(bdev_nvme_poll_adminq, ctrlr,
43 g_nvme_adminq_poll_timeout_us);
44
45 TAILQ_INSERT_TAIL(&g_nvme_ctrlrs, nvme_ctrlr, tailq);
46
47 ...
48 }
49
50 /* 注意:bdev初始化時(shí)并不調(diào)用該函數(shù) */
51 static int
52 bdev_nvme_create_cb(void *io_device, void *ctx_buf)
53 {
54 struct spdk_nvme_ctrlr *ctrlr = io_device;
55 struct nvme_io_channel *ch = ctx_buf;
56
57 /* 分配一個(gè)nvme queue pair作為該IO Channel的實(shí)際對(duì)象 */
58 ch->qpair = spdk_nvme_ctrlr_alloc_io_qpair(ctrlr, NULL, 0);
59 ...
60 /* 向reactor注冊(cè)一個(gè)poller,輪循新分配queue pair中已完成的響應(yīng)信息 */
61 ch->poller = spdk_poller_register(bdev_nvme_poll, ch, 0);
62 return 0;
63 }

類似地,我們?cè)倏匆幌聉irtio模塊的初始化。

  1. virtio模塊初始化

virtio雖說(shuō)起源于qemu-kvm虛擬化,但是它也是一種可用物理硬件實(shí)現(xiàn)的協(xié)議規(guī)范。因此SPDK也把它當(dāng)做一種后端存儲(chǔ)類型加以實(shí)現(xiàn)。當(dāng)然,如果SPDK的vhost進(jìn)程是運(yùn)行在虛擬機(jī)中(而虛擬機(jī)virtio設(shè)備作為后端存儲(chǔ)),virtio模塊就是一個(gè)必不可少的驅(qū)動(dòng)模塊了。

我們以virtio-blk設(shè)備為例,來(lái)看一下其初始化過(guò)程:

spdk/lib/bdev/virtio/bdev_virtio_blk.c:

1 static struct spdk_bdev_module virtio_blk_if = {
2 .name = "virtio_blk",
3 .module_init = bdev_virtio_initialize,
4 .get_ctx_size = bdev_virtio_blk_get_ctx_size,
5 };

bdev_virtio_initialize通過(guò)配置文件獲取相關(guān)配置信息,并同樣借助DPDK的用戶態(tài)PCI設(shè)備管理框架識(shí)別到該設(shè)備后,調(diào)用virtio_pci_blk_dev_create來(lái)創(chuàng)建一個(gè)virtio_blk對(duì)象:

spdk/lib/bdev/virtio/bdev_virtio_blk.c:

1 static struct virtio_blk_dev *
2 virtio_pci_blk_dev_create(const char *name, struct virtio_pci_ctx *pci_ctx)
3 {
4 static int pci_dev_counter = 0;
5 struct virtio_blk_dev *bvdev;
6 struct virtio_dev *vdev;
7 char *default_name = NULL;
8 uint16_t num_queues;
9 int rc;
10
11 /* 分配一個(gè)virtio_blk_dev對(duì)象 */
12 bvdev = calloc(1, sizeof(*bvdev));
13 ...
14 vdev = &bvdev->vdev;
15
16 /* 為該virtio對(duì)象綁定用戶態(tài)操作接口,注,該操作接口實(shí)現(xiàn)了virtio 1.0規(guī)范 */
17 rc = virtio_pci_dev_init(vdev, name, pci_ctx);
18 ...
19
20 /* 重置設(shè)備狀態(tài) */
21 rc = virtio_dev_reset(vdev, VIRTIO_BLK_DEV_SUPPORTED_FEATURES);
22 ...
23
24 /* 獲取設(shè)備支持的最大隊(duì)列數(shù)。如果支持多隊(duì)列,從設(shè)備的配置寄存器中聊??;否則為1 */
25 /* TODO: add a way to limit usable virtqueues */
26 if (virtio_dev_has_feature(vdev, VIRTIO_BLK_F_MQ)) {
27 virtio_dev_read_dev_config(vdev, offsetof(struct virtio_blk_config, num_queues),
28 &num_queues, sizeof(num_queues));
29 } else {
30 num_queues = 1;
31 }
32
33 /* 初始化隊(duì)列并創(chuàng)建bdev對(duì)象 */
34 rc = virtio_blk_dev_init(bvdev, num_queues);
35 ...
36
37 return bvdev;
38 }
39
40 static int
41 virtio_blk_dev_init(struct virtio_blk_dev *bvdev, uint16_t max_queues)
42 {
43 struct virtio_dev *vdev = &bvdev->vdev;
44 struct spdk_bdev *bdev = &bvdev->bdev;
45 uint64_t capacity, num_blocks;
46 uint32_t block_size;
47 uint16_t host_max_queues;
48 int rc;
49
50 /* 獲取當(dāng)前設(shè)備的塊大小,默認(rèn)為512字節(jié) */
51 if (virtio_dev_has_feature(vdev, VIRTIO_BLK_F_BLK_SIZE)) {
52 virtio_dev_read_dev_config(vdev, offsetof(struct virtio_blk_config, blk_size),
53 &block_size, sizeof(block_size));
54 } else {
55 block_size = 512;
56 }
57
58 /* 獲取設(shè)備容量 */
59 virtio_dev_read_dev_config(vdev, offsetof(struct virtio_blk_config, capacity),
60 &capacity, sizeof(capacity));
61
62 /* `capacity` is a number of 512-byte sectors. */
63 num_blocks = capacity * 512 / block_size;
64
65 /* 獲取最大隊(duì)列數(shù) */
66 if (virtio_dev_has_feature(vdev, VIRTIO_BLK_F_MQ)) {
67 virtio_dev_read_dev_config(vdev, offsetof(struct virtio_blk_config, num_queues),
68 &host_max_queues, sizeof(host_max_queues));
69 } else {
70 host_max_queues = 1;
71 }
72
73 if (virtio_dev_has_feature(vdev, VIRTIO_BLK_F_RO)) {
74 bvdev->readonly = true;
75 }
76
77 /* bdev is tied with the virtio device; we can reuse the name */
78 bdev->name = vdev->name;
79
80 /* 按max_queues分配隊(duì)列,并啟動(dòng)設(shè)備 */
81 rc = virtio_dev_start(vdev, max_queues, 0);
82 ...
83
84 /* 為bdev對(duì)象賦值 */
85 bdev->product_name = "VirtioBlk Disk";
86 bdev->write_cache = 0;
87 bdev->blocklen = block_size;
88 bdev->blockcnt = num_blocks;
89
90 bdev->ctxt = bvdev;
91 bdev->fn_table = &virtio_fn_table;
92 bdev->module = &virtio_blk_if;
93
94 /* 將virtio_blk_dev添加為一個(gè)io device;其IO Channel創(chuàng)建回調(diào)bdev_virtio_blk_ch_create_cb會(huì)申請(qǐng)一個(gè)
95 virtio的IO環(huán)作為該IO Channel的實(shí)際對(duì)象 */
96 spdk_io_device_register(bvdev, bdev_virtio_blk_ch_create_cb,
97 bdev_virtio_blk_ch_destroy_cb,
98 sizeof(struct bdev_virtio_blk_io_channel));
99
100 /* 注冊(cè)該bdev對(duì)象,便于后續(xù)查找 */
101 rc = spdk_bdev_register(bdev);
102 ...
103
104 return 0;
105 }

【SPDK】六、vhost子系統(tǒng)

vhost子系統(tǒng)在SPDK中屬于應(yīng)用層或叫協(xié)議層,為虛擬機(jī)提供vhost-blk、vhost-scsi和vhost-nvme三種虛擬設(shè)備。這里我們以vhost-blk為分析對(duì)象,來(lái)討論vhost子系統(tǒng)基本原理。

vhost子系統(tǒng)初始化

vhost子系統(tǒng)的描述如下:

spdk/lib/event/subsystems/vhost/vhost.c:

1 static struct spdk_subsystem g_spdk_subsystem_vhost = {
2 .name = "vhost",
3 .init = spdk_vhost_subsystem_init,
4 .fini = spdk_vhost_subsystem_fini,
5 .config = NULL,
6 .write_config_json = spdk_vhost_config_json,
7 };
8
9 static void
10 spdk_vhost_subsystem_init(void)
11 {
12 int rc = 0;
13
14 rc = spdk_vhost_init();
15
16 spdk_subsystem_init_next(rc);
17 }

vhost子系統(tǒng)初始化時(shí),會(huì)依次償試對(duì)vhost-scsi、vhost-blk和vhost-nvme進(jìn)行初始化,如果配置文件中配置了對(duì)應(yīng)類型的設(shè)備,那就會(huì)完成對(duì)應(yīng)設(shè)備的創(chuàng)建并初始化監(jiān)聽(tīng)socket等待qemu客戶端進(jìn)行連接。

spdk/lib/vhost/vhost.c:

1 int
2 spdk_vhost_init(void)
3 {
4 int ret;
5
6 ...
7
8 ret = spdk_vhost_scsi_controller_construct();
9 if (ret != 0) {
10 SPDK_ERRLOG("Cannot construct vhost controllersn");
11 return -1;
12 }
13
14 ret = spdk_vhost_blk_controller_construct();
15 if (ret != 0) {
16 SPDK_ERRLOG("Cannot construct vhost block controllersn");
17 return -1;
18 }
19
20 ret = spdk_vhost_nvme_controller_construct();
21 if (ret != 0) {
22 SPDK_ERRLOG("Cannot construct vhost NVMe controllersn");
23 return -1;
24 }
25
26 return 0;
27 }

vhost-blk初始化

vhost-blk初始化時(shí)主要完成了兩部分工作:一是vhost設(shè)備通用部分,即建立監(jiān)聽(tīng)socket并拉起監(jiān)聽(tīng)線程等待客戶端連接;另一方面是vhost-blk特有的初始化動(dòng)作,即打開(kāi)bdev設(shè)備并建立聯(lián)系:

spdk/lib/vhost/vhost_blk.c:

1 int
2 spdk_vhost_blk_construct(const char *name, const char *cpumask, const char *dev_name, bool readonly)
3 {
4 struct spdk_vhost_blk_dev *bvdev = NULL;
5 struct spdk_bdev *bdev;
6 int ret = 0;
7
8 spdk_vhost_lock();
9
10 /* 首先通過(guò)bdev名稱查找對(duì)應(yīng)的bdev對(duì)象;bdev子系統(tǒng)在vhost子系統(tǒng)之前先完成初始化,正常情況下這里能找到對(duì)應(yīng)的bdev */
11 bdev = spdk_bdev_get_by_name(dev_name);
12 ...
13
14 bvdev = spdk_dma_zmalloc(sizeof(*bvdev), SPDK_CACHE_LINE_SIZE, NULL);
15 ...
16
17 /* 打開(kāi)對(duì)應(yīng)的bdev,并將句柄記錄到bvdev->bdev_desc中 */
18 ret = spdk_bdev_open(bdev, true, bdev_remove_cb, bvdev, &bvdev->bdev_desc);
19 ...
20
21 bvdev->bdev = bdev;
22 bvdev->readonly = readonly;
23
24 /* 完成vhost設(shè)備通用部分功能的初始化,并將該vhost設(shè)備的backend操作集合設(shè)為vhost_blk_device_backend;
25 說(shuō)明:不同的vhost類型實(shí)現(xiàn)了不同的backend,以完成不同類型特定的一些操作過(guò)程。我們?cè)诤罄m(xù)分析客戶端連接
26 操作時(shí)會(huì)深入分析backend的實(shí)現(xiàn) */
27 ret = spdk_vhost_dev_register(&bvdev->vdev, name, cpumask, &vhost_blk_device_backend);
28 ...
29
30 spdk_vhost_unlock();
31 return ret;
32 }

vhost設(shè)備初始化主要提供了一個(gè)可供客戶端(如qemu)連接的socket,并遵循vhost協(xié)議實(shí)現(xiàn)連接服務(wù),這部分功能也是DPDK中已實(shí)現(xiàn)的功能,SPDK直接借用了相關(guān)代碼:

spdk/lib/vhost/vhost.c:

1 int
2 spdk_vhost_dev_register(struct spdk_vhost_dev *vdev, const char *name, const char *mask_str,
3 const struct spdk_vhost_dev_backend *backend)
4 {
5 char path[PATH_MAX];
6 struct stat file_stat;
7 struct spdk_cpuset *cpumask;
8 int rc;
9
10
11 /* 將配置文件中讀取的mask_str轉(zhuǎn)換成位圖記錄到cpumask中,代表該vhost設(shè)備可以綁定的CPU核范圍 */
12 cpumask = spdk_cpuset_alloc();
13 ...
14 if (spdk_vhost_parse_core_mask(mask_str, cpumask) != 0) {
15 ...
16 }
17 ...
18
19 /* 生成socket文件路徑名,規(guī)則是設(shè)備路徑名(vhost命令啟動(dòng)時(shí)-S參數(shù)指定)加上vhost對(duì)象名稱,
20 例如 “/var/tmp/vhost.2” */
21 if (snprintf(path, sizeof(path), "%s%s", dev_dirname, name) >= (int)sizeof(path)) {
22 ...
23 }
24 ...
25
26 /* 生成socket監(jiān)聽(tīng)句柄 */
27 if (rte_vhost_driver_register(path, 0) != 0) {
28 ...
29 }
30 if (rte_vhost_driver_set_features(path, backend->virtio_features) ||
31 rte_vhost_driver_disable_features(path, backend->disabled_features)) {
32 ...
33 }
34
35 /* 注冊(cè)socket連接建立后的消息處理notify_op回調(diào) */
36 if (rte_vhost_driver_callback_register(path, &g_spdk_vhost_ops) != 0) {
37 ...
38 }
39
40 /* 拉起一個(gè)監(jiān)聽(tīng)線程,開(kāi)始等待客戶連接請(qǐng)求 */
41 if (spdk_call_unaffinitized(_start_rte_driver, path) == NULL) {
42 ...
43 }
44
45 vdev->name = strdup(name);
46 vdev->path = strdup(path);
47 vdev->id = ctrlr_num++;
48 vdev->vid = -1; /* 代表客戶端連接對(duì)象,在客戶端連接過(guò)程中生成 */
49 vdev->lcore = -1; /* 代表當(dāng)前vhost設(shè)備綁定到哪個(gè)核上運(yùn)行,也是在客戶端連接后請(qǐng)求處理過(guò)程中生成 */
50 vdev->cpumask = cpumask;
51 vdev->registered = true;
52 vdev->backend = backend;
53
54 ...
55
56 TAILQ_INSERT_TAIL(&g_spdk_vhost_devices, vdev, tailq);
57
58 return 0;
59 }

_start_rte_driver會(huì)拉起一個(gè)監(jiān)聽(tīng)線程執(zhí)行fdset_event_dispatch函數(shù),該函數(shù)等待客戶端的連接請(qǐng)求。當(dāng)qemu向socket發(fā)起連接請(qǐng)求時(shí),監(jiān)聽(tīng)線程收到該請(qǐng)求并調(diào)用vhost_user_server_new_connection建立一個(gè)新的連接,然后在新的連接上等待客戶端發(fā)消息。收到消息時(shí),監(jiān)聽(tīng)線程會(huì)調(diào)用vhost_user_read_cb函數(shù)處理消息。消息的處理代表了vhost協(xié)議的基本原理,我們將在后續(xù)獨(dú)立的博文介紹。

【SPDK】七、vhost客戶端連接請(qǐng)求處理

vhost客戶端連接后,將遵循vhost協(xié)議進(jìn)行一系統(tǒng)復(fù)雜的消息傳遞與處理過(guò)程,最終服務(wù)端將生成一個(gè)可處理IO環(huán)中請(qǐng)求并返回響應(yīng)的處理線程。本篇博文將分析其中最為重要兩類消息的處理原理:內(nèi)存映射消息和IO環(huán)信息傳遞消息。最后將一起來(lái)看一下vhost通用消息處理完成后,vhost-blk設(shè)備是如何完成最后的初始化動(dòng)作的(其它類型的vhost設(shè)備大家可以自行閱讀代碼分析)。

vhost內(nèi)存映射

vhost的reactor線程在處理IO請(qǐng)求時(shí),需要訪問(wèn)虛擬機(jī)的內(nèi)存空間。我們知道,虛擬機(jī)可見(jiàn)的內(nèi)存是由qemu進(jìn)程分配的,通過(guò)KVM內(nèi)核模塊將內(nèi)存映射關(guān)系記錄到EPT頁(yè)表中(CPU硬件提供的地址轉(zhuǎn)換功能),以此實(shí)現(xiàn)從GPA(Guest Physical Address)到HPA(Host Physical Address)的轉(zhuǎn)換。同時(shí)qemu分配的這部分內(nèi)存會(huì)映射到qemu虛擬地址空間中(Qemu Virtual Adress),以便qemu進(jìn)程中IO線程可以訪問(wèn)虛擬機(jī)內(nèi)存。映射關(guān)系如下圖所示:

圖片

SPDK中vhost進(jìn)程將取代qemu IO線程對(duì)IO進(jìn)行處理,因此它也需要將虛擬機(jī)可見(jiàn)地址映射到自身的虛擬地址空間中(Vhost Virtual Address),并記錄VVA到HPA的映射關(guān)系,便于將HPA發(fā)送給物理存儲(chǔ)控制器進(jìn)行DMA操作。

vhost進(jìn)程映射虛擬機(jī)地址的基本原理就是通過(guò)大頁(yè)內(nèi)存的mmap系統(tǒng)調(diào)用:

  • qemu進(jìn)程通過(guò)大頁(yè)文件(/dev/hugepages/xxx)為虛擬機(jī)申請(qǐng)內(nèi)存,然后將大頁(yè)文件句柄傳遞給vhost進(jìn)程;
  • vhost進(jìn)程接收句柄后,會(huì)識(shí)別到qemu創(chuàng)建的大頁(yè)文件(/dev/hugepages/xxx),然后調(diào)用mmap系統(tǒng)調(diào)用將該大頁(yè)文件映射到自身虛擬地址空間中。

下面我們結(jié)合代碼,再來(lái)深入理解一下內(nèi)存映射過(guò)程。首先qemu連接vhost進(jìn)程后,會(huì)通過(guò)發(fā)送VHOST_USER_SET_MEM_TABLE消息傳遞qemu內(nèi)部的內(nèi)存映射信息,vhost對(duì)該消息的處理過(guò)程如下:

spdk/lib/vhost/rte_vhost/vhost_user.c:

1 static int
2 vhost_user_set_mem_table(struct virtio_net *dev, struct VhostUserMsg *pmsg)
3 {
4 uint32_t i;
5
6 memcpy(&dev->mem_table, &pmsg->payload.memory, sizeof(dev->mem_table));
7 memcpy(dev->mem_table_fds, pmsg->fds, sizeof(dev->mem_table_fds));
8 dev->has_new_mem_table = 1;
9
10 ...
11 return 0;
12 }

從上述代碼,我們可以看到這里僅是簡(jiǎn)單地將socket消息中內(nèi)容復(fù)制到dev對(duì)象中。注意一點(diǎn),這里的dev代表客戶端對(duì)象;對(duì)象類型名為virtio_net是由于這部分代碼完全借用自DPDK導(dǎo)致,并不是說(shuō)客戶端是一個(gè)virtio_net對(duì)象。

后續(xù)在進(jìn)行g(shù)pa地址轉(zhuǎn)換前,后續(xù)通過(guò)vhost_setup_mem_table完成內(nèi)存映射:

spdk/lib/vhost/rte_vhost/vhost_user.c:

1 static int
2 vhost_setup_mem_table(struct virtio_net *dev)
3 {
4 struct VhostUserMemory memory = dev->mem_table;
5 struct rte_vhost_mem_region *reg;
6 void *mmap_addr;
7 uint64_t mmap_size;
8 uint64_t mmap_offset;
9 uint64_t alignment;
10 uint32_t i;
11 int fd;
12
13 ...
14 dev->mem = rte_zmalloc("vhost-mem-table", sizeof(struct rte_vhost_memory) +
15 sizeof(struct rte_vhost_mem_region) * memory.nregions, 0);
16 dev->mem->nregions = memory.nregions;
17
18 for (i = 0; i < memory.nregions; i++) {
19 fd = dev->mem_table_fds[i]; /* 取出大頁(yè)文件句柄,注,這里是經(jīng)過(guò)內(nèi)核處理后的句柄,不是qemu中的原始句柄號(hào) */
20 reg = &dev->mem->regions[i];
21
22 reg->guest_phys_addr = memory.regions[i].guest_phys_addr; /* 虛擬機(jī)物理內(nèi)存地址,gpa*/
23 reg->guest_user_addr = memory.regions[i].userspace_addr; /* qemu中的虛擬地址,qva*/
24 reg->size = memory.regions[i].memory_size; /* 內(nèi)存段大小 */
25 reg->fd = fd;
26
27 mmap_offset = memory.regions[i].mmap_offset; /* 映射段內(nèi)偏移,通常為零 */
28 mmap_size = reg->size + mmap_offset; /* 映射段大小 */
29
30 ...
31
32 /* 將大頁(yè)文件重新映射到當(dāng)前進(jìn)程中 */
33 mmap_addr = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE, fd, 0);
34
35 reg->mmap_addr = mmap_addr;
36 reg->mmap_size = mmap_size;
37 reg->host_user_addr = (uint64_t)(uintptr_t)mmap_addr + mmap_offset; /* vhost虛擬地址,vva */
38
39 ...
40 }
41
42 return 0;
43 }

vhost IO環(huán)信息傳遞

vhost內(nèi)存映射完成后,便可進(jìn)行IO環(huán)信息的傳遞,處理完成后使得vhost進(jìn)程可以訪問(wèn)IO環(huán)中信息。

這里注意一點(diǎn),vhost在處理IO環(huán)相關(guān)消息時(shí),首先會(huì)通過(guò)vhost_user_check_and_alloc_queue_pair來(lái)創(chuàng)建IO環(huán)相關(guān)對(duì)象。IO環(huán)相關(guān)的消息主要有VHOST_USER_SET_VRING_NUM、VHOST_USER_SET_VRING_ADDR、VHOST_USER_SET_VRING_BASE、VHOST_USER_SET_VRING_KICK、VHOST_USER_SET_VRING_CALL,這里我們重點(diǎn)分析一下VHOST_USER_SET_VRING_ADDR消息的處理:

spdk/lib/vhost/rte_vhost/vhost_user.c:

1 static int
2 vhost_user_set_vring_addr(struct virtio_net *dev, VhostUserMsg *msg)
3 {
4 struct vhost_virtqueue *vq;
5 uint64_t len;
6
7 /* 如果還未完成vhost內(nèi)存的映射,則先進(jìn)行內(nèi)存映射,可參考前文分析 */
8 if (dev->has_new_mem_table) {
9 vhost_setup_mem_table(dev);
10 dev->has_new_mem_table = 0;
11 }
12 ...
13
14 /* 根據(jù)消息中的索引找到對(duì)應(yīng)的vq對(duì)象 */
15 vq = dev->virtqueue[msg->payload.addr.index];
16
17 /* The addresses are converted from QEMU virtual to Vhost virtual. */
18 len = sizeof(struct vring_desc) * vq->size;
19 /* 將消息中包含的desc數(shù)組的qva地址轉(zhuǎn)換成vva地址,便于vhost線程后續(xù)訪問(wèn)IO環(huán)中desc數(shù)組中內(nèi)容 */
20 vq->desc = (struct vring_desc *)(uintptr_t)qva_to_vva(dev, msg->payload.addr.desc_user_addr, &len);
21
22 dev = numa_realloc(dev, msg->payload.addr.index);
23 vq = dev->virtqueue[msg->payload.addr.index];
24
25 /* 同理將avail數(shù)組的qva地址轉(zhuǎn)換成vva地址 */
26 len = sizeof(struct vring_avail) + sizeof(uint16_t) * vq->size;
27 vq->avail = (struct vring_avail *)(uintptr_t)qva_to_vva(dev, msg->payload.addr.avail_user_addr, &len);
28
29 /* 同理將used數(shù)組的qva地址轉(zhuǎn)換成vva地址 */
30 len = sizeof(struct vring_used) + sizeof(struct vring_used_elem) * vq->size;
31 vq->used = (struct vring_used *)(uintptr_t)qva_to_vva(dev, msg->payload.addr.used_user_addr, &len);
32
33 ...
34 return 0;
35 }

vhost-blk回調(diào)處理

vhost設(shè)備完成內(nèi)存映射及IO環(huán)信息傳遞動(dòng)作后,就進(jìn)行不同vhost設(shè)備特有的初始化動(dòng)作:

spdk/lib/vhost/rte_vhost/vhost_user.c:

1 int
2 vhost_user_msg_handler(int vid, int fd)
3 {
4
5 /* 從socket句柄中讀取消息 */
6 ret = read_vhost_message(fd, &msg);
7 ...
8
9 /* 如果消息中涉及IO環(huán)則先創(chuàng)建IO環(huán)對(duì)象 */
10 ret = vhost_user_check_and_alloc_queue_pair(dev, &msg);
11
12 /* 根據(jù)不同的消息類型進(jìn)行處理 */
13 switch (msg.request) {
14 case VHOST_USER_GET_CONFIG:
15 ...
16 }
17
18 if (!(dev->flags & VIRTIO_DEV_RUNNING) && virtio_is_ready(dev)) {
19 dev->flags |= VIRTIO_DEV_READY;
20
21 if (!(dev->flags & VIRTIO_DEV_RUNNING)) {
22
23 /* 通過(guò)notify_ops回調(diào)設(shè)備相關(guān)的初始化函數(shù) */
24 if (dev->notify_ops->new_device(dev->vid) == 0)
25 dev->flags |= VIRTIO_DEV_RUNNING;
26 }
27 }
28
29 return 0;
30 }

g_spdk_vhost_ops的new_device函數(shù)指向start_device,這里仍是vhost設(shè)備通用的初始化邏輯:

spdk/lib/vhost/vhost.c:

1 static int
2 start_device(int vid)
3 {
4 struct spdk_vhost_dev *vdev;
5 int rc = -1;
6 uint16_t i;
7
8 /* 根據(jù)客戶端vid找到對(duì)應(yīng)的vhost_dev設(shè)備 */
9 vdev = spdk_vhost_dev_find_by_vid(vid);
10
11 /* 將客戶端對(duì)象(virtio_net)中記錄的IO環(huán)信息同步一份到vhost_dev中,后續(xù)IO處理時(shí)主要操作vhost_dev對(duì)象 */
12 vdev->max_queues = 0;
13 memset(vdev->virtqueue, 0, sizeof(vdev->virtqueue));
14 for (i = 0; i < SPDK_VHOST_MAX_VQUEUES; i++) {
15 if (rte_vhost_get_vhost_vring(vid, i, &vdev->virtqueue[i].vring)) {
16 continue;
17 }
18
19 if (vdev->virtqueue[i].vring.desc == NULL ||
20 vdev->virtqueue[i].vring.size == 0) {
21 continue;
22 }
23
24 /* Disable notifications. */
25 if (rte_vhost_enable_guest_notification(vid, i, 0) != 0) {
26 SPDK_ERRLOG("vhost device %d: Failed to disable guest notification on queue %"PRIu16"n", vid, i);
27 goto out;
28 }
29
30 vdev->max_queues = i + 1;
31 }
32
33 /* 同理,將客戶端對(duì)象中的內(nèi)存映射表同步一份到vhost_dev中 */
34 if (rte_vhost_get_mem_table(vid, &vdev->mem) != 0) {
35
36 }
37
38 /* 為vhost_dev對(duì)象分配一個(gè)運(yùn)行核 */
39 vdev->lcore = spdk_vhost_allocate_reactor(vdev->cpumask);
40
41 /* 記錄該vdev對(duì)象內(nèi)存表中虛擬地址到物理地址的映射關(guān)系,后續(xù)操作物理DMA時(shí)可用 */
42 spdk_vhost_dev_mem_register(vdev);
43
44 /* 向vhost_dev對(duì)象的運(yùn)行核發(fā)送一個(gè)事件,使該核上的reactor線程可以執(zhí)行backend的start_device函數(shù) */
45 rc = spdk_vhost_event_send(vdev, vdev->backend->start_device, 3, "start device");
46 ...
47
48 return rc;
49 }

vhost_dev的運(yùn)行核上的reactor線程會(huì)執(zhí)行backend的start_device,即spdk_vhost_blk_start:

spdk/lib/vhost/vhost_blk.c:

1 static int
2 spdk_vhost_blk_start(struct spdk_vhost_dev *vdev, void *event_ctx)
3 {
4 struct spdk_vhost_blk_dev *bvdev;
5 int i, rc = 0;
6
7 bvdev = to_blk_dev(vdev);
8 ...
9
10 /* 為vhost設(shè)備中的每個(gè)隊(duì)列分配task數(shù)組,task與隊(duì)列中元素個(gè)數(shù)相同,一一對(duì)應(yīng) */
11 rc = alloc_task_pool(bvdev);
12 ...
13
14 if (bvdev->bdev) {
15 /* 為vhost_blk對(duì)應(yīng)申請(qǐng)IO Channel,此時(shí)已確定執(zhí)行線程上下文 */
16 bvdev->bdev_io_channel = spdk_bdev_get_io_channel(bvdev->bdev_desc);
17 ...
18 }
19
20 /* 在當(dāng)前reactor線程中添加一個(gè)poller,用來(lái)處理IO環(huán)中的所有請(qǐng)求 */
21 bvdev->requestq_poller = spdk_poller_register(bvdev->bdev ? vdev_worker : no_bdev_vdev_worker, bvdev, 0);
22 ...
23 return rc;
24 }

至此,SPDK中vhost進(jìn)程的初始化流程已介紹完畢,過(guò)程非常漫長(zhǎng),大家可以在對(duì)數(shù)據(jù)面的處理流程有一定的熟悉之后再來(lái)閱讀分析這部分代碼,這樣可以理解得更深刻。

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問(wèn)題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • 存儲(chǔ)
    +關(guān)注

    關(guān)注

    13

    文章

    4265

    瀏覽量

    85676
  • 虛擬化
    +關(guān)注

    關(guān)注

    1

    文章

    367

    瀏覽量

    29774
  • 源碼
    +關(guān)注

    關(guān)注

    8

    文章

    633

    瀏覽量

    29140
收藏 人收藏

    評(píng)論

    相關(guān)推薦

    虛擬現(xiàn)實(shí)+工業(yè)該如何發(fā)展?六大應(yīng)用場(chǎng)景搶先看

    的工作方式,成為當(dāng)前虛擬現(xiàn)實(shí)+工業(yè)生 產(chǎn)中最成熟的落地應(yīng)用場(chǎng)景,解決了電網(wǎng)巡檢、管路巡檢等特殊場(chǎng)合的痛點(diǎn)需求。場(chǎng)景三:
    發(fā)表于 09-27 17:37

    AT32系列MCU上Flash模擬EEPRO的應(yīng)用原理和使用方法

    型號(hào)的 MCU 并未搭載片上 EEPROM,但是在此我們將介紹一種使用片上 Flash來(lái)模擬 EEPROM 使用的方法,以作為對(duì)此應(yīng)用需求的補(bǔ)充。本文檔將詳細(xì)闡述 AT32 系列 MCU 上使用片上
    發(fā)表于 11-26 07:15

    數(shù)字電路實(shí)驗(yàn)的虛擬設(shè)計(jì)方案

    數(shù)字電路實(shí)驗(yàn)的虛擬設(shè)計(jì)方案 介紹了虛擬儀器的簡(jiǎn)單使用方法及其在數(shù)字電路實(shí)驗(yàn)教學(xué)中的應(yīng)用, 列舉了幾個(gè)例子, 并通過(guò)虛擬儀器與傳統(tǒng)儀器的比
    發(fā)表于 03-30 16:15 ?20次下載

    簡(jiǎn)要介紹了操作系統(tǒng)虛擬的概念,以及實(shí)現(xiàn)操作系統(tǒng)虛擬的技術(shù)

    本文簡(jiǎn)要介紹了操作系統(tǒng)級(jí)虛擬的概念,并簡(jiǎn)要闡述了實(shí)現(xiàn)操作系統(tǒng)虛擬所用到的技術(shù)Namespace及cgroups的原理及使用方法。
    的頭像 發(fā)表于 01-10 15:00 ?1.3w次閱讀
    簡(jiǎn)要介紹了操作系統(tǒng)<b class='flag-5'>虛擬</b><b class='flag-5'>化</b>的概念,以及實(shí)現(xiàn)操作系統(tǒng)<b class='flag-5'>虛擬</b><b class='flag-5'>化</b>的技術(shù)

    虛擬主機(jī)用途_虛擬主機(jī)使用方法步驟_虛擬主機(jī)如何綁定域名

    為什么要用虛擬主機(jī)呢,因?yàn)樽约嘿?gòu)買服務(wù)器到安裝操作系統(tǒng)和應(yīng)用軟件需要較長(zhǎng)時(shí)間。而租用虛擬主機(jī)通常只需要幾分鐘的時(shí)間可以開(kāi)通,方便用戶的使用。關(guān)于虛擬主機(jī)用途以及使用方法,如何綁定域名等
    發(fā)表于 01-19 09:23 ?2411次閱讀

    超全的SPDK性能評(píng)估指南

    SPDK采用異步I/O(Asynchronous I/O)加輪詢(Polling)的工作模式,通常與Kernel的異步I/O作為對(duì)比。在此,主要介紹通過(guò)使用fio評(píng)估Kernel異步I/O,以及spdk fio_plugin的兩種模式。
    的頭像 發(fā)表于 11-26 09:58 ?8935次閱讀

    如何使用一種形式方法的3D虛擬祭祀場(chǎng)景建模語(yǔ)言與環(huán)境

    針對(duì)現(xiàn)有三維(3D)場(chǎng)景建模方法普遍存在著業(yè)務(wù)耦合度高,復(fù)雜場(chǎng)景對(duì)象屬性和特征描述能力不強(qiáng)、不豐富,不能很好地解決3D虛擬祭祀場(chǎng)景建模的問(wèn)題
    發(fā)表于 01-02 14:13 ?9次下載
    如何使用一種形式<b class='flag-5'>化</b><b class='flag-5'>方法</b>的3D<b class='flag-5'>虛擬</b>祭祀<b class='flag-5'>場(chǎng)景</b>建模語(yǔ)言與環(huán)境

    虛擬現(xiàn)實(shí)頭盔如何_虛擬現(xiàn)實(shí)頭盔的使用方法

    虛擬現(xiàn)實(shí)頭盔如何?虛擬現(xiàn)實(shí)頭盔,即VR頭顯。早期也有VR眼鏡、VR頭盔等稱呼。VR頭顯是一種利用頭戴式顯示器將人的對(duì)外界的視覺(jué)、聽(tīng)覺(jué)封閉,引導(dǎo)用戶產(chǎn)生一種身在虛擬環(huán)境中的感覺(jué)。虛擬現(xiàn)實(shí)
    發(fā)表于 05-27 10:48 ?3250次閱讀

    I/O軟件模擬虛擬和類虛擬

    的標(biāo)準(zhǔn)接口。Virtio成為整個(gè)問(wèn)題的焦點(diǎn):不管是SPDK/vhost、還是vDPA加速,都是圍繞著Virtio接口展開(kāi)。 1 I/O設(shè)備虛擬:從軟件模擬到SR-IOV I/O
    的頭像 發(fā)表于 10-13 11:09 ?2588次閱讀

    DWIN屏使用方法總結(jié)(

    DWIN屏使用方法總結(jié)()DWIN屏使用方法總結(jié)()數(shù)據(jù)幀常用的系統(tǒng)指令常用控件基礎(chǔ)觸控按鍵返回?cái)?shù)據(jù)變量錄入圖標(biāo)變量數(shù)據(jù)變量顯示總結(jié)DWIN屏使
    發(fā)表于 12-31 18:56 ?10次下載
    DWIN屏<b class='flag-5'>使用方法</b>總結(jié)(<b class='flag-5'>下</b>)

    虛擬人+虛擬場(chǎng)景”顛覆傳統(tǒng)直播模式

    元宇宙時(shí)代的趨勢(shì),越來(lái)越多的企業(yè)和品牌正試圖用虛擬數(shù)字人+虛擬場(chǎng)景來(lái)改變以往的營(yíng)銷模式,轉(zhuǎn)向短視頻和直播室,通過(guò)
    的頭像 發(fā)表于 06-27 17:19 ?1523次閱讀

    pwru的使用方法、經(jīng)典場(chǎng)景及實(shí)現(xiàn)原理

    pwru 是 Cilium 推出的基于 eBPF 開(kāi)發(fā)的網(wǎng)絡(luò)數(shù)據(jù)包排查工具,它提供了更細(xì)粒度的網(wǎng)絡(luò)數(shù)據(jù)包排查方案。本文將介紹 pwru 的使用方法和經(jīng)典場(chǎng)景,并介紹其實(shí)現(xiàn)原理。
    的頭像 發(fā)表于 06-28 17:27 ?2010次閱讀

    SPDK Thread模型設(shè)計(jì)與實(shí)現(xiàn) NVMe-oF的使用案例

    SPDK Thread 模型是SPDK誕生以來(lái)十分重要的模塊,它的設(shè)計(jì)確保了spdk應(yīng)用的無(wú)鎖編程模型,本文基于spdk最新的releas
    的頭像 發(fā)表于 07-03 16:20 ?2391次閱讀

    探究I/O虛擬及Virtio接口技術(shù)(

    I/O虛擬是SmartNIC/DPU/IPU中最核心的部分,AWS NITRO就是從I/O硬件虛擬化開(kāi)始,逐漸開(kāi)啟了DPU這個(gè)新處理器類型的創(chuàng)新。而Virtio接口,已經(jīng)是事實(shí)上的云計(jì)算虛擬
    的頭像 發(fā)表于 04-04 17:03 ?2650次閱讀
    探究I/O<b class='flag-5'>虛擬</b><b class='flag-5'>化</b>及Virtio接口技術(shù)(<b class='flag-5'>下</b>)

    XR虛擬拍攝中攝像機(jī)的使用方法

    XR虛擬拍攝是一種新型的拍攝技術(shù),它結(jié)合了虛擬現(xiàn)實(shí)技術(shù)、計(jì)算機(jī)技術(shù)和影像技術(shù),讓攝影師能夠虛擬環(huán)境中進(jìn)行拍攝,創(chuàng)造出更加逼真、立體的影像效果。
    的頭像 發(fā)表于 07-24 17:51 ?1240次閱讀