參考文獻:
《深入淺出DPDK》
linux 閱馬場 公眾號
..............................................................................................................
一. PCIe 介紹(參考 linux 閱馬場文章)
首 先我們來看一下在x86系統中,PCIe是什么樣的一個體系架構。下圖是一個PCIe的拓撲結構示例,PCIe協議支持256個Bus, 每條Bus最多支持32個Device,每個Device最多支持8個Function,所以由BDF(Bus,device,function)構成了 每個PCIe設備節點的身份證號。
PCIe 體系架構一般由root complex,switch,endpoint等類型的PCIe設備組成,在root complex和switch中通常會有一些embeded endpoint(這種設備對外不出PCIe接口)。這么多的設備,CPU啟動后要怎么去找到並認出它們呢? Host對PCIe設備掃描是采用了深度優先算法,其過程簡要來說是對每一個可能的分支路徑深入到不能再深入為止,而且每個節點只能訪問一次。我們一般稱 這個過程為PCIe設備枚舉。枚舉過程中host通過配置讀事物包來獲取下游設備的信息,通過配置寫事物包對下游設備進行設置。
第 一步,PCI Host主橋掃描Bus 0上的設備(在一個處理器系統中,一般將Root complex中與Host Bridge相連接的PCI總線命名為PCI Bus 0),系統首先會忽略Bus 0上的embedded EP等不會掛接PCI橋的設備,主橋發現Bridge 1后,將Bridge1 下面的PCI Bus定為 Bus 1,系統將初始化Bridge 1的配置空間,並將該橋的Primary Bus Number 和 Secondary Bus Number寄存器分別設置成0和1,以表明Bridge1 的上游總線是0,下游總線是1,由於還無法確定Bridge1下掛載設備的具體情況,系統先暫時將Subordinate Bus Number設為0xFF。
第 二步,系統開始掃描Bus 1,將會發現Bridge 3,並發現這是一個switch設備。系統將Bridge 3下面的PCI Bus定為Bus 2,並將該橋的Primary Bus Number 和 Secondary Bus Number寄存器分別設置成1和2,和上一步一樣暫時把Bridge 3 的Subordinate Bus Number設為0xFF。
第 三步,系統繼續掃描Bus 2,將會發現Bridge 4。繼續掃描,系統會發現Bridge下面掛載的NVMe SSD設備,系統將Bridge 4下面的PCI Bus定為Bus 3,並將該橋的Primary Bus Number 和 Secondary Bus Number寄存器分別設置成2和3,因為Bus3下面掛的是端點設備(葉子節點),下面不會再有下游總線了,因此Bridge 4的Subordinate Bus Number的值可以確定為3。
第 四步,完成Bus 3的掃描后,系統返回到Bus 2繼續掃描,會發現Bridge 5。繼續掃描,系統會發現下面掛載的NIC設備,系統將Bridge 5下面的PCI Bus設置為Bus 4,並將該橋的Primary Bus Number 和 Secondary Bus Number寄存器分別設置成2和4,因為NIC同樣是端點設備,Bridge 5的Subordinate Bus Number的值可以確定為4。
第 五步,除了Bridge 4和Bridge 5以外,Bus2下面沒有其他設備了,因此返回到Bridge 3,Bus 4是找到的掛載在這個Bridge下的最后一個bus號,因此將Bridge 3的Subordinate Bus Number設置為4。Bridge 3的下游設備都已經掃描完畢,繼續向上返回到Bridge 1,同樣將Bridge 1的Subordinate Bus Number設置為4。
第 六步,系統返回到Bus0繼續掃描,會發現Bridge 2,系統將Bridge 2下面的PCI Bus定為Bus 5。並將Bridge 2的Primary Bus Number 和 Secondary Bus Number寄存器分別設置成0和5, Graphics card也是端點設備,因此Bridge 2 的Subordinate Bus Number的值可以確定為5
至此,掛在PCIe總線上的所有設備都被掃描到,枚舉過程結束,Host通過這一過程獲得了一個完整的PCIe設備拓撲結構。
系統上電以后,host會自動完成上述的設備枚舉過程。除一些專有系統外,普通系統只會在開機階段進行進行設備的掃描,啟動成功后(枚舉過程結束),即使插入一個PCIe設備,系統也不會再去識別它
在 linux操作系統中,我們可以通過lspci –v -t命令來查詢系統上電階段掃描到的PCIe設備,執行結果會以一個樹的形式列出系統中所有的pcie設備。如下圖所示,其中黃色方框中的PCIe設備是 北京憶芯科技公司(Bejing Starblaze Technology Co., LTD.)推出的STAR1000系列NVMe SSD主控芯片,圖中顯示的9d32是Starblaze在PCI-SIG組織的注冊碼,1000是設備系列號。
STAR1000設備的BDF也可以從上圖中找出,其中bus是0x3C,device是0x00,function是0x0,BDF表示為3C:00.0,與之對應的上游端口是00:1d.0。
我 們可以通過“lspci –xxx –s 3C:00.0”命令來列出該設備的PCIe詳細信息(技術發燒友或數字控請關注該部分)。這些內容存儲在PCIe配置空間,它們描述的是PCIe本身的 特性。如下圖所示(低位地址0x00在最左邊),可以看到這是一個非易失性存儲控制器,0x00起始地址是PCIe的Vendor ID和Device ID。Class code 0x010802表示這是一個NVMe存儲設備。0x40是第一組capability的指針,如果你需要查看PCIe的特性,就需要從這個位置開始去查 詢,在每組特征的頭字段都會給出下一組特性的起始地址。從0x40地址開始依次是power management,MSI中斷,鏈路控制與狀態,MSI-X中斷等特性組。這兒特別列出了鏈路特征中的一個0x43字段,表示STAR1000設備是 一個x4lane的鏈接,支持PCIe Gen3速率(8Gbps)
當然也可以使用lspci –vvv –s 3C:00.0命令來查看設備特性
二. 從pcie角度看包處理
pcie規范遵循開放系統互聯網參考模型,自上而下分為事務傳輸層,數據鏈路層,物理層。
三. mbuf
Mbuf庫提供了申請和釋放mbufs的功能,DPDK應用程序使用這些buffer存儲消息緩沖。 消息緩沖存儲在mempool中
數據結構rte_mbuf可以承載網絡數據包buffer或者通用控制消息buffer(由CTRL_MBUF_FLAG指示)。 也可以擴展到其他類型。 rte_mbuf頭部結構盡可能小,目前只使用兩個緩存行,最常用的字段位於第一個緩存行中
為了存儲數據包數據(報價協議頭部), 考慮了兩種方法:
- 在單個存儲buffer中嵌入metadata,后面跟着數據包數據固定大小區域
- 為metadata和報文數據分別使用獨立的存儲buffer。
第一種方法的優點是他只需要一個操作來分配/釋放數據包的整個存儲表示。 但是,第二種方法更加靈活,並允許將元數據的分配與報文數據緩沖區的分配完全分離。
DPDK選擇了第一種方法。 Metadata包含諸如消息類型,長度,到數據開頭的偏移量等控制信息,以及允許緩沖鏈接的附加mbuf結構指針。
用於承載網絡數據包buffer的消息緩沖可以處理需要多個緩沖區來保存完整數據包的情況。 許多通過下一個字段鏈接在一起的mbuf組成的jumbo幀,就是這種情況。
對於新分配的mbuf,數據開始的區域是buffer之后 RTE_PKTMBUF_HEADROOM 字節的位置,這是緩存對齊的。 Message buffers可以在系統中的不同實體中攜帶控制信息,報文,事件等。 Message buffers也可以使用起buffer指針來指向其他消息緩沖的數據字段或其他數據結構。

