[轉自:
vhost 代碼分析: 吳斌的博客 https://rootw.github.io/2018/05/SPDK-all/
spdk vhost vring ,熱升級/遷移 https://testerhome.com/topics/19355
qemu vhost-user protocol https://git.qemu.org/?p=qemu.git;a=blob;f=docs/specs/vhost-user.txt;h=2c8e9347ccee80d11b5e740b717093c1e536af02;hb=HEAD]
一.概述
隨着越來越多公有雲服務提供商采用SPDK技術作為其高性能雲存儲的核心技術之一,intel推出的SPDK技術備受業界關注。本篇博文就和大家一起探索SPDK。
什么是SPDK?為什么需要它?
SPDK(全稱Storage Performance Development Kit),提供了一整套工具和庫,以實現高性能、擴展性強、全用戶態的存儲應用程序。它是繼DPDK之后,intel在存儲領域推出的又一項顛覆性技術,旨在大幅縮減存儲IO棧的軟件開銷,從而提升存儲性能,可以說它就是為了存儲性能而生。
為便於大家理解,我們先介紹一下SPDK在虛擬化場景下的使用方法,以給大家一些直觀的認識。
1. DPDK的編譯與安裝
SPDK使用了DPDK中一些通用的功能和機制,因此首先需要下載DPDK的源碼並完成編譯和安裝:
[root@linux:~/DPDK]# make config T=x86_64-native-linuxapp-gcc
[root@linux:~/DPDK]# make
[root@linux:~/DPDK]# make install (默認安裝到/usr/local,包括.a庫文件和頭文件)
2. SPDK的編譯
[root@linux:~/SPDK]# ./configure –with-dpdk=/usr/local
[root@linux:~/SPDK]# make
編譯成功后,我們在spdk/app/vhost目錄下可以看到一個名為vhost的可執行文件,它就是SPDK在虛擬化場景下為虛擬機模擬程序qemu提供的存儲轉發服務,借此為虛擬機用戶帶來高性能的虛擬磁盤。
3. 大頁內存配置
SPDK vhost進程和qemu進程通過大頁共享虛擬機可見內存,因此需要進行一些大頁的配置和調整:
- 可通過設置/sys/kernel/mm/hugepages/hugepages-xxx/nr_hugepages來調整大頁數量(xxx通常為2M或1G)
- qemu使用掛載到/dev/hugepages目錄下的hugetlbfs來使用大頁內存,可在掛載參數中指定大頁大小,如mount -t hugetlbfs -o pagesize=1G nodev /dev/hugepages
4. vhost配置與啟動
[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命令執行過程中,是一個常駐的服務進程;-S參數指定了socket文件的生成的目錄,每個虛擬磁盤(vhost-blk)或虛擬存儲控制器(vhost-scsi)都會在該目錄下產生一個socket文件,以便qemu程序與vhost進程建立連接;-m參數指定了vhost進程中的輪循線程所綁定的物理CPU核,例如0x3代表在0號和1號核上各綁定一個輪循線程;-c參數指定了vhost進程所需的配置文件,例如這里我通過內存設備(SPDK中稱之為Malloc設備)提供了一個vhost-blk磁盤:
[root@linux:~/SPDK]# cat etc/spdk/rootw.conf
[root@linux:~/SPDK]#
[Malloc]
NumberOfLuns 1 #創建一個內存設備,默認名稱為Malloc0
LunSizeInMB 128 #該內存設備大小為128M
BlockSize 4096 #該內存設備塊大小為4096字節
[VhostBlk0]
Name vhost.2 #創建一個vhost-blk設備,名稱為vhost.2
Dev Malloc0 #該設備后端對應的物理設備為Malloc0
Cpumask 0x1 #將該設備綁定到0號核的輪循線程上
5. 虛擬機啟動與驗證
vhost進程啟動后,我們就可以拉起qemu進程來啟動一個新虛擬機,qemu進程的命令行參數如下(重點關注與SPDK vhost相關部分):
[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
通過上述啟動參數,我們可以看出:
- vhost進程和qemu進程通過大頁方式共享虛擬機可見的所有內存(原因我們將在深入分析時討論)
- qemu在配置vhost-user-blk-pci設備時,只需要指定vhost生成的socket文件即可(-S參數指定的路徑后拼接上設備名稱)
虛擬機啟動成功后,我們通過vnc工具登陸虛擬機,執行lsblk命令可以查看到vda和vdb兩個virtio-blk塊設備,表明vhost后端已成功生效。這里要說明一下,qemu中配置的virtio-blk-pci設備、vhost-user-blk-pci設備或vhost-blk-pci設備,在虛擬機內部均呈現為virtio-blk-pci設備,因此在虛擬機中采用相同的virtio-blk-pci和virtio-blk驅動進行使能,如此一來不同的后端實現技術在虛擬機內部均采用一套驅動,可以減少驅動的開發和維護工作量。
如何實現SPDK?
SPDK能實現高性能,得益於以下三個關鍵技術:
- 全用戶態,它把所有必要的驅動全部移到了用戶態,避免了系統調用的開銷並真正實現內存零拷貝
- 輪循模式,針對高速物理存儲設備,采用輪循的方式而非中斷通知方式判斷請求完成,大大降低時延並減少性能波動
- 無鎖機制,在IO路徑上避免采用任何鎖機制進行同步,降低時延並提升吞吐量
下面我們將深入到SPDK的實現細節,去看看這些關鍵點分別是如何提升性能的。
1. 整體架構
首先,我們來了解一下SPDK內部的整體組件架構:

SPDK整體分為三層:
- 存儲協議層(Storage Protocols),指SPDK支持存儲應用類型。iSCSI Target對外提供iSCSI服務,用戶可以將運行SPDK服務的主機當前標准的iSCSI存儲設備來使用;vhost-scsi或vhost-blk對qemu提供后端存儲服務,qemu可以基於SPDK提供的后端存儲為虛擬機掛載virtio-scsi或virtio-blk磁盤;NVMF對外提供基於NVMe協議的存儲服務端。注意,圖中vhost-blk在spdk-18.04版本中已實現,后面我們主要基於此版本進行代碼分析。
- 存儲服務層(Storage Services),該層實現了對塊和文件的抽象。目前來說,SPDK主要在塊層實現了QoS特性,這一層整體上還是非常薄的。
- 驅動層(drivers),這一層實現了存儲服務層定義的抽象接口,以對接不同的存儲類型,如NVMe,RBD,virtio,aio等等。圖中把驅動細分成兩層,和塊設備強相關的放到了存儲服務層,而把和硬件強相關部分放到了驅動層。
2. 深入數據面
接下來我們將以SPDK前端配置成vhost-blk、后端配置成NVMe SSD場景為例,來分析整個數據面流程。我們將分兩部分完成數據面的分析:
3. 深入管理面
管理面流程比數據面要復雜得多,也無趣得多。因此我們在分析完數據面流程之后,再回頭看看數據面中涉及的各個對象分別是如何被創建和初始化的,這樣更利於我們理解這樣做的目的,也不會一下子就被這些復雜的流程嚇住而無法堅持往下分析。
整個管理面功能包含vhost啟動初始化和通過rpc動態管理兩個部分,這里我們主要討化啟動初始化,根據啟動時的先后順序,分為
【SPDK】二、IO棧對比與線程模型
這里我們以SPDK前端配置成vhost-blk、后端配置成NVMe SSD場景為例,來分析SPDK的IO棧和線程模型。
IO棧對比與時延分析
我們先來對比一下qemu使用普通內核NVMe驅動和使用SPDK vhost時IO棧的差別,如下圖所示:

無論使用傳統內核NVMe驅動,還是使用vhost,虛擬機內部的IO處理流程都是一樣的:IO請求下發時需要從用戶態應用程序中切換到內核態,並穿過文件系統和virtio-blk驅動后,才能借助IO環(IO Ring)將請求信息傳遞給虛擬設備進行處理;虛擬設備處理完成后,以中斷方式通知虛擬機,虛擬機內進過驅動和文件系統的回調后,最終喚醒應用程序返回用戶態繼續執行業務邏輯。在intel Xeon E5620@2.4GHz服務器上的測試結果表明,虛擬機內部的請求下發與響應處理總時延約15us。
針對傳統內核NVMe驅動,qemu進程中io線程負責處理虛擬機下發的IO請求:它通過virtio backend從IO環中取出請求,並將請求通過系統調用傳遞給內核塊層和NVMe驅動層進行處理,最后由NVMe驅動將請求通過Queue Pair(類似IO環)交由物理NVMe控制器進行處理;NVMe控制器處理完成后以物理中斷方式通知qemu io線程,由它將響應放入虛擬機IO環中並以虛擬中斷通知虛擬機請求完成。在此我們看到,qemu中總共的處理時延約15us,而NVMe硬件(華為ES3000 NVMe SSD)上的處理時延才10us(讀請求)。
針對SPDK vhost,qemu進程不參與IO請求的處理(僅在初始化時起作用),所有虛擬機下發的IO請求均由vhost進程處理。vhost進程以輪循的方式不斷從IO環中取出請求(意味着虛擬機下發IO請求時,不用通知虛擬設備),對於取出的每個請求,vhost將其以任務方式交給bdev抽象層進行處理;bdev根據后端設備的類型來選擇不同的驅動進行處理,例如對於NVMe設備,將使用用戶態的NVMe驅動在用戶空間完成對Queue Pair的操作。vhost進程同樣會輪循物理NVMe設備的Queue Pair,如果有響應例會立刻進行處理,而無須等待物理中斷。vhost在處理NVMe響應過程中,會向虛擬機IO環中添加響應,並以虛擬中斷方式通知虛擬機。我們可以看到,vhost中絕大部分操作都是在用戶態完成的(中斷通知虛擬機時會進入內核態通過KVM模塊完成),各層時延均非常短,app和bdev抽象層約2us,NVMe用戶態驅動約2us。
因此,端到端時延對比來看,我們可以發現傳統NVMe IO棧的總時延約40us,而SPDK用戶態NVMe IO棧時延不到30us,時延上有25%以上的優化。另一方面,在吞吐量(IOPS)方面,如果我們給virtio-blk設備配置多隊列(確保虛擬機IO壓力足夠),並在后端NVMe設備不成為瓶頸的前提下,傳統NVMe IO棧在單個qemu io線程處理時,最多能達到20萬IOPS,而SPDK vhost在單線程處理時可達100萬IOPS,同等CPU開銷下,吞吐量上有5倍以上的性能提升。傳統NVMe IO棧在處理多隊列模型時,相比單隊列模型,減少了線程間通知開銷,一次通知可以處理多個IO請求,因此多隊列相比單隊列模型會有較大的IOPS提升;而vhost得益於全用戶態及輪循模式,進一步減少了內核切換和通知開銷,帶來了吞吐量的大幅提升。
線程模型分析
在了解了SPDK的IO棧之后,我們進一步來分析一下vhost進程的線程模型,如下圖所示。圖中示例場景為,一台服務器上插了一張NVMe SSD卡,卡上划分了三個namespace;三個namespace分別配給了三台虛擬機的vhost-user-blk-pci設備。

vhost進程啟動時可以配置多個輪循線程(reactor),每個線程綁定一個物理CPU。在示例場景下,我們假設配置了兩個輪循線程reactor_0和reactor_1,分別對應物理CPU0和物理CPU1。每配置一個vhost-blk設備時,同樣要為該設備綁定物理核,並且只能綁定到一個物理核上,例如這里我們假設vm1的vhost-blk設備綁定到CPU0,vm2和vm3綁定到CPU1。那么reactor_0將輪循vm1中vhost-blk的IO環,reactor_1將依次輪循vm2和vm3的IO環。
vhost線程在操作相同NVMe控制器下的namespace時,不同的vhost線程會申請不同的IO Channel(實際對應NVMe Queue Pair,作用類似虛擬機IO環),並且每個線程都會輪循各自申請的IO Channel中的響應消息。例如圖中reactor_0會向NVMe控制器申請QueuePair1,並在輪循過程中注冊對該QueuePair的poller函數(負責從中取響應);reactor_1則會向NVMe控制器申請QueuePair2並輪循該QueuePair。如此一來,就能提升對后端NVMe設備的並發訪問度,充分發揮物理設備的吞吐量優勢。
綜上所述,
- 每個vhost線程都會輪循若干個vhost設備的IO環(一個vhost設備無論有多少個環,都只會在一個線程中處理),並且會向有操作述求的物理存儲控制器(例如NVMe控制器、virtio-blk控制器、virtio-scsi控制器等)申請一個獨立的IO Channel(IO環可以理解為對前端虛擬機呈現的一個IO Channel)並對其進行輪循。
- 無論是前端虛擬機IO環,還是后端IO Channel,都只會在一個vhost線程中被輪循,因此這就避免了多線程並發操作同一個對象,可以通過無鎖的方式操作IO環或IO Channel。
- 針對前端虛擬機來說,一個vhost設備無論有多少個環,都只會在一個vhost線程中處理。這種設計上的約束雖說可以簡化實現,但也帶來了吞吐量性能擴展上的限制,即一個vhost設備在后端物理存儲非瓶頸的前提下,最高的IOPS為100萬。因此我們可以考慮將vhost的多個IO環拆分到多個vhost線程中處理,進一步提升吞吐量。
【SPDK】三、IO流程代碼解析
在分析SPDK數據面代碼之前,需要我們對qemu中實現的IO環以及virtio前后端驅動的實現有所了解(后續我計划出專門的博文來介紹qemu)。這里我們仍以SPDK前端配置vhost-blk,后端對接NVMe SSD為例(有關NVMe驅動涉及較多規范細節,這里也不作過於深入的討論,感興趣的讀者可以結合NVMe規范展開閱讀)進行分析。
總流程
前文在分析SPDK IO棧時已經大致分析了IO處理的調用層次,在此我們進一步打開內部實現細節,更細致地分析一下IO處理流程:

首先,從虛擬機視角來說,它看到的是一個virtio-blk-pci設備,該pci設備內部包含一條virtio總線,其上又連接了virtio-blk設備。qemu在對虛擬機用戶呈現這個virtio-blk-pci設備時,采用的具體設備類型是vhost-user-blk-pci(這是virtio-blk-pci設備的一種后端實現方式。另外兩種是:vhost-blk-pci,由內核實現后端;普通virtio-blk-pci,由qemu實現后端處理),這樣便可與用戶態的SPDK vhost進程建立連接。SPDK vhost進程內部對於虛擬機所見的virtio-blk-pci設備也有一個對象來表示它,這就是spdk_vhost_blk_dev。該對象指向一個bdev對象和一個io channel對象,bdev對象代表真正的后端塊存儲(這里對應NVMe SSD上的一個namespace),io channel代表當前線程訪問存儲的獨立通道(對應NVMe SSD的一個Queue Pair)。這兩個對象在驅動層會進一步擴展新的成員變量,用來表示驅動層可見的一些詳細信息。
其次,當虛擬機往IO環中放入IO請求后,便立刻被vhost進程中的某個reactor線程輪循到該請求(輪循過種中執行函數為vdev_worker)。reactor線程取出請求后,會將其映成一個任務(spdk_vhost_blk_task)。對於讀寫請求,會進一步走到bdev層,將任務封狀成一個bdev_io對象(類似內核的bio)。bdev_io繼續往驅動層遞交,它會擴展為適配具體驅動的io對象,例如針對NVMe驅動,bdev_io將擴展成nvme_bdev_io對象。NVMe驅動會根據nvme_bdev_io對象中的請求內容在當前reactor線程對應的QueuePair中生成一個新的請求項,並通知NVMe控制器有新的請求產生。
最后,當物理NVMe控制器完成IO請求后,會往QueuePair中添加IO響應。該響應信息也會很快被reactor線程輪循到(輪循執行函數為bdev_nvme_poll)。reactor取出響應后,根據其id找到對應的nvme_bdev_io,進一步關聯到對應的bdev_io,再調用bdev_io中的記錄的回調函數。vhost-blk下發請求時注冊的回調函數為blk_request_complete_cb,回調參數為當前的spdk_vhost_blk_task對象。在blk_request_complete_cb中會往虛擬機IO環中放入IO響應,並通過虛擬中斷通知虛擬機IO完成。
IO請求下發流程代碼解析
vhost進程通過vdev_worker函數以輪循方式處理虛擬機下發的IO請求,調用棧如下:
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()
下面我們先來分析一下vhost-blk層的具體代碼實現:
spdk/lib/vhost/vhost-blk.c:
1 /* reactor線程會采用輪循方式周期性地調用vdev_worker函數來處理虛擬機下發的請求 */ 2 static int 3 vdev_worker(void *arg) 4 { 5 /* arg在注冊輪循函數時指定,代表當前操作的vhost-blk對象 */ 6 struct spdk_vhost_blk_dev *bvdev = arg; 7 uint16_t q_idx; 8 9 /* vhost-blk對象bvdev中含有一個抽象的spdk_vhost_dev對象,其內部記錄所有vhost_dev類別對象 10 均含有的公共內容,max_queues代表當前vhost_dev對象共有多少個IO環,virtqueue[]數組記錄了 11 所有的IO環信息 */ 12 for (q_idx = 0; q_idx < bvdev->vdev.max_queues; q_idx++) { 13 /* 根據IO環的個數,依次處理每個環中的請求 */ 14 process_vq(bvdev, &bvdev->vdev.virtqueue[q_idx]); 15 } 16 17 ... 18 19 } 20 21 /* 處理IO環中的所有請求 */ 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 /* 先給出一些關於IO環的知識: 31 (1) 簡單來說,每個IO環分成descriptor數組、avail數組和used數組三個部分,數組元素個數均為環的最大請求個數。 32 (2) descriptor數組元素代表一段虛擬機內存,每個IO請求至少包含三段,請求頭部段、數據段(至少一個)和響應段。 33 請求頭部包含請求類型(讀或寫)、訪問偏移,數據段代表實際的數據存放位置,響應段記錄請求處理結果。一般來說, 34 每個IO請求在descriptor中至少要占據三個元素;不過當配置了indirect特性后,一個IO請求只占用一項,只不過 35 該項指向的內存段又是一個descriptor數組,該數組元素個數為IO請求實際所需內存段。 36 (3) avail數組用來記錄已下發的IO請求,數組元素內容為IO請求在descriptor數組中的下標,該下標可作為請求的id。 37 (4) used數組用來記錄已完成的IO響應,數組元素內容同樣為IO在descritpror數組中的下標。 38 */ 39 40 /* 從IO環的avail數組中中取出一批請求,將請求id放入reqs數組中;每次將環取空或者最多取32個請求 */ 41 reqs_cnt = spdk_vhost_vq_avail_ring_get(vq, reqs, SPDK_COUNTOF(reqs)); 42 ... 43 44 /* 依次對reqs數組中的請求進行處理 */ 45 for (i = 0; i < reqs_cnt; i++) { 46 ... 47 48 /* 以請求id作為下標,找到對應的task對象。注,初始化時,會按IO環的最大請求個數來申請tasks數組 */ 49 task = &((struct spdk_vhost_blk_task *)vq->tasks)[reqs[i]]; 50 ... 51 52 bvdev->vdev.task_cnt++; /* 作統計計數 */ 53 54 task->used = true; /* 代表tasks數組中該項正在被使用 */ 55 task->iovcnt = SPDK_COUNTOF(task->iovs); /* iovs數組將來會記錄IO請求中數據段的內存映射信息 */ 56 task->status = NULL; /* 將來指向IO響應段,用來給虛擬機返回IO處理結果 */ 57 task->used_len = 0; 58 59 /* 將IO環中請求的詳細信息記錄到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環descriptor數組中記錄的請求內存段(以gpa表示,即Guest Physical Address)映成vhost進程中的 76 虛擬地址(vva, vhost virtual address),並保存到task的iovs數組中 */ 77 if (blk_iovs_setup(&bvdev->vdev, vq, task->req_idx, task->iovs, &task->iovcnt, &payload_len)) { 78 ... 79 } 80 81 /* 第一個請求內存段為請求頭部,即struct virtio_blk_outhdr,記錄請求類型、訪問位置信息 */ 82 iov = &task->iovs[0]; 83 ... 84 req = iov->iov_base; 85 86 /* 最后一個請求內存段用來保存請求處理結果 */ 87 iov = &task->iovs[task->iovcnt - 1]; 88 ... 89 task->status = iov->iov_base; 90 91 /* 除去一頭一尾,中間的請求內存段為數據段 */ 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 /* 對於讀寫請求,調用bdev讀寫接口,並注冊請求完成后的回調函數為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環descriptor數組中獲取請求對應的所有內存段信息,並映射成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環中記錄的gpa地址轉換成vhost的虛擬地址,qemu和vhost之間的內存映射關系管理我們將在管理面分析時討論 */ 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層對IO請求的處理,以讀請求為例:
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 /* 將字節轉換成塊進行實際的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是一個線程強相關對象,不同的線程對應不同的channel, 25 這里spdk_bdev_channel包含一個線程獨立的緩存池,先從中申請bdev_io內存(免鎖), 26 如果申請不到,再到全局的mempool中申請內存 */ 27 bdev_io = spdk_bdev_get_io(channel); 28 ... 29 30 /* 將接口參數記錄到bdev_io中,並繼續遞交 */ 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) { /* 開啟了bdev的qos特性時走該流程 */ 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; /* 底層驅動對應的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 /* 不同的驅動在生成bdev對象時會注冊不同的fn_table,這里將調用驅動注冊的submit_request函數 */ 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 }
最后,我們來看一下bdev的NVMe驅動的處理邏輯:
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擴展成具體的nvme_io_channel,其對應一個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 /* 針對讀寫請求,會將bdev_io擴展成nvme_bdev_io請求后,再將請求內容填入io channel 38 對應的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 }
詳細的NVMe請求處理不在本文的討論范圍內,感興趣的讀者可以自行深入分析。
IO響應返回流程代碼解析
reactor線程通過bdev_nvme_poll函數獲知已完成的NVMe響應,最終會調用bdev層的spdk_bdev_io_complete來處理響應:
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 /* 如果請求執行成功,則更新一些統計信息 */ 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 /* 調用上層注冊回調,這里將回到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 /* 往虛擬機中放入響應並以虛擬中斷方式通知虛擬機IO完成 */ 56 spdk_vhost_vq_used_ring_enqueue(&task->bvdev->vdev, task->vq, task->req_idx, 57 task->used_len); 58 59 /* 釋放當前task,實際就是將task->used置為false */ 60 blk_task_finish(task); 61 }
至此,整個IO流程已經分析完畢,可見SPDK對IO的處理還是非常簡潔的,這便是高性能的基石。
【SPDK】四、reactor線程
reactor線程是SPDK中負責實際業務處理邏輯的單元,它們在vhsot服務啟動時創建,直到服務停止。目前還不支持reactor線程的動態增減。
reactor線程總流程
我們順着vhost進程的代碼執行順序來看看總體流程:
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 /* 首先進行參數解析,解析后的結果保存於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 /* 接着根據配置文件指明的物理核啟動reactors線程(主線程最終也成為一個reactor)。 22 這些reactors線程會執行輪循函數,直到外部將服務狀態置為退出 */ 23 24 /* Blocks until the application is exiting */ 25 rc = spdk_app_start(&opts, vhost_started, NULL, NULL); 26 27 /* 所有reactor線程退出后,進行資源清理 */ 28 spdk_app_fini(); 29 30 return rc; 31 }
上述整體流程中最為重要的便是spdk_app_start函數,該函數內部調用了DPDK關於系統CPU、內存、PCI設備管理等通用性服務代碼,這里我們盡可能以理解其功能為主而不做深入的代碼分析:
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 /* 將配置文件中的內容導入到config對象中 */ 14 config = spdk_app_setup_conf(opts->config_file); 15 ... 16 spdk_app_read_config_file_global_params(opts); 17 18 ... 19 20 /* 調用DPDK系統服務: 21 (1)通過內核sysfs獲取物理CPU信息,並通過配置文件指定的運行核,在各個核上啟動服務線程; 22 各服務線程啟動后因為在等待主線程給它們發送需要執行的任務而處於睡眠狀態; 23 (2)基於大頁內存創建內存池以供其它模塊使用; 24 (3)初始化PCI設備枚舉服務,可以實現類似內核的設備發現及驅動初始化流程。SPDK基於此並借 25 助內核uio或vfio驅動實現全用戶態的PCI驅動 */ 26 /* 完成DPDK的初始化后,SPDK會建立一張由vva(vhost virtual address)到pa(physical address) 27 的內存映射表g_vtophys_map。每當有新的內存映射到vhost中時,都需要調用spdk_mem_register在該 28 表中注冊新的映射關系。設計該表的原因是當SPDK向物理設備發送DMA請求時,需要向設備提供pa而非vva */ 29 if (spdk_app_setup_env(opts) < 0) { 30 ... 31 } 32 33 /* 這里為reactors分配相應的內存 */ 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 /* 設置一些全局變量 */ 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的各個子系統,如bdev、vhost均為子系統。但這里需注意一點,此處僅是產生了一個初始化事件,事件的處理要在 58 reactor線程正式進入輪循函數后才開始 */ 59 spdk_subsystem_init(app_start_event); 60 61 /* 從此處開始,各個線程(包括主線程)開始執行_spdk_reactor_run,線程名也正式變更為reactor_X; 62 直到所有線程均退出_spdk_reactor_run后,主線程才會返回 */ 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 /* 針對主線程之外的其它核上的線程,通過發送通知使它們開始執行_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 /* 主線程也會執行_spdk_reactor_run */ 25 /* Start the master reactor */ 26 reactor = spdk_reactor_get(current_core); 27 _spdk_reactor_run(reactor); 28 29 /* 主線程退出后會等待其它核上的線程均退出 */ 30 spdk_env_thread_wait_all(); 31 32 /* 執行到此處,說明vhost服務進程即將退出 */ 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 }
輪循函數_spdk_reactor_run
通過對vhost代碼流程的分析,我們看到vhost中所有線程最終都會調用_spdk_reactor_run,該函數是一個死循環,由此實現輪循邏輯:
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_[核號] */ 14 snprintf(thread_name, sizeof(thread_name), "reactor_%u", reactor->lcore); 15 16 /* 創建SPDK線程對象: 17 (1)線程間通過_spdk_reactor_send_msg發送消息,本質是向接收方的event隊列中添加事件; 18 (2)線程通過_spdk_reactor_start_poller和_spdk_reactor_stop_poller啟動和停止poller; 19 (3)IO Channel等線程相關對象也會記錄到線程對象中 */ 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代表最短輪循時間 */ 28 spin_cycles = SPDK_REACTOR_SPIN_TIME_USEC * spdk_get_ticks_hz() / SPDK_SEC_TO_USEC; 29 /* sleep_cycles代表最長睡眠時間 */ 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 /* 輪循的死循環正式開始 */ 35 while (1) { 36 bool took_action = false; 37 38 /* 首先,每個reactor線程通過DPDK的無鎖隊列實現了一個事件隊列;這里從事件隊列中取出事件並調用事件 39 的處理函數。例如,vhost的子系統的初始化即是在spdk_subsystem_init中產生了一個verify事件並 40 添加到主線程reactor的事件隊列中,該事件處理函數為spdk_subsystem_verify */ 41 event_count = _spdk_event_queue_run_batch(reactor); 42 if (event_count > 0) { 43 took_action = true; 44 } 45 46 /* 接着,每個reactor線程從active_pollers鏈表頭部取出一個poller並調用其fn函數。poller代表一次 47 具體的處理動作,例如處理某個vhost_blk設備的所有IO環中的請求,又或者處理后端NVMe某個queue 48 pair中的所有響應 */ 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線程還實現了定時器邏輯,這里判斷是否有定時器到期;如果確有定時器到期則執行其回調並將 64 其放到定時器隊列尾部 */ 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 /* 下面的邏輯主要用來決定輪循線程是否可以睡眠一會 */ 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)) { /* 保證輪循線程最少已執行了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 } /* 死循環結束 */ 131 132 ... 133 spdk_free_thread(); 134 return 0; 135 }
至此,reactor線程整體執行邏輯已分析完成,后續我們將以verify_event為線索開始分析各個子系統的初始化過程。
【SPDK】五、bdev子系統
SPDK從功能角度將各個獨立的部分划分為“子系統“。例如對各種后端存儲的訪問屬於bdev子系統,又例如對虛擬機呈現各種設備屬於vhost子系統。不同場景下,各種工具可以通過組合不同的子系統來實現各種不同的功能。例如虛擬化場景下,vhost主要集成了bdev、vhost、scsi等子系統。這些子系統存在一定依賴關系,例如vhost子系統依賴bdev,這就需要將被依賴的子系統先初始化完成,才能執行其它子系統的初始化。
本篇博文我們先整體介紹一下SPDK子系統的初始化流程,然后再深入分析一下bdev子系統。vhost子系統我們將在獨立的博文中展開分析。
SPDK子系統
通過前文的分析,我們知道主線程在執行_spdk_reactor_run時,首先處理的事件便是verify事件,該事件處理函數為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 /* 檢查當前應用中所有需要的子系統及其依賴系統是否均已成功注冊 */ 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 missing\n", 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 missing\n", 16 dep->name, dep->depends_on); 17 spdk_app_stop(-1); 18 return; 19 } 20 } 21 22 /* 按依賴關系對所有子系統進行排序 */ 23 subsystem_sort(); 24 25 /* 依據排序依次執行各個子系統的init函數 */ 26 spdk_subsystem_init_next(0); 27 }
bdev子系統
bdev和vhost是虛擬化場景下兩個最為主要的子系統,且vhost依賴bdev,因此我們先來分析一下bdev子系統。
我們可以看到bdev子系統的初始化函數為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子系統針對不同的后端存儲設備實現了不同的“模塊”,例如nvme模塊主要實現了用戶態對nvme設備的訪問操作,virtio實現了用戶態對virtio設備的訪問操作,又例如malloc模塊通過內存實現了一個模擬的塊設備。因此bdev子系統在初始化時主要針對配置文件中已經配置的后端存儲模塊進行初始化操作。
另外,bdev借助IO Channel的概念也實現了系統級的management_channel和模塊級的module_channel。我們知道IO Channel是一個線程相關的概念,management_channel和module_channel也是如此:
-
management_channel是線程唯一的一個對象,不同線程具備不同的的management_channel,同一個線程只有一個。目前management_channel中實現了一個線程內部獨立的內存池,用來緩存bdev_io對象;
-
module_channel是線程內部屬於同一個模塊的bdev所共享的一個對象,用來記錄同一線程中屬於同一模塊的所有對象。例如同一個線程如果操作兩個nvme的bdev對象且這兩個bdev屬於不同的nvme控制器,那么雖然這兩個bdev對應不同的NVMe IO Channel,但是它們屬於同一個module_channel。目前module_channel只含有一個模塊級的引用計數和內存不足時的bdev io臨時隊列(當有內存空間時,實現IO重發)。
每個模塊都會提供一個module_init函數,當bdev子系統初始化時會依次調用這些初始化函數。下面我們以NVMe和virtio兩個模塊為例,來簡要看下模塊的初始化邏輯。
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模塊的初始化函數為bdev_nvme_library_init,另外bdev_nvme_get_ctx_size返回的context大小為nvme_bdev_io的大小。bdev子系統會以所有模塊最大的context大小來創建bdev_io內存池,以此確保為所有模塊申請bdev_io時都能獲得足夠的擴展內存(nvme_bdev_io即是對bdev_io的擴展)。
bdev_nvme_library_init函數從SPDK的配置文件中讀取“Nvme”字段開始的相關信息,並通過這些信息創建一個NVMe控制器並獲取其下的namespace,最后將namespace表示成一個bdev對象。這里我們打開看一下識別到對應NVMe控制器后的回調處理邏輯:
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 /* 首先根據DPDK中PCI驅動框架識別到的NVMe控制器信息來創建一個nvme_ctrlr對象 */ 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控制器對象添加為一個io device;每個io device可申請獨立的IO Channel; 31 bdev_nvme_create_cb負責在IO Channel對象創建時初始化底層驅動相關對象,這里 32 即是獲取一個新的queue pair */ 33 spdk_io_device_register(ctrlr, bdev_nvme_create_cb, bdev_nvme_destroy_cb, 34 sizeof(struct nvme_io_channel)); 35 36 /* 此處開始枚舉nvme控制器下的所有namespace,並將其建為bdev對象。注意一點,此時並不會為 37 bdev申請IO channel,它是vhost子系統初始時,完成線程綁定后才創建的 */ 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初始化時並不調用該函數 */ 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 /* 分配一個nvme queue pair作為該IO Channel的實際對象 */ 58 ch->qpair = spdk_nvme_ctrlr_alloc_io_qpair(ctrlr, NULL, 0); 59 ... 60 /* 向reactor注冊一個poller,輪循新分配queue pair中已完成的響應信息 */ 61 ch->poller = spdk_poller_register(bdev_nvme_poll, ch, 0); 62 return 0; 63 }
類似地,我們再看一下virtio模塊的初始化。
2. virtio模塊初始化
virtio雖說起源於qemu-kvm虛擬化,但是它也是一種可用物理硬件實現的協議規范。因此SPDK也把它當做一種后端存儲類型加以實現。當然,如果SPDK的vhost進程是運行在虛擬機中(而虛擬機virtio設備作為后端存儲),virtio模塊就是一個必不可少的驅動模塊了。
我們以virtio-blk設備為例,來看一下其初始化過程:
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通過配置文件獲取相關配置信息,並同樣借助DPDK的用戶態PCI設備管理框架識別到該設備后,調用virtio_pci_blk_dev_create來創建一個virtio_blk對象:
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 /* 分配一個virtio_blk_dev對象 */ 12 bvdev = calloc(1, sizeof(*bvdev)); 13 ... 14 vdev = &bvdev->vdev; 15 16 /* 為該virtio對象綁定用戶態操作接口,注,該操作接口實現了virtio 1.0規范 */ 17 rc = virtio_pci_dev_init(vdev, name, pci_ctx); 18 ... 19 20 /* 重置設備狀態 */ 21 rc = virtio_dev_reset(vdev, VIRTIO_BLK_DEV_SUPPORTED_FEATURES); 22 ... 23 24 /* 獲取設備支持的最大隊列數。如果支持多隊列,從設備的配置寄存器中聊取;否則為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 /* 初始化隊列並創建bdev對象 */ 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 /* 獲取當前設備的塊大小,默認為512字節 */ 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 /* 獲取設備容量 */ 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 /* 獲取最大隊列數 */ 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分配隊列,並啟動設備 */ 81 rc = virtio_dev_start(vdev, max_queues, 0); 82 ... 83 84 /* 為bdev對象賦值 */ 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添加為一個io device;其IO Channel創建回調bdev_virtio_blk_ch_create_cb會申請一個 95 virtio的IO環作為該IO Channel的實際對象 */ 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 /* 注冊該bdev對象,便於后續查找 */ 101 rc = spdk_bdev_register(bdev); 102 ... 103 104 return 0; 105 }
【SPDK】六、vhost子系統
vhost子系統在SPDK中屬於應用層或叫協議層,為虛擬機提供vhost-blk、vhost-scsi和vhost-nvme三種虛擬設備。這里我們以vhost-blk為分析對象,來討論vhost子系統基本原理。
vhost子系統初始化
vhost子系統的描述如下:
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子系統初始化時,會依次償試對vhost-scsi、vhost-blk和vhost-nvme進行初始化,如果配置文件中配置了對應類型的設備,那就會完成對應設備的創建並初始化監聽socket等待qemu客戶端進行連接。
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 controllers\n"); 11 return -1; 12 } 13 14 ret = spdk_vhost_blk_controller_construct(); 15 if (ret != 0) { 16 SPDK_ERRLOG("Cannot construct vhost block controllers\n"); 17 return -1; 18 } 19 20 ret = spdk_vhost_nvme_controller_construct(); 21 if (ret != 0) { 22 SPDK_ERRLOG("Cannot construct vhost NVMe controllers\n"); 23 return -1; 24 } 25 26 return 0; 27 }
vhost-blk初始化
vhost-blk初始化時主要完成了兩部分工作:一是vhost設備通用部分,即建立監聽socket並拉起監聽線程等待客戶端連接;另一方面是vhost-blk特有的初始化動作,即打開bdev設備並建立聯系:
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 /* 首先通過bdev名稱查找對應的bdev對象;bdev子系統在vhost子系統之前先完成初始化,正常情況下這里能找到對應的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 /* 打開對應的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設備通用部分功能的初始化,並將該vhost設備的backend操作集合設為vhost_blk_device_backend; 25 說明:不同的vhost類型實現了不同的backend,以完成不同類型特定的一些操作過程。我們在后續分析客戶端連接 26 操作時會深入分析backend的實現 */ 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設備初始化主要提供了一個可供客戶端(如qemu)連接的socket,並遵循vhost協議實現連接服務,這部分功能也是DPDK中已實現的功能,SPDK直接借用了相關代碼:
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轉換成位圖記錄到cpumask中,代表該vhost設備可以綁定的CPU核范圍 */ 12 cpumask = spdk_cpuset_alloc(); 13 ... 14 if (spdk_vhost_parse_core_mask(mask_str, cpumask) != 0) { 15 ... 16 } 17 ... 18 19 /* 生成socket文件路徑名,規則是設備路徑名(vhost命令啟動時-S參數指定)加上vhost對象名稱, 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監聽句柄 */ 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 /* 注冊socket連接建立后的消息處理notify_op回調 */ 36 if (rte_vhost_driver_callback_register(path, &g_spdk_vhost_ops) != 0) { 37 ... 38 } 39 40 /* 拉起一個監聽線程,開始等待客戶連接請求 */ 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; /* 代表客戶端連接對象,在客戶端連接過程中生成 */ 49 vdev->lcore = -1; /* 代表當前vhost設備綁定到哪個核上運行,也是在客戶端連接后請求處理過程中生成 */ 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會拉起一個監聽線程執行fdset_event_dispatch函數,該函數等待客戶端的連接請求。當qemu向socket發起連接請求時,監聽線程收到該請求並調用vhost_user_server_new_connection建立一個新的連接,然后在新的連接上等待客戶端發消息。收到消息時,監聽線程會調用vhost_user_read_cb函數處理消息。消息的處理代表了vhost協議的基本原理,我們將在后續獨立的博文介紹。
【SPDK】七、vhost客戶端連接請求處理
vhost客戶端連接后,將遵循vhost協議進行一系統復雜的消息傳遞與處理過程,最終服務端將生成一個可處理IO環中請求並返回響應的處理線程。本篇博文將分析其中最為重要兩類消息的處理原理:內存映射消息和IO環信息傳遞消息。最后將一起來看一下vhost通用消息處理完成后,vhost-blk設備是如何完成最后的初始化動作的(其它類型的vhost設備大家可以自行閱讀代碼分析)。
vhost內存映射
vhost的reactor線程在處理IO請求時,需要訪問虛擬機的內存空間。我們知道,虛擬機可見的內存是由qemu進程分配的,通過KVM內核模塊將內存映射關系記錄到EPT頁表中(CPU硬件提供的地址轉換功能),以此實現從GPA(Guest Physical Address)到HPA(Host Physical Address)的轉換。同時qemu分配的這部分內存會映射到qemu虛擬地址空間中(Qemu Virtual Adress),以便qemu進程中IO線程可以訪問虛擬機內存。映射關系如下圖所示:

