概述
本文基于SPDK v23.1版本的hello_world
示例來說明SPDK的nvme命令處理流程,代碼架構(gòu)如下:
example\nvme\hello_world.c
int main(int argc, char **argv)
{
spdk_env_opts_init(&opts);
rc = parse_args(argc, argv, &opts); // 參數(shù)解析
opts.name = "hello_world";
if (spdk_env_init(&opts) < 0) { // spdk環(huán)境初始化,最終調(diào)用的是dpdk的環(huán)境初始化
}
// 掃描設(shè)備,并將驅(qū)動和設(shè)備綁定,調(diào)用用戶提供的回調(diào)`probe_cb`和`attach_cb`
rc = spdk_nvme_probe(&g_trid, NULL, probe_cb, attach_cb, NULL);
hello_world(); // IO qpair創(chuàng)建循集、nvme的讀寫
cleanup(); // 資源釋放
return rc;
}
標(biāo)準(zhǔn)的NVMe處理涉及到NVMe子系統(tǒng)、HOST CPU蔗草、HOST 內(nèi)存三方面暇榴,下圖展示這三者之間的關(guān)系:
- NVMe子系統(tǒng)作為PCIe總線的Endpoint存在,可以直接與RC連接蕉世,也可以通過一個Switch連接到PCIe總線蔼紧;
- NVMe命令存放在HOST內(nèi)存的SQ中,命令處理完NVMe子系統(tǒng)會生成一個完成命令并放到HOST內(nèi)存的CQ中狠轻;
-
NVMe協(xié)議對于SQ/CQ的個數(shù)并沒有要求一一對應(yīng)奸例,但是SPDK中是一一對應(yīng)的都放在一個qpair中
初始化SPDK環(huán)境
接口:int spdk_env_init(const struct spdk_env_opts *opts)
這里需要的參數(shù)opts
可以通過接口spdk_env_opts_init(&opts)
來設(shè)置,以及通過當(dāng)前程序提供的參數(shù)來修改parse_args(argc, argv, &opts)
默認(rèn)的opts
參數(shù)配置如下:
[ DPDK EAL parameters: hello_world --no-shconf -c 0x1 --huge-unlink --log-level=lib.eal:6 --log-level=lib.cryptodev:5 --log-level=user1:6 --iova-mode=pa --base-virtaddr=0x200000000000 --match-allocations --file-prefix=spdk_pid76450 ]
最終調(diào)用DPDK的接口rte_eal_init
來完成SPDK環(huán)境的初始化向楼,為后面的操作做好準(zhǔn)備工作
設(shè)備查找
設(shè)備的注冊
SPDK對設(shè)備的管理類似Linux的設(shè)備驅(qū)動模型:包含bus
查吊、device
、driver
三個部分湖蜕。如下:
/**
* Structure describing the PCI bus
*/
struct rte_pci_bus {
struct rte_bus bus; /**< Inherit the generic class */
RTE_TAILQ_HEAD(, rte_pci_device) device_list; /**< List of PCI devices */
RTE_TAILQ_HEAD(, rte_pci_driver) driver_list; /**< List of PCI drivers */
};
另外逻卖,SPDK對于傳輸使用的協(xié)議或者總線虛擬化成一個transport
,主要包含PCIE
昭抒、TCP
评也、Fabric
、RDMA
等類型灭返。本文是基于example\nvme\hello_world
來說明盗迟,此示例使用的是PCIE
類型的transport
在main
函數(shù)執(zhí)行之前會進(jìn)行設(shè)備的注冊,SPDK中使用的是gnu
的attribute
特性來實(shí)現(xiàn)
-
bus
注冊RTE_REGISTER_BUS(pci, rte_pci_bus.bus); #define RTE_REGISTER_BUS(nm, bus) \ RTE_INIT_PRIO(businitfn_ ##nm, BUS) \ {\ (bus).name = RTE_STR(nm);\ rte_bus_register(&bus); \ // TAILQ_INSERT_TAIL(&rte_bus_list, bus, next);熙含,將bus放到rte_bus_list鏈表中罚缕,pci對應(yīng)的bus為rte_pci_bus.bus } #define RTE_INIT_PRIO(func, prio) \ static void __attribute__((constructor(RTE_PRIO(prio)), used)) func(void)
-
driver
注冊前半部分SPDK_PCI_DRIVER_REGISTER(nvme, nvme_pci_driver_id, SPDK_PCI_DRIVER_NEED_MAPPING | SPDK_PCI_DRIVER_WC_ACTIVATE); #define SPDK_PCI_DRIVER_REGISTER(name, id_table, flags) \ __attribute__((constructor)) static void _spdk_pci_driver_register_##name(void) \ { \ spdk_pci_driver_register(#name, id_table, flags); \ // 會將driver加到鏈表g_pci_drivers中 }
-
transport
注冊SPDK_NVME_TRANSPORT_REGISTER(pcie, &pcie_ops); .name = "PCIE", .type = SPDK_NVME_TRANSPORT_PCIE,
注冊使用的宏如下(黃色部分保證宏在main之前執(zhí)行,這種用法在spdk中很多怎静,用到時(shí)再記錄):
#define SPDK_NVME_TRANSPORT_REGISTER(name, transport_ops) \ static void __attribute__((constructor)) _spdk_nvme_transport_register_##name(void) \ { \ spdk_nvme_transport_register(transport_ops); \ } spdk_nvme_transport_register(transport_ops); //在g_spdk_transports中申請一個空間存放這種transport對應(yīng)的ops邮弹,并且把申請的空間放到鏈表g_spdk_nvme_transports中 ```c
在
main
函數(shù)執(zhí)行過程中的spdk初始化中會做設(shè)備的注冊以及driver的后半部注冊黔衡,將設(shè)備和驅(qū)動都注冊到bus中
兩個回調(diào)接口
probe_cb
: 找到NVMe controller之后進(jìn)行回調(diào),hello_world示例中只做了一條日志打印腌乡。
attach_cb
: NVMe controller連接到用戶空間驅(qū)動程序后調(diào)用盟劫,hello_world示例中做了兩件事,一是將初始化好的controller連接到g_controllers中导饲;二是將NS注冊到controller中捞高。
設(shè)備查找流程
總體入口為SPDK接口
int
spdk_nvme_probe(const struct spdk_nvme_transport_id *trid, void *cb_ctx,
spdk_nvme_probe_cb probe_cb, spdk_nvme_attach_cb attach_cb,
spdk_nvme_remove_cb remove_cb)
其中三個callback參數(shù)都可以用戶自定義氯材,在hello_world示例中使用了probe_cb
和attach_cb
整體調(diào)用關(guān)系如下圖所示
- 調(diào)用DPDK的接口
dpdk_bus_probe
將device和對應(yīng)的driver做綁定操作 - 調(diào)用用戶自定義
probe_cb
- 創(chuàng)建
controller
渣锦、admin qpair
、controller bar
空間等 - 初始化
controller
- 調(diào)用用戶自定義的
attach_cb
其中QPair
是SPDK的一種結(jié)構(gòu)如下圖所示(Admin和IO是一樣的結(jié)構(gòu)):
- SQ和CQ內(nèi)存區(qū)域既可基于DPDK所管理的大頁內(nèi)存來構(gòu)建氢哮,也可基于CMB進(jìn)行構(gòu)建袋毙。
- 為了便于請求對象的復(fù)用管理,每個QP會引入一個free_req對象池來緩存nvme_request對象實(shí)例冗尤,同時(shí)還會引入一個free_tr對象池(緩存nvme_tracker對象听盖,索引為cmdId),來跟蹤每個nvme_request的執(zhí)行情況(在其執(zhí)行結(jié)束時(shí)觸發(fā)相應(yīng)的回調(diào))裂七,在nvme_request的內(nèi)部主要維護(hù)了spdk_nvme_cmd數(shù)據(jù)結(jié)構(gòu)皆看,由于其和SQ采用不同的物理空間,因此在提交命令的時(shí)候需要做一次數(shù)據(jù)拷貝背零。
- 針對執(zhí)行失敗的請求腰吟,QP并不會將其丟棄,而是先加入queued_req隊(duì)列徙瓶,以便后續(xù)做retry處理毛雇,當(dāng)queued_req不為空的時(shí)候,后續(xù)新的請求都會提交到該隊(duì)列侦镇,以保證之前失敗的請求先做執(zhí)行灵疮。
- 最后,每個QP還會綁定兩個doorbell寄存器(每個doorbell占用4字節(jié)空間)壳繁,以便向設(shè)備控制通知cmd的就緒情況震捣,以及cpl的完成情況(基于MMIO方式更新)
IO處理
創(chuàng)建IO qpair
根據(jù)NVMe協(xié)議要求:先創(chuàng)建IO CQ
再創(chuàng)建IO SQ
,如下圖所示
-
SPDK使用polling機(jī)制來檢查CQ闹炉,而不是使用MSI/MSI-X等中斷形式伍派,在創(chuàng)建CQ時(shí)對DW11的IV/IEN字段都設(shè)置為0,只設(shè)置了PC字段為1(即使用連續(xù)地址)
代碼如下:
int nvme_pcie_ctrlr_cmd_create_io_cq(struct spdk_nvme_ctrlr *ctrlr, struct spdk_nvme_qpair *io_que, spdk_nvme_cmd_cb cb_fn, void *cb_arg) { ...... req = nvme_allocate_request_null(ctrlr->adminq, cb_fn, cb_arg); ...... cmd->cdw11_bits.create_io_cq.pc = 1; cmd->dptr.prp.prp1 = pqpair->cpl_bus_addr; return nvme_ctrlr_submit_admin_request(ctrlr, req); }
NVMe協(xié)議描述如下:
-
通過循環(huán)檢查CQE中的
Phase Tag(P)
字段來確定哪些是新來的CQE代碼如下:
int32_t nvme_pcie_qpair_process_completions(struct spdk_nvme_qpair *qpair, uint32_t max_completions) { ...... pqpair->stat->polls++; while (1) { cpl = &pqpair->cpl[pqpair->cq_head]; ...... next_cpl = &pqpair->cpl[next_cq_head]; next_is_valid = (next_cpl->status.p == next_phase); if (next_is_valid) { __builtin_prefetch(&pqpair->tr[next_cpl->cid]); } ...... tr = &pqpair->tr[cpl->cid]; /* Prefetch the req's STAILQ_ENTRY since we'll need to access it * as part of putting the req back on the qpair's free list. */ __builtin_prefetch(&tr->req->stailq); pqpair->sq_head = cpl->sqhd; if (tr->req) { nvme_pcie_qpair_complete_tracker(qpair, tr, cpl, true); } else { SPDK_ERRLOG("cpl does not map to outstanding cmd\n"); spdk_nvme_qpair_print_completion(qpair, cpl); assert(0); } if (++num_completions == max_completions) { break; } } ...... return num_completions; }
NVMe協(xié)議對CQE中
Phase Tag
的描述如下
- 在
Create IO CQ
命令提交時(shí)SPDK設(shè)置了一個回調(diào)接口剩胁,在收到此命令的CQE之后調(diào)用此回調(diào)函數(shù)诉植,而在此回調(diào)函數(shù)中做了Create IO SQ
的命令處理,命令處理過程與Create IO CQ
類似
構(gòu)造IO讀寫
SPDK中的命令處理流程為:一個命令執(zhí)行之前會添加此命令執(zhí)行完成之后的回調(diào)接口昵观,也即當(dāng)此命令執(zhí)行完并且收到對應(yīng)CQE時(shí)會調(diào)用回調(diào)接口晾腔。
所以Hello_world示例通過此特性來實(shí)現(xiàn)了先寫再讀的操作舌稀,在寫命令時(shí)設(shè)置回調(diào)write_complete
,而在write_complete
里面執(zhí)行nvme的讀操作灼擂,操作流程與前面的create_io_cq
和create_io_sq
類似