1. 前言

RDMA指的是遠程直接內存訪問,這是一種通過網路在兩個應用程序之間搬運緩衝區里的數據的方法。RDMA與傳統的網路介面不同,因為它繞過了操作系統。這允許實現了RDMA的程序具有如下特點:

  • 絕對的最低時延
  • 最高的吞吐量
  • 最小的CPU足跡 (也就是說,需要CPU參與的地方被最小化)

2. RDMA Verbs操作

使用RDMA, 我們需要有一張實現了RDMA引擎的網卡。我們把這種卡稱之為HCA(主機通道適配器)。 適配器創建一個貫穿PCIe匯流排的從RDMA引擎到應用程序內存的通道。一個好的HCA將在導線上執行的RDMA協議所需要的全部邏輯都在硬體上予以實現。這包括分組,重組以及流量控制和可靠性保證。因此,從應用程序的角度看,只負責處理所有緩衝區即可。

在RDMA中我們使用內核態驅動建立一個數據通道。我們稱之為命令通道(Command Channel)。使用命令通道,我們能夠建立一個數據通道(Data Channel),該通道允許我們在搬運數據的時候完全繞過內核。一旦建立了這種數據通道,我們就能直接讀寫數據緩衝區。

建立數據通道的API是一種稱之為"verbs"的API。"verbs" API是由一個叫做OFED的Linux開源項目維護的。在站點openfabrics.org上,為Windows WinOF提供了一個等價的項目。"verbs" API跟你用過的socket編程API是不一樣的。但是,一旦你掌握了一些概念後,就會變得非常容易,而且在設計你的程序的時候更簡單。

2. Queue Pairs

RDMA操作開始於「搞」內存。當你在對內存進行操作的時候,就是告訴內核這段內存名花有主了,主人就是你的應用程序。於是,你告訴HCA,就在這段內存上定址,趕緊準備開闢一條從HCA卡到這段內存的通道。我們將這一動作稱之為註冊一個內存區域(MR)。一旦MR註冊完畢,我們就可以使用這段內存來做任何RDMA操作。在下面的圖中,我們可以看到註冊的內存區域(MR)和被通信隊列所使用的位於內存區域之內的緩衝區(buffer)。

RDMA Memory Registration

struct ibv_mr {
struct ibv_context *context;
struct ibv_pd *pd;
void *addr;
size_t length;
uint32_t handle;
uint32_t lkey;
uint32_t rkey;
};

RDMA硬體不斷地從工作隊列(WQ)中去取工作請求(WR)來執行,執行完了就給完成隊列(CQ)中放置工作完成通知(WC)。這個WC意思就是Work Completion。表示這個WR RDMA請求已經被處理完成,可以從這個Completion Queue從取出來,表示這個RDMA請求已經被處理完畢。

RDMA通信基於三條隊列(SQ, RQ和CQ)組成的集合。 其中, 發送隊列(SQ)和接收隊列(RQ)負責調度工作,他們總是成對被創建,稱之為隊列對(QP)。當放置在工作隊列上的指令被完成的時候,完成隊列(CQ)用來發送通知。

當用戶把指令放置到工作隊列的時候,就意味著告訴HCA那些緩衝區需要被發送或者用來接受數據。這些指令是一些小的結構體,稱之為工作請求(WR)或者工作隊列元素(WQE)。 WQE的發音為"WOOKIE",就像星球大戰里的猛獸。一個WQE主要包含一個指向某個緩衝區的指針。一個放置在發送隊列(SQ)里的WQE中包含一個指向待發送的消息的指針。一個放置在接受隊列里的WQE里的指針指向一段緩衝區,該緩衝區用來存放待接受的消息。

下面我們來看一下RDMA中的Work Request(SendWR和ReceWR)

RDMA Send Work Request請求