SPDK中vhost進程將取代qemu IO線程對IO進行處理,因此它也需要將虛擬機可見地址映射到自身的虛擬地址空間中(Vhost Virtual Address),並記錄VVA到HPA的映射關系,便於將HPA發送給物理存儲控制器進行DMA操作。
vhost進程映射虛擬機地址的基本原理就是通過大頁內存的mmap系統調用:
- qemu進程通過大頁文件(/dev/hugepages/xxx)為虛擬機申請內存,然后將大頁文件句柄傳遞給vhost進程;
- vhost進程接收句柄后,會識別到qemu創建的大頁文件(/dev/hugepages/xxx),然后調用mmap系統調用將該大頁文件映射到自身虛擬地址空間中。
下面我們結合代碼,再來深入理解一下內存映射過程。首先qemu連接vhost進程后,會通過發送VHOST_USER_SET_MEM_TABLE消息傳遞qemu內部的內存映射信息,vhost對該消息的處理過程如下:
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 }
從上述代碼,我們可以看到這里僅是簡單地將socket消息中內容復制到dev對象中。注意一點,這里的dev代表客戶端對象;對象類型名為virtio_net是由於這部分代碼完全借用自DPDK導致,並不是說客戶端是一個virtio_net對象。
后續在進行gpa地址轉換前,后續通過vhost_setup_mem_table完成內存映射:
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]; /* 取出大頁文件句柄,注,這里是經過內核處理后的句柄,不是qemu中的原始句柄號 */ 20 reg = &dev->mem->regions[i]; 21 22 reg->guest_phys_addr = memory.regions[i].guest_phys_addr; /* 虛擬機物理內存地址,gpa*/ 23 reg->guest_user_addr = memory.regions[i].userspace_addr; /* qemu中的虛擬地址,qva*/ 24 reg->size = memory.regions[i].memory_size; /* 內存段大小 */ 25 reg->fd = fd; 26 27 mmap_offset = memory.regions[i].mmap_offset; /* 映射段內偏移,通常為零 */ 28 mmap_size = reg->size + mmap_offset; /* 映射段大小 */ 29 30 ... 31 32 /* 將大頁文件重新映射到當前進程中 */ 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環信息傳遞
vhost內存映射完成后,便可進行IO環信息的傳遞,處理完成后使得vhost進程可以訪問IO環中信息。
這里注意一點,vhost在處理IO環相關消息時,首先會通過vhost_user_check_and_alloc_queue_pair來創建IO環相關對象。IO環相關的消息主要有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,這里我們重點分析一下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內存的映射,則先進行內存映射,可參考前文分析 */ 8 if (dev->has_new_mem_table) { 9 vhost_setup_mem_table(dev); 10 dev->has_new_mem_table = 0; 11 } 12 ... 13 14 /* 根據消息中的索引找到對應的vq對象 */ 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數組的qva地址轉換成vva地址,便於vhost線程后續訪問IO環中desc數組中內容 */ 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數組的qva地址轉換成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數組的qva地址轉換成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回調處理
vhost設備完成內存映射及IO環信息傳遞動作后,就進行不同vhost設備特有的初始化動作:
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環則先創建IO環對象 */ 10 ret = vhost_user_check_and_alloc_queue_pair(dev, &msg); 11 12 /* 根據不同的消息類型進行處理 */ 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 /* 通過notify_ops回調設備相關的初始化函數 */ 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函數指向start_device,這里仍是vhost設備通用的初始化邏輯:
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 /* 根據客戶端vid找到對應的vhost_dev設備 */ 9 vdev = spdk_vhost_dev_find_by_vid(vid); 10 11 /* 將客戶端對象(virtio_net)中記錄的IO環信息同步一份到vhost_dev中,后續IO處理時主要操作vhost_dev對象 */ 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 /* 同理,將客戶端對象中的內存映射表同步一份到vhost_dev中 */ 34 if (rte_vhost_get_mem_table(vid, &vdev->mem) != 0) { 35 36 } 37 38 /* 為vhost_dev對象分配一個運行核 */ 39 vdev->lcore = spdk_vhost_allocate_reactor(vdev->cpumask); 40 41 /* 記錄該vdev對象內存表中虛擬地址到物理地址的映射關系,后續操作物理DMA時可用 */ 42 spdk_vhost_dev_mem_register(vdev); 43 44 /* 向vhost_dev對象的運行核發送一個事件,使該核上的reactor線程可以執行backend的start_device函數 */ 45 rc = spdk_vhost_event_send(vdev, vdev->backend->start_device, 3, "start device"); 46 ... 47 48 return rc; 49 }
vhost_dev的運行核上的reactor線程會執行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設備中的每個隊列分配task數組,task與隊列中元素個數相同,一一對應 */ 11 rc = alloc_task_pool(bvdev); 12 ... 13 14 if (bvdev->bdev) { 15 /* 為vhost_blk對應申請IO Channel,此時已確定執行線程上下文 */ 16 bvdev->bdev_io_channel = spdk_bdev_get_io_channel(bvdev->bdev_desc); 17 ... 18 } 19 20 /* 在當前reactor線程中添加一個poller,用來處理IO環中的所有請求 */ 21 bvdev->requestq_poller = spdk_poller_register(bvdev->bdev ? vdev_worker : no_bdev_vdev_worker, bvdev, 0); 22 ... 23 return rc; 24 }
至此,SPDK中vhost進程的初始化流程已介紹完畢,過程非常漫長,大家可以在對數據面的處理流程有一定的熟悉之后再來閱讀分析這部分代碼,這樣可以理解得更深刻。