spdk探祕-----塊設備開發指導

這裏的塊設備是一種存儲設備,它支持在固定大小的塊中讀寫數據。這些塊通常是512或4096字節。這些設備可能是軟件中的邏輯結構,或者對應於像NVMe ssd這樣的物理設備。

通用庫的公共頭文件是bdev.h,它是與任何類型的塊設備交互所需的全部API。

除了爲所有塊設備提供一個通用的抽象之外,bdev層還提供了許多有用的特性:

1、I/O請求自動排隊,以響應隊列滿或內存不足的情況

2、熱刪除支持,即使有I/O流量時。

3、I/O統計信息,如帶寬和延遲

4、設備重啓和I/O超時跟蹤

struct spdk_bdev(bdev)表示一個通用塊設備。struct spdk_bdev_desc,稱爲描述符,表示給定塊設備的句柄。描述符用於建立和跟蹤使用底層塊設備的權限,非常文件描述符。對塊設備的請求是異步的,並由spdk_bdev_io對象表示。請求必須在關聯的I/O通道上提交。將從消息傳遞和併發兩個方面闡述了I/O通道的設計和動機。

Bdevs可以分層,這樣一些Bdevs通過將請求路由到其他Bdevs來服務I/O。這可以用於實現緩存、RAID、邏輯卷管理等。將I/O路由到其他bdev的bdev通常稱爲虛擬bdev,或簡稱vbdev。

初始化庫

bdev層依賴於頭文件include/spdk/thread.h提供的通用消息傳遞基礎設施。最重要的是,只能通過spdk_thread_create()分配的線程才能調用bdev庫。

在分配的線程中,可以通過調用spdk_bdev_initialize()來初始化bdev庫,這是一個異步操作。在調用完成回調之前,不能調用其他bdev庫函數。相反的要卸載bdev庫,可以調用spdk_bdev_finish()。

發現塊設備

所有塊設備都有一個簡單的字符串名稱。在任何時候都可以通過調用spdk_bdev_get_by_name()獲得指向設備對象的指針,也可以使用spdk_bdev_first()和spdk_bdev_next()迭代整個bdevs集合。

一些塊設備也可能被賦予別名,這也是字符串名稱。別名的行爲類似符號鏈接——它們可以與真名互換使用,以查找塊設備。

塊設備準備

爲了向塊設備發送I/O請求,必須首先通過調用spdk_bdev_open_ext()來打開它。這將返回一個描述符。多個用戶可能同時打開一個bdev,用戶之間讀寫的協調必須由bdev層之上的機制來處理。如果虛擬bdev模塊claimed了bdevA,那麼使用寫權限打開bdevA可能會失敗。因爲虛擬bdev模塊實現像RAID或邏輯卷管理這樣的邏輯,並將它們的I/O轉發給較低級別的bdevA,因此它們將這些較低級別的bdevA標記爲claimed的,以防止外部用戶發出寫操作。

當塊設備打開時,必須提供回調和上下文,當bdev觸發異步事件(如bdev刪除)時,將使用適當的spdk_bdev_event_type enum作爲參數調用它們。例如,當NVMe SSD熱拔時,將對物理NVMe SSD支持的bdev的每個打開的描述符調用回調。在這種情況下,可以將回調看作是關閉描述符的請求,以便釋放其他內存。當描述符存打開時不能卸載bdev,因此需要提供回調。

當用戶使用完描述符時,他們可以通過調用spdk_bdev_close()來釋放它。

描述符可以同時傳遞給多個線程並在多個線程使用。但是,對於每個線程,必須通過調用spdk_bdev_get_io_channel()來獲得一個單獨的I/O通道。這將爲每個線程分配必要的資源,以便在不獲取鎖的情況下向bdev提交I/O請求。要釋放一個通道,請調用spdk_put_io_channel()。在所有相關聯的通道被銷燬之前,描述符不能被關閉。

發送I / O