struct ibv_send_wr {
uint64_t wr_id;
struct ibv_send_wr *next;
struct ibv_sge *sg_list;
int num_sge;
enum ibv_wr_opcode opcode;
int send_flags;
uint32_t imm_data; /* in network byte order */
union {
struct {
uint64_t remote_addr;
uint32_t rkey;
} rdma;
struct {
uint64_t remote_addr;
uint64_t compare_add;
uint64_t swap;
uint32_t rkey;
} atomic;
struct {
struct ibv_ah *ah;
uint32_t remote_qpn;
uint32_t remote_qkey;
} ud;
} wr;
};

RDMA Receive Work Request請求

struct ibv_recv_wr {
uint64_t wr_id;
struct ibv_recv_wr *next;
struct ibv_sge *sg_list;
int num_sge;
};

RDMA是一種非同步傳輸機制。因此我們可以一次性在工作隊列里放置好多個發送或接收WQE。HCA將儘可能快地按順序處理這些WQE。當一個WQE被處理了,那麼數據就被搬運了。 一旦傳輸完成,HCA就創建一個完成隊列元素(CQE)並放置到完成隊列(CQ)中去。 相應地,CQE的發音為"COOKIE"。

RDMA Complete Queue Element

c++ struct ibv_wc { uint64_t wr_id; enum ibv_wc_status status; enum ibv_wc_opcode opcode; uint32_t vendor_err; uint32_t byte_len; uint32_t imm_data; /* in network byte order */ uint32_t qp_num; uint32_t src_qp; int wc_flags; uint16_t pkey_index; uint16_t slid; uint8_t sl; uint8_t dlid_path_bits; };

3. RDMA Send/Receive

讓我們看個簡單的例子。在這個例子中,我們將把一個緩衝區里的數據從系統A的內存中搬到系統B的內存中去。這就是我們所說的消息傳遞語義學。接下來我們要講的一種操作為SEND,是RDMA中最基礎的操作類型。

3.1 第一步

第1步:系統A和B都創建了他們各自的QP的完成隊列(CQ), 並為即將進行的RDMA傳輸註冊了相應的內存區域(MR)。 系統A識別了一段緩衝區,該緩衝區的數據將被搬運到系統B上。系統B分配了一段空的緩衝區,用來存放來自系統A發送的數據。

3.2 第二步

第二步:系統B創建一個WQE並放置到它的接收隊列(RQ)中。這個WQE包含了一個指針,該指針指向的內存緩衝區用來存放接收到的數據。系統A也創建一個WQE並放置到它的發送隊列(SQ)中去,該WQE中的指針執行一段內存緩衝區,該緩衝區的數據將要被傳送。

3.3 第三步

第三步:系統A上的HCA總是在硬體上幹活,看看發送隊列里有沒有WQE。HCA將消費掉來自系統A的WQE, 然後將內存區域里的數據變成數據流發送給系統B。當數據流開始到達系統B的時候,系統B上的HCA就消費來自系統B的WQE,然後將數據放到該放的緩衝區上去。在高速通道上傳輸的數據流完全繞過了操作系統內核。

3.4 第四步

第四步:當數據搬運完成的時候,HCA會創建一個CQE。 這個CQE被放置到完成隊列(CQ)中,表明數據傳輸已經完成。HCA每消費掉一個WQE, 都會生成一個CQE。因此,在系統A的完成隊列中放置一個CQE,意味著對應的WQE的發送操作已經完成。同理,在系統B的完成隊列中也會放置一個CQE,表明對應的WQE的接收操作已經完成。如果發生錯誤,HCA依然會創建一個CQE。在CQE中,包含了一個用來記錄傳輸狀態的欄位。

我們剛剛舉例說明的是一個RDMA Send操作。在IB或RoCE中,傳送一個小緩衝區里的數據耗費的總時間大約在1.3μs。通過同時創建很多WQE, 就能在1秒內傳輸存放在數百萬個緩衝區里的數據。

4. 總結

在這博客中,我們學習了如何使用RDMA verbs API。同時也介紹了隊列的概念,而隊列概念是RDMA編程的基礎。最後,我們演示了RDMA send操作,展現了緩衝區的數據是如何在從一個系統搬運到另一個系統上去的。


推薦閱讀:
相关文章