Fig. 6.1 An mbuf with One Segment

Buffer Manager實現了一組相當標准的buffer訪問操作來操縱網絡數據包。
Buffer Manager 使用 Mempool Library 來申請buffer。 因此確保了數據包頭部均衡分布到信道上並進行L3處理。 mbuf中包含一個字段,用於表示它從哪個池中申請出來。 當調用 rte_ctrlmbuf_free(m) 或 rte_pktmbuf_free(m),mbuf被釋放到原來的池中。
Packet 及 control mbuf構造函數由API提供。 接口rte_pktmbuf_init() 及 rte_ctrlmbuf_init() 初始化mbuf結構中的某些字段,這些字段一旦創建將不會被用戶修改(如mbuf類型、源池、緩沖區起始地址等)。 此函數在池創建時作為rte_mempool_create()函數的回掉函數給出。
分配一個新mbuf需要用戶指定從哪個池中申請。 對於任意新分配的mbuf,它包含一個段,長度為0。 緩沖區到數據的偏移量被初始化,以便使得buffer具有一些字節(RTE_PKTMBUF_HEADROOM)的headroom。
釋放mbuf意味着將其返回到原始的mempool。 當mbuf的內容存儲在一個池中(作為一個空閑的mbuf)時,mbuf的內容不會被修改。 由構造函數初始化的字段不需要在mbuf分配時重新初始化。
當釋放包含多個段的數據包mbuf時,他們都被釋放,並返回到原始mempool。
這個庫提供了一些操作數據包mbuf中的數據的功能。 例如:
- 獲取數據長度
- 獲取指向數據開始位置的指針
- 數據前插入數據
- 數據之后添加數據
- 刪除緩沖區開頭的數據(rte_pktmbuf_adj())
- 刪除緩沖區末尾的數據(rte_pktmbuf_trim()) 詳細信息請參閱 DPDK API Reference
部分信息由網絡驅動程序檢索並存儲在mbuf中使得處理更簡單。 例如,VLAN、RSS哈希結果(參見 Poll Mode Driver)及校驗和由硬件計算的標志等。
mbuf中還包含數據源端口和報文鏈中mbuf數目。 對於鏈接的mbuf,只有鏈的第一個mbuf存儲這個元信息。
例如,對於IEEE1588數據包,RX側就是這種情況,時間戳機制,VLAN標記和IP校驗和計算。 在TX端,應用程序還可以將一些處理委托給硬件。 例如,PKT_TX_IP_CKSUM標志允許卸載IPv4校驗和的計算。
以下示例說明如何在vxlan封裝的tcp數據包上配置不同的TX卸載:out_eth/out_ip/out_udp/vxlan/in_eth/in_ip/in_tcp/payload
-
計算out_ip的校驗和:
mb->l2_len = len(out_eth) mb->l3_len = len(out_ip) mb->ol_flags |= PKT_TX_IPV4 | PKT_TX_IP_CSUM set out_ip checksum to 0 in the packet
配置DEV_TX_OFFLOAD_IPV4_CKSUM支持在硬件計算。
-
計算out_ip 和 out_udp的校驗和:
mb->l2_len = len(out_eth) mb->l3_len = len(out_ip) mb->ol_flags |= PKT_TX_IPV4 | PKT_TX_IP_CSUM | PKT_TX_UDP_CKSUM set out_ip checksum to 0 in the packet set out_udp checksum to pseudo header using rte_ipv4_phdr_cksum()
配置DEV_TX_OFFLOAD_IPV4_CKSUM 和 DEV_TX_OFFLOAD_UDP_CKSUM支持在硬件上計算。
-
計算in_ip的校驗和:
mb->l2_len = len(out_eth + out_ip + out_udp + vxlan + in_eth) mb->l3_len = len(in_ip) mb->ol_flags |= PKT_TX_IPV4 | PKT_TX_IP_CSUM set in_ip checksum to 0 in the packet
這以情況1類似,但是l2_len不同。 配置DEV_TX_OFFLOAD_IPV4_CKSUM支持硬件計算。 注意,只有外部L4校驗和為0時才可以工作。
-
計算in_ip 和 in_tcp的校驗和:
mb->l2_len = len(out_eth + out_ip + out_udp + vxlan + in_eth) mb->l3_len = len(in_ip) mb->ol_flags |= PKT_TX_IPV4 | PKT_TX_IP_CSUM | PKT_TX_TCP_CKSUM 在報文中設置in_ip校驗和為0 使用rte_ipv4_phdr_cksum()將in_tcp校驗和設置為偽頭
這與情況2類似,但是l2_len不同。 配置DEV_TX_OFFLOAD_IPV4_CKSUM 和 DEV_TX_OFFLOAD_TCP_CKSUM支持硬件實現。 注意,只有外部L4校驗和為0才能工作。
-
segment inner TCP:
mb->l2_len = len(out_eth + out_ip + out_udp + vxlan + in_eth) mb->l3_len = len(in_ip) mb->l4_len = len(in_tcp) mb->ol_flags |= PKT_TX_IPV4 | PKT_TX_IP_CKSUM | PKT_TX_TCP_CKSUM | PKT_TX_TCP_SEG; 在報文中設置in_ip校驗和為0 將in_tcp校驗和設置為偽頭部,而不使用IP載荷長度
配置DEV_TX_OFFLOAD_TCP_TSO支持硬件實現。 注意,只有L4校驗和為0時才能工作。
-
計算out_ip, in_ip, in_tcp的校驗和:
mb->outer_l2_len = len(out_eth) mb->outer_l3_len = len(out_ip) mb->l2_len = len(out_udp + vxlan + in_eth) mb->l3_len = len(in_ip) mb->ol_flags |= PKT_TX_OUTER_IPV4 | PKT_TX_OUTER_IP_CKSUM | PKT_TX_IP_CKSUM | PKT_TX_TCP_CKSUM; 設置 out_ip 校驗和為0 設置 in_ip 校驗和為0 使用rte_ipv4_phdr_cksum()設置in_tcp校驗和為偽頭部
配置DEV_TX_OFFLOAD_IPV4_CKSUM, DEV_TX_OFFLOAD_UDP_CKSUM 和 DEV_TX_OFFLOAD_OUTER_IPV4_CKSUM支持硬件實現。
Flage標記的意義在mbuf API文檔(rte_mbuf.h)中有詳細描述。 更多詳細信息還可以參閱testpmd 源碼(特別是csumonly.c)。
直接緩沖區是指緩沖區完全獨立。 間接緩沖區的行為類似於直接緩沖區,但緩沖區的指針和數據便宜量指的是另一個直接緩沖區的數據。 這在數據包需要復制或分段的情況下是很有用的,因為間接緩沖區提供跨越多個緩沖區重用相同數據包數據的手段。
當使用接口 rte_pktmbuf_attach()
函數將緩沖區附加到直接緩沖區時,該緩沖區變成間接緩沖區。 每個緩沖區有一個引用計數器字段,每當直接緩沖區附加一個間接緩沖區時,直接緩沖區上的應用計數器遞增。 類似的,每當間接緩沖區被分裂時,直接緩沖區上的引用計數器遞減。 如果生成的引用計數器為0,則直接緩沖區將被釋放,因為它不再使用。
處理間接緩沖區時需要注意幾件事情。 首先,間接緩沖區從不附加到另一個間接緩沖區。 嘗試將緩沖區A附加到間接緩沖區B(且B附加到C上了),將使得rte_pktmbuf_attach() 自動將A附加到C上。 其次,為了使緩沖區變成間接緩沖區,其引用計數必須等於1,也就是說它不能被另一個間接緩沖區引用。 最后,不可能將間接緩沖區重新鏈接到直接緩沖區(除非它已經被分離了)。
雖然可以使用推薦的rte_pktmbuf_attach()和rte_pktmbuf_detach()函數直接調用附加/分離操作, 但建議使用更高級的rte_pktmbuf_clone()函數,該函數負責間接緩沖區的正確初始化,並可以克隆具有多個段的緩沖區。
由於間接緩沖區不應該實際保存任何數據,間接緩沖區的內存池應配置為指示減少的內存消耗。 可以在幾個示例應用程序中找到用於間接緩沖區的內存池(以及間接緩沖區的用例示例)的初始化示例,例如IPv4組播示例應用程序。