一旦獲得了描述符和通道,就可以通過調用各種I/O提交函數(如spdk_bdev_read())來發送I/O。這些調用都接受一個回調函數作爲參數,在後面會使用spdk_bdev_io對象的句柄來調用該回調函數。爲了響應這個完成,用戶必須調用spdk_bdev_free_io()來釋放資源。在這個回調中,用戶還可以使用函數spdk_bdev_io_get_nvme_status()和spdk_bdev_io_get_scsi_status()來獲取他們選擇的格式的錯誤信息。

I/O提交是通過調用spdk_bdev_read()或spdk_bdev_write()等函數來執行的。存儲需要寫入device的數據的內存必須通過spdk_dma_malloc()或它的變體來分配。在用戶空間的直接內存訪問(DMA)中會介紹爲什麼必須使用DMA來分配。在一般情況下,內存中的數據將被直接傳輸到塊設備。這個過程零拷貝。

所有的I/O提交函數都是異步的和非阻塞的。它們不會因爲任何原因阻塞或停止線程。但是,I/O提交函數可能有兩種失敗。首先,它們可能會立即失敗並返回一個錯誤代碼。在這種情況下,提供的回調將不會被調用。其次,它們可能異步失敗。在這種情況下,相關的spdk_bdev_io將被傳遞給回調函數,它將報告錯誤信息。

有些I/O請求類型是可選的,可能不被給定的bdev支持。要查詢bdev支持的I/O請求類型,請調用spdk_bdev_io_type_supported()。

重置塊設備

爲了處理意外的故障條件,bdev庫提供了一種通過調用spdk_bdev_reset()來重置的機制。這將向bdev存在I/O通道的每個其他線程傳遞消息,暫停它,然後將一個復位請求轉發給底層bdev模塊,並等待完成。完成後,I/O通道將恢復,重置將完成。bdev模塊中的特定行爲是特定於模塊的。例如,NVMe設備將刪除所有隊列對,執行一個NVMe重置,然後重新創建隊列對並繼續。最重要的是,無論設備類型是什麼,對塊設備的所有I/O都將在重置完成之前完成。

編寫自定義塊設備模塊

這個指南是爲編寫自己的塊設備模塊以與SPDK的bdev層集成的開發人員準備的。

在SPDK中,塊設備模塊相當於傳統操作系統中的設備驅動程序。模塊提供了一組函數指針,用於服務塊設備I/O請求。SPDK提供了許多塊設備模塊,包括NVMe、RAM-disk和Ceph RBD。但是,有些用戶希望編寫自己的存儲,以便與自定義硬件或現有的存儲軟件堆棧交互。本指南旨在準確地演示如何編寫模塊。

創建新模塊

塊設備模塊現在位於lib/bdev的子目錄中。目前還不能將bdev模塊的代碼放在其他地方。要創建模塊,添加一個帶有單個C文件和Makefile的新目錄。一個很好的途徑是複製現有的“null”bdev模塊。

bdev模塊將與之交互的主要接口在include/spdk/bdev_module.h中。在這個頭文件中定義了一個宏SPDK_BDEV_MODULE_REGISTER來註冊一個新的bdev模塊。這個宏採用一個指針spdk_bdev_module結構作爲參數,該結構用於註冊新的bdev模塊。spdk_bdev_module結構描述了模塊屬性初始化(module_init)和卸載(module_fini)函數。可以查看struct spdk_bdev_module的文檔瞭解更多細節。

創建Bdevs

通過調用spdk_bdev_register()在模塊中創建新的bdev。新模塊必須分配struct spdk_bdev,並初始化,並將它傳遞給寄存器調用。要初始化的最重要的字段是fn_table,它指向這個數據結構:

/*
* Function table for a block device backend.
*
* The backend block device function table provides a set of APIs to allow
* communication with a backend. The main commands are read/write API
* calls for I/O via submit_request.
*/
struct spdk_bdev_fn_table {
/* Destroy the backend block device object */
int (*destruct)(void *ctx);
/* Process the IO. */
void (*submit_request)(struct spdk_io_channel *ch, struct spdk_bdev_io *);
/* Check if the block device supports a specific I/O type. */
bool (*io_type_supported)(void *ctx, enum spdk_bdev_io_type);
/* Get an I/O channel for the specific bdev for the calling thread. */
struct spdk_io_channel *(*get_io_channel)(void *ctx);
/*
* Output driver-specific configuration to a JSON stream. Optional - may be NULL.
*
* The JSON write context will be initialized with an open object, so the bdev
* driver should write a name (based on the driver name) followed by a JSON value
* (most likely another nested object).
*/
int (*dump_config_json)(void *ctx, struct spdk_json_write_ctx *w);
/* Get spin-time per I/O channel in microseconds.
* Optional - may be NULL.
*/
uint64_t (*get_spin_time)(struct spdk_io_channel *ch);
};

bdev模塊必須實現這些函數回調。

當系統不再需要該設備時,調用析構函數來拆除該設備。destruct所做的是由模塊決定:可能只是釋放內存,也可能是關閉一塊硬件。

io_type_supported函數返回是否支持特定的I/O類型。可用的I/O類型有:

enum spdk_bdev_io_type {
SPDK_BDEV_IO_TYPE_INVALID = 0,
SPDK_BDEV_IO_TYPE_READ,
SPDK_BDEV_IO_TYPE_WRITE,
SPDK_BDEV_IO_TYPE_UNMAP,
SPDK_BDEV_IO_TYPE_FLUSH,
SPDK_BDEV_IO_TYPE_RESET,
SPDK_BDEV_IO_TYPE_NVME_ADMIN,
SPDK_BDEV_IO_TYPE_NVME_IO,
SPDK_BDEV_IO_TYPE_NVME_IO_MD,
SPDK_BDEV_IO_TYPE_WRITE_ZEROES,
};

對於最簡單的bdev模塊,只需要SPDK_BDEV_IO_TYPE_READ和SPDK_BDEV_IO_TYPE_WRITE。SPDK_BDEV_IO_TYPE_UNMAP通常被稱爲“trim”或“deallocate”,它是一個將一組塊標記爲不再包含有效數據的請求。SPDK_BDEV_IO_TYPE_FLUSH請求使之前完成的所有寫操作具有持久性,許多設備不需要flush。SPDK_BDEV_IO_TYPE_WRITE_ZEROES就像常規的寫操作,但是不提供數據緩衝區(它只包含所有的0)。如果不支持它,一般的bdev代碼可以通過發送常規的寫請求來模擬它。

SPDK_BDEV_IO_TYPE_RESET是一箇中止所有I/O並將底層設備返回到初始狀態的請求。在以某種方式完成所有I/O之前,不要完成重置請求。

SPDK_BDEV_IO_TYPE_NVME_ADMIN、SPDK_BDEV_IO_TYPE_NVME_IO_MD和SPDK_BDEV_IO_TYPE_NVME_IO_MD都是通過SPDK bdev層傳遞原始NVMe命令的機制。它們是嚴格可選的,可能只有在後備存儲設備能夠處理NVMe命令時纔有意義。

get_io_channel函數應該返回一個I/O通道。通用bdev層將每個線程調用一次get_io_channel,緩存結果,並將結果傳遞給submit_request。它將爲調用submit_request的線程使用相應的通道。

submit_request函數被調用來實際地向塊設備提交I/O請求。I/O請求完成後,模塊必須調用spdk_bdev_io_complete()。I/O不必在submit_request的調用上下文中完成。

創建虛擬Bdevs

如果塊設備A通過將I/O路由到其他塊設備B來處理I/O請求,則認爲A是虛擬的。典型的例子是實現RAID的bdev模塊就是個虛擬模塊。虛擬bdev的創建方式與常規bdev相同,但需要額外的一個步驟。虛擬模塊可以使用spdk_bdev_get_by_name()查找它希望將I/O路由到的底層bdevs,其中字符串名稱由用戶在配置文件中或通過RPC提供。然後通過打開底層bdev獲取描述符,併爲bdev創建I/O通道(可能是對get_io_channel回調的響應),模塊可以正常運行。最後一步是讓虛擬模塊用其底層塊設備描述符來調用spdk_bdev_module_claim_bdev(),表明該虛擬模塊正在使用底層的bdev。這將防止其他用戶在該底層模塊上打開具有寫權限的描述符。這有效地將描述符“提升”爲寫獨佔,並且這是一個僅對bdev模塊可用的操作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章