Linux網路報文捕獲/抓包技術對比:napi、libpcap、afpacket、PF_RING、PACKET_MMAP、DPDK、XDP(eXpress Data Path)

confirmwz發表於2020-12-29

Table of Contents

1.傳統linux網路協議棧流程和效能分析

協議棧的主要問題

針對單個資料包級別的資源分配和釋放

流量的序列訪問

從驅動到使用者態的資料拷貝

核心到使用者空間的上下文切換

跨記憶體訪問

2. 提高捕獲效率的技術

預分配和重用記憶體資源

資料包採用並行直接通道傳遞.

記憶體對映.

資料包的批處理.

親和性與預取.

3. 典型收包引擎

3.1 libpcap

3.2 libpcap-mmap

3.3 PF_RING

3.4 PACKET_MMAP

PACKET_MMAP的實現原理

PACKET_MMAP原始碼分析

3.5 DPDK

UIO+mmap 實現零拷貝(zero copy)

UIO+PMD 減少中斷和CPU上下文切換

HugePages 減少TLB miss

其它優化

3.6 XDP(eXpress Data Path)

PS:使用XDP(eXpress Data Path)防禦DDoS攻擊

新的分層方法

繞過更低層的門

XDP

關於DDoS防禦

怎麼做?

4. 無鎖佇列技術

CAS原子指令操作

記憶體屏障

5. 基於pfring/dpdk的應用

參考

相關閱讀


 

1.傳統linux網路協議棧流程和效能分析

Linux網路協議棧是處理網路資料包的典型系統,它包含了從物理層直到應用層的全過程。

  1. 資料包到達網路卡裝置。
  2. 網路卡裝置依據配置進行DMA操作。(第1次拷貝:網路卡暫存器->核心為網路卡分配的緩衝區ring buffer)
  3. 網路卡傳送中斷,喚醒處理器。
  4. 驅動軟體從ring buffer中讀取,填充核心skbuff結構(第2次拷貝:核心網路卡緩衝區ring buffer->核心專用資料結構skbuff)
  5. 資料包文達到核心協議棧,進行高層處理。
  6. socket系統呼叫將資料從核心搬移到使用者態。(第3次拷貝:核心空間->使用者空間)

研究者們發現,Linux核心協議棧在資料包的收發過程中,記憶體拷貝操作的時間開銷佔了整個處理過程時間開銷的65%,此外層間傳遞的系統呼叫時間也佔據了8%~10%。

 

協議棧的主要問題


針對單個資料包級別的資源分配和釋放

每當一個資料包到達網路卡,系統就會分配一個分組描述符用於儲存資料包的資訊和頭部,直到分組傳送到使用者態空間,其描述符才被釋放。此外,sk_buff龐大的資料結構中的大部分資訊對於大多數網路任務而言都是無用的.

 

流量的序列訪問


現代網路卡包括多個硬體的接收端擴充套件(receiver-side scaling, RSS)佇列可以將分組按照五元組雜湊函式分配到不同的接收佇列。使用這種技術,分組的捕獲過程可以被並行化,因為每個RSS佇列可以對映到一個特定的CPU核,並且可以對應相應的NAPI執行緒。這樣整個捕獲過程就可以做到並行化。

但是問題出現在之上的層次,Linux中的協議棧在網路層和傳輸層需要分析合併的所有資料包

  • ①所有流量在一個單一模組中被處理,產生效能瓶頸;
  • ②使用者程式不能夠從一個單一的RSS佇列接收訊息.

這就造成了上層應用無法利用現代硬體的並行化處理能力,這種在使用者態分配流量先後序列的過程降低了系統的效能,丟失了驅動層面所獲得的加速.

此外,從不同佇列合併的流量可能會產生額外的亂序分組

 

從驅動到使用者態的資料拷貝

從網路卡收到資料包到應用取走資料的過程中,存在至少2次資料包的複製

 

核心到使用者空間的上下文切換

從應用程式的視角來看,它需要執行系統呼叫來接收每個分組.每個系統呼叫包含一次從使用者態到核心態的上下文切換,隨之而來的是大量的CPU時間消耗.在每個資料包上執行系統呼叫時產生的上下文切換可能消耗近1 000個CPU週期.

 

跨記憶體訪問

例如,當接收一個64 B分組時,cache未命中造成了額外13.8%的CPU週期的消耗.另外,在一個基於NUMA的系統中,記憶體訪問的時間取決於訪問的儲存節點.因此,cache未命中在跨記憶體塊訪問環境下會產生更大的記憶體訪問延遲,從而導致效能下降.

 

2. 提高捕獲效率的技術


目前高效能報文捕獲引擎中常用的提高捕獲效率的技術,這些技術能夠克服之前架構的效能限制.

 

預分配和重用記憶體資源

這種技術包括:

  • 開始分組接收之前,預先分配好將要到達的資料包所需的記憶體空間用來儲存資料和後設資料(分組描述符)。尤其體現在,在載入網路卡驅動程式時就分配好 N 個描述符佇列(每個硬體佇列和裝置一個).
  • 同樣,當一個資料包被傳送到使用者空間,其對應的描述符也不會被釋放,而是重新用於儲存新到達的分組.得益於這一策略,在每個資料包分配/釋放所產生的效能瓶頸得到了消除.此外,也可以通過簡化sk_buff的資料結構來減少記憶體開銷.

 

資料包採用並行直接通道傳遞.


為了解決序列化的訪問流量,需要建立從RSS佇列到應用之間的直接並行資料通道.這種技術通過特定的RSS佇列、特定的CPU核和應用三者的繫結來實現效能的提升.

這種技術也存在一些缺點:

  • ①資料包可能會亂序地到達使用者態,從而影響某些應用的效能;
  • ②RSS使用Hash函式在每個接收佇列間分配流量.當不同核的資料包間沒有相互關聯時,它們可以被獨立地分析,但如果同一條

流的往返資料包被分配到不同的CPU核上時,就會造成低效的跨核訪問.

 

記憶體對映.


使用這種方法,應用程式的記憶體區域可以對映到核心態的記憶體區域,應用能夠在沒有中間副本的情況下讀寫這片記憶體區域.
用這種方式我們可以使應用直接訪問網路卡的DMA記憶體區域,這種技術被稱為零拷貝.但零拷貝也存在潛在的安全問題,嚮應用暴露出網路卡環形佇列和暫存器會影響系統的安全性和穩定性 .

 

資料包的批處理.


為了避免對每個資料包的重複操作的開銷,可以使用對資料包的批量處理.

這個策略將資料包劃分為組,按組分配緩衝區,將它們一起復制到核心/使用者記憶體.運用這種技術減少了系統呼叫以及隨之而來的上下文切換的次數;同時也減少了拷貝的次數,從而減少了平攤到處理和複製每個資料包的開銷.

但由於分組必須等到一個批次已滿或定時器期滿才會遞交給上層,批處理技術的主要問題是延遲抖動以及接收報文時間戳誤差的增加.

 

親和性與預取.


由於程式執行的區域性性原理,為程式分配的記憶體必須與正在執行它的處理器操作的記憶體塊一致,這種技術被稱為記憶體的親和性.
CPU親和性是一種技術,它允許程式或執行緒在指定的處理器核心上執行.

在核心與驅動層面,軟體和硬體中斷可以用同樣的方法指定具體的CPU核或處理器來處理,稱為中斷親和力.每當一個執行緒希望訪問所接收的資料,如果先前這些資料已被分配到相同CPU核的中斷處理程式接收,則它們在本地cache能夠更容易被訪問到.

 

3. 典型收包引擎


3.1 libpcap


參考:libpcap實現機制及介面函式

libpcap的包捕獲機制是在資料鏈路層增加一個旁路處理,不干擾系統自身的網路協議棧的處理,對傳送和接收的資料包通過Linux核心做過濾和緩衝處理,最後直接傳遞給上層應用程式。

  1. 資料包到達網路卡裝置。
  2. 網路卡裝置依據配置進行DMA操作。(第1次拷貝:網路卡暫存器->核心為網路卡分配的緩衝區ring buffer)
  3. 網路卡傳送中斷,喚醒處理器。
  4. 驅動軟體從ring buffer中讀取,填充核心skbuff結構(第2次拷貝:核心網路卡緩衝區ring buffer->核心專用資料結構skbuff)
  5. 接著呼叫netif_receive_skb函式:
    如果有抓包程式,由網路分介面進入BPF過濾器,將規則匹配的報文拷貝到系統核心快取 (第3次拷貝)。BPF為每一個要求服務的抓包程式關聯一個filter和兩個buffer。BPF分配buffer 且通常情況下它的額度是4KB the store buffer 被使用來接收來自介面卡的資料; the hold buffer被使用來拷貝包到應用程式。
  6. 處理資料鏈路層的橋接功能;根據skb->protocol欄位確定上層協議並提交給網路層處理,進入網路協議棧,進行高層處理。libpcap繞過了Linux核心收包流程中協議棧部分的處理,使得使用者空間API可以直接呼叫套接字PF_PACKET從鏈路層驅動程式中獲得資料包文的拷貝,將其從核心緩衝區拷貝至使用者空間緩衝區第4次拷貝

 

3.2 libpcap-mmap


libpcap-mmap是對舊的libpcap實現的改進,新版本的libpcap基本都採用packet_mmap機制(見3.4 PACKET_MMAP小節)。PACKET_MMAP通過mmap,減少一次記憶體拷貝(第4次拷貝沒有了),減少了頻繁的系統呼叫,大大提高了報文捕獲的效率。

 

3.3 PF_RING


參考:PF_RING學習筆記

我們看到之前libpcap有4次記憶體拷貝。

libpcap_mmap有3次記憶體拷貝。

PF_RING提出的核心解決方案便是減少報文在傳輸過程中的拷貝次數。

我們可以看到,相對與libpcap_mmap來說,pfring允許使用者空間記憶體直接和rx_buffer做mmap。這又減少了一次拷貝(libpcap_mmap的第2次拷貝:rx_buffer->skb

PF-RING ZC實現了DNA(Direct NIC Access 直接網路卡訪問)技術,將使用者記憶體空間對映到驅動的記憶體空間,使使用者的應用可以直接訪問網路卡的暫存器和資料。

通過這樣的方式,避免了在核心對資料包快取,減少了一次拷貝(libpcap的第1次拷貝,DMA到核心緩衝區的拷貝)。這就是完全的零拷貝。

其缺點是,只有一個應用可以在某個時間開啟DMA ring(請注意,現在的網路卡可以具有多個RX / TX佇列,從而就可以在每個佇列上同時一個應用程式),換而言之,使用者態的多個應用需要彼此溝通才能分發資料包。

 

https://cloud.tencent.com/developer/article/1521276


PF_RING針對libpcap的改進方法:將網路卡接收到的資料包儲存在一個環狀快取中,這個環狀快取有兩個介面,一個供網路卡向其中寫資料,另一個為應用層程式提供讀取資料包的介面,從而減少了記憶體的拷貝次數,若將收到的資料包分發給多個環形緩衝區則可以實現多執行緒應用程式的讀取。

每建立一個PF_RING套接字便分配一個環形緩衝區,當套接字結束時釋放緩衝區,不同套接字擁有不同緩衝區,將PF_RING套接字繫結到某網路卡上,當資料包到達網路卡時,將其放入環形緩衝區,若緩衝區已滿,則丟棄該資料包。當有新的資料包到達時,直接覆蓋掉已經被使用者空間讀取過的資料包空間。

網路卡接收到新的資料包後,直接寫入環形緩衝區,以便應用程式直接讀,若應用程式需要向外傳送資料包,也可以直接將資料包寫入環形緩衝區,以便網路卡驅動程式將該資料包傳送到相應介面上。

PF_RING的工作流程:

普通的網路接收函式中,網路卡驅動到核心傳遞資料的核心是netif_rx()函式,若使用了裝置輪詢(NAPI)機制(中斷機制+輪詢機制,以中斷方式通知系統,將裝置註冊到輪詢佇列後關閉中斷,輪詢佇列中註冊的網路裝置從而讀取資料包,採用NAPI機制可以減少中斷觸發的時間),則傳遞資料的核心是netif_receive_skb()函式。PF_RING定義了一個處理函式skb_ring_handler(),插入前兩個核心函式的起始位置,每當有資料包需要傳遞時,先經過skb_ring_handler()的處理。

(1) 一般的資料包捕獲(libpcap):

(2)非零拷貝的pf_ring(pf_ring noZC):

(3)零拷貝的pf_ring(pf_ring ZC):

PF_RING有三種工作模式:

  1. Transparent_mode=0:使用者通過mmap獲取已經拷貝到核心的資料包,相當於libpcap-mmap技術;
  2. Transparent_mode=1:將資料包放入環形緩衝區;
  3. Transparent_mode=2:資料包只由PF_RING模組處理,不經過核心,直接mmap到使用者態

後兩種模式需要使用PF_RING特殊定製的網路卡驅動:pf_ring.ko

PF_RING部分內容分享自微信公眾號 - nginx遇上redis(GGame_over_the_world)

原文出處及轉載資訊見文內詳細說明,如有侵權,請聯絡 yunjia_community@tencent.com 刪除。

原始發表時間:2019-08-29

 

3.4 PACKET_MMAP


https://blog.csdn.net/dandelionj/article/details/16980571

PACKET_MMAP實現的程式碼都在net/packet/af_packet.c中,其中一些巨集、結構等定義在include/linux/if_packet.h中。

PACKET_MMAP的實現原理


PACKET_MMAP在核心空間中分配一塊核心緩衝區,然後使用者空間程式呼叫mmap對映到使用者空間。將接收到的skb拷貝到那塊核心緩衝區中,這樣使用者空間的程式就可以直接讀到捕獲的資料包了。

如果沒有開啟PACKET_MMAP,只是依靠AF_PACKET非常的低效。它有緩衝區的限制,並且每捕獲一個報文就需要一個系統呼叫,如果為了獲得packet的時間戳就需要兩個系統呼叫了(獲得時間戳還需要一個系統呼叫,libpcap就是這樣做的)。

PACKET_MMAP非常高效,它提供一個對映到使用者空間的大小可配置的環形緩衝區。這種方式,讀取報文只需要等待報文就可以了,大部分情況下不需要系統呼叫(其實poll也是一次系統呼叫)。通過核心空間和使用者空間共享的緩衝區還可以起到減少資料拷貝的作用。

當然為了提高捕獲的效能,不僅僅只是PACKET_MMAP。如果你在捕獲一個高速網路中的資料,你應該檢查NIC是否支援一些中斷負載緩和機制或者是NAPI,確定開啟這些措施。

PACKET_MMAP減少了系統呼叫,不用recvmsg就可以讀取到捕獲的報文,相比原始套接字+recvfrom的方式,減少了一次拷貝和一次系統呼叫。

 

PACKET_MMAP的使用:

從系統呼叫的角度來看待如何使用PACKET_MMAP,可以從 libpcap底層實現變化的分析中strace的分析中看出來:

[setup]:
socket()   ------> 捕獲socket的建立
setsockopt()  ------> 環形緩衝區的分配
mmap()   ------> 將分配的緩衝區對映到使用者空間中
[capture]
poll()   ------> 等待新進的報文
[shutdown]
close   ------> 銷燬捕獲socket和所有相關的資源

接下來的這些內容,翻譯自Document/networking/packet_mmap.txt,但是根據需要有所刪減


1. socket的建立和銷燬如下,與不使用PACKET_MMAP是一樣的:

int fd = socket(PF_PACKET, mode, htons(ETH_P_ALL))

如果mode設定為SOCK_RAW,鏈路層資訊也會被捕獲;如果mode設定為SOCK_DGRAM,那麼對應介面的鏈路層資訊捕獲就不會被支援,核心會提供一個虛假的頭部。

銷燬socket和釋放相關的資源,可以直接呼叫一個簡單的close()系統呼叫就可以了。


2. PACKET_MMAP的設定

使用者空間設定PACKET_MMAP只需要下面的系統呼叫就可以了:

setsockopt(fd, SOL_PACKET, PACKET_RX_RING, (void *)&req, sizeof(req));

上面系統呼叫中最重要的就是req引數,其定義如下:

    struct tpacket_req
    {
        unsigned int    tp_block_size;  /* Minimal size of contiguous block */
        unsigned int    tp_block_nr;    /* Number of blocks */
        unsigned int    tp_frame_size;  /* Size of frame */
        unsigned int    tp_frame_nr;    /* Total number of frames */
    };

這個結構被定義在include/linux/if_packet.h中,在捕獲程式中建立一個不可交換(unswappable)記憶體的環形緩衝區。通過被對映的記憶體,捕獲程式就可以無需系統呼叫就可以訪問到捕獲的報文和報文相關的元資訊,像時間戳等。

捕獲frame被劃分為多個block,每個block是一塊物理上連續的記憶體區域,有tp_block_size/tp_frame_size個frame。block的總數是tp_block_nr。其實tp_frame_nr是多餘的,因為我們可以計算出來:

    frames_per_block = tp_block_size/tp_frame_size

實際上,packet_set_ring檢查下面的條件是否正確:

    frames_per_block * tp_block_nr == tp_frame_nr

下面我們可以一個例子:

     tp_block_size= 4096
     tp_frame_size= 2048
     tp_block_nr  = 4
     tp_frame_nr  = 8

得到的緩衝區結構應該如下:

        block #1                 block #2         
+---------+---------+    +---------+---------+    
| frame 1 | frame 2 |    | frame 3 | frame 4 |    
+---------+---------+    +---------+---------+    

        block #3                 block #4
+---------+---------+    +---------+---------+
| frame 5 | frame 6 |    | frame 7 | frame 8 |
+---------+---------+    +---------+---------+

每個frame必須放在一個block中,每個block儲存整數個frame,也就是說一個frame不能跨越兩個block。


3. 對映和使用環形緩衝區

在使用者空間對映緩衝區可以直接使用方便的mmap()函式。雖然那些buffer在核心中是由多個block組成的,但是對映後它們在使用者空間中是連續的。

    mmap(0, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

如果tp_frame_size能夠整除tp_block_size,那麼每個frame都將會是tp_frame_size長度;如果不是,那麼tp_block_size/tp_frame_size個frame之間就會有空隙,那是因為一個frame不會跨越兩個block。

在每一個frame的開始有一個status域(可以檢視struct tpacket_hdr),這些status定義在include/linux/if_packet.h中:

#define TP_STATUS_KERNEL   0
#define TP_STATUS_USER   1
#define TP_STATUS_COPY   2
#define TP_STATUS_LOSING   4
#define TP_STATUS_CSUMNOTREADY   8

這裡我們只關心前兩個,TP_STATUS_KERNEL和TP_STATUS_USER。如果status為TP_STATUS_KERNEL,表示這個frame可以被kernel使用,實際上就是可以將存放捕獲的資料存放在這個frame中;如果status為TP_STATUS_USER,表示這個frame可以被使用者空間使用,實際上就是這個frame中存放的是捕獲的資料,應該讀出來。

核心將所有的frame的status初始化為TP_STATUS_KERNEL,當核心接受到一個報文的時候,就選一個frame,把報文放進去,然後更新它的狀態為TP_STATUS_USER(這裡假設不出現其他問題,也就是忽略其他的狀態)。使用者程式讀取報文,一旦報文被讀取,使用者必須將frame對應的status設定為0,也就是設定為TP_STATUS_KERNEL,這樣核心就可以再次使用這個frame了。

使用者可以通過poll或者是其他機制來檢測環形緩衝區中的新報文:

struct pollfd pfd;

pfd.fd = fd;

pfd.revents = 0;
pfd.events = POLLIN|POLLRDNORM|POLLERR;

if (status == TP_STATUS_KERNEL)
    retval = poll(&pfd, 1, timeout);

先檢查狀態值,然後再對frame進行輪循,這樣就可以避免競爭條件了(如果status已經是TP_STATUS_USER了,也就是說在呼叫poll前已經有了一個報文到達。這個時候再呼叫poll,並且之後不再有新報文到達的話,那麼之前的那個報文就無法讀取了,這就是所謂的競爭條件)。


在libpcap-1.0.0中是這麼設計的:

pcap-linux.c中的pcap_read_linux_mmap:

// 如果frame的狀態在poll前已經為TP_STATUS_USER了,說明已經在poll前已經有一個資料包被捕獲了,如果poll後不再有資料包被捕獲,那麼這個報文不會被處理,這就是所謂的競爭情況。

if ((handle->md.timeout >= 0) && !pcap_get_ring_frame(handle, TP_STATUS_USER)) {
    struct pollfd pollinfo;
    int ret;
    
    pollinfo.fd = handle->fd;
    pollinfo.events = POLLIN;
    
    do {
        /* poll() requires a negative timeout to wait forever */
        ret = poll(&pollinfo, 1, (handle->md.timeout > 0)? handle->md.timeout: -1);
        if ((ret < 0) && (errno != EINTR)) {
            return -1;
        }
        ......
    } while (ret < 0);
}

//依次處理捕獲的報文

while ((pkts < max_packets) || (max_packets <= 0)) { 
    ...... 
    //如果frame的狀態為TP_STATUS_USER就讀出資料frame,否則就退出迴圈。
    //注意這裡是環形緩衝區
    h.raw = pcap_get_ring_frame(handle, TP_STATUS_USER);
    if (!h.raw)break; 
    ......
    /* pass the packet to the user */
    pkts++;
    callback(user, &pcaphdr, bp);
    handle->md.packets_read++;
skip:
    /* next packet */
    switch (handle->md.tp_version) {
        case TPACKET_V1:
            //重新設定frame的狀態為TP_STATUS_KERNEL 
            h.h1->tp_status = TP_STATUS_KERNEL; 
            break;
        ...... 
    }
}

 

PACKET_MMAP原始碼分析


這裡就不再像上一篇文章中那樣大段大段的貼上程式碼了,只是分析一下流程就可以了,需要的同學可以對照著follow一下程式碼;-)

資料包進入網路卡後,建立了skb,之後會進入軟中斷處理,呼叫netif_receive_skb,並呼叫dev_add_pack註冊的一些func。很明顯可以看到af_packet.c中的tpacket_rcv和packet_rcv就是我們找的目標。

tpacket_rcv是PACKET_MMAP的實現,packet_rcv是普通AF_PACKET的實現。


tpacket_rcv:

  • 1. 進行些必要的檢查
  • 2. 執行run_filter,通過BPF過濾中我們設定條件的報文,得到需要捕獲的長度snaplen
  • 3. 在ring buffer中查詢TP_STATUS_KERNEL的frame
  • 4. 計算macoff、netoff等資訊
  • 5. 如果snaplen+macoff>frame_size,並且skb為共享的,那麼就拷貝skb  <一般不會拷貝>
if(skb_shared(skb))
    skb_clone()
  • 6. 將資料從skb拷貝到kernel Buffer中  <拷貝>
skb_copy_bits(skb, 0,  h.raw+macoff, snaplen);
  • 7. 設定拷貝到frame中報文的頭部資訊,包括時間戳、長度、狀態等資訊
  • 8. flush_dcache_page()把某頁在data cache中的內容同步回記憶體。

x86應該不用這個,這個多為RISC架構用的

  • 9. 呼叫sk_data_ready,通知睡眠程式,呼叫poll
  • 10. 應用層在呼叫poll返回後,就會呼叫pcap_get_ring_frame獲得一個frame進行處理。這裡面沒有拷貝也沒有系統呼叫。

開銷分析:1次拷貝+1個系統呼叫(poll)


packet_rcv:

  • 1. 進行些必要的檢查
  • 2. 執行run_filter,通過BPF過濾中我們設定條件的報文,得到需要捕獲的長度snaplen
  • 3. 如果skb為共享的,那麼就拷貝skb  <一般都會拷貝>
if(skb_shared(skb))
    skb_clone()
  • 4. 設定拷貝到frame中報文的頭部資訊,包括時間戳、長度、狀態等資訊
  • 5. 將skb追加到socket的sk_receive_queue中
  • 6. 呼叫sk_data_ready,通知睡眠程式有資料到達
  • 7. 應用層睡眠在recvfrom上,當資料到達,socket可讀的時候,呼叫packet_recvmsg,其中將資料拷貝到使用者空間。  <拷貝>

skb_recv_datagram()從sk_receive_queue中獲得skb

skb_copy_datagram_iovec()將資料拷貝到使用者空間

開銷分析:2次拷貝+1個系統呼叫(recvfrom)

注:其實在packet處理之前還有一次拷貝過程,在NIC Driver中,建立一個skb,然後NIC把資料DMA到skb的data中。

在另外一些ZeroCopy實現中(例如 ntz),如果不希望NIC資料進入協議棧的話,就可以不用考慮skb_shared的問題了,直接將資料從NIC Driver中DMA到制定的一塊記憶體,然後使用mmap到使用者空間。這樣就只有一次DMA過程,當然DMA也是一種拷貝;-)

關於資料包如何從NIC Driver到packet_rcv/tpacket_rcv,資料包經過中斷、軟中斷等處理,進入netif_receive_skb中對skb進行分發,就會呼叫dev_add_pack註冊的packet_type->func。

 

3.5 DPDK


參考:DPDK解析-----DPDK,PF_RING對比

pf-ring zc和dpdk均可以實現資料包的零拷貝,兩者均旁路了核心,但是實現原理略有不同。pf-ring zc通過zc驅動(也在應用層)接管資料包,dpdk基於UIO實現。

 

UIO+mmap 實現零拷貝(zero copy)


UIO(Userspace I/O)是執行在使用者空間的I/O技術。Linux系統中一般的驅動裝置都是執行在核心空間,而在使用者空間用應用程式呼叫即可,而UIO則是將驅動的很少一部分執行在核心空間,而在使用者空間實現驅動的絕大多數功能。
採用Linux提供UIO機制,可以旁路Kernel,將所有報文處理的工作在使用者空間完成。

UIO+PMD 減少中斷和CPU上下文切換


DPDK的UIO驅動遮蔽了硬體發出中斷,然後在使用者態採用主動輪詢的方式,這種模式被稱為PMD(Poll Mode Driver)。

與DPDK相比,pf-ring(no zc)使用的是NAPI polling和應用層polling,而pf-ring zc與DPDK類似,僅使用應用層polling。

 

HugePages 減少TLB miss


在作業系統引入MMU(Memory Management Unit)後,CPU讀取記憶體的資料需要兩次訪問記憶體。第一次要查詢頁表將邏輯地址轉換為實體地址,然後訪問該實體地址讀取資料或指令。

為了減少頁數過多,頁表過大而導致的查詢時間過長的問題,便引入了TLB(Translation Lookaside Buffer),可翻譯為地址轉換緩衝器。TLB是一個記憶體管理單元,一般儲存在暫存器中,裡面儲存了當前最可能被訪問到的一小部分頁表項。

引入TLB後,CPU會首先去TLB中定址,由於TLB存放在暫存器中,且其只包含一小部分頁表項,因此查詢速度非常快。若TLB中定址成功(TLB hit),則無需再去RAM中查詢頁表;若TLB中定址失敗(TLB miss),則需要去RAM中查詢頁表,查詢到後,會將該頁更新至TLB中。

而DPDK採用HugePages ,在x86-64下支援2MB、1GB的頁大小,大大降低了總頁個數和頁表的大小,從而大大降低TLB miss的機率,提升CPU定址效能。

 

其它優化


SNA(Shared-nothing Architecture),軟體架構去中心化,儘量避免全域性共享,帶來全域性競爭,失去橫向擴充套件的能力。NUMA體系下不跨Node遠端使用記憶體。

SIMD(Single Instruction Multiple Data),從最早的mmx/sse到最新的avx2,SIMD的能力一直在增強。DPDK採用批量同時處理多個包,再用向量程式設計,一個週期內對所有包進行處理。比如,memcpy就使用SIMD來提高速度。
cpu affinity

 

3.6 XDP(eXpress Data Path)


參考:DPDK and XDP

xdp代表eXpress資料路徑,使用ebpf 做包過濾,相對於dpdk將資料包直接送到使用者態,用使用者態當做快速資料處理平面,xdp是在驅動層建立了一個資料快速平面。

在資料被網路卡硬體dma到記憶體,分配skb之前,對資料包進行處理。

請注意,XDP並沒有對資料包做Kernel bypass,它只是提前做了一點預檢而已。

相對於DPDK,XDP具有以下優點:

  • 無需第三方程式碼庫和許可
  • 同時支援輪詢式和中斷式網路
  • 無需分配大頁
  • 無需專用的CPU
  • 無需定義新的安全網路模型

XDP的使用場景包括:

  • DDoS防禦
  • 防火牆
  • 基於XDP_TX的負載均衡
  • 網路統計
  • 複雜網路取樣
  • 高速交易平臺

 

PS:使用XDP(eXpress Data Path)防禦DDoS攻擊

https://blog.csdn.net/dog250/article/details/77993218


 

人們總是覺得Linux協議棧實現得不夠好,特別是效能方面,所以在這種不信任的基調下,人們當然很自信地覺得把資料包從協議棧里拉出來,自己會處理得比核心協議棧要好,但是,真的是這樣嗎?我來猜測幾點背後的原因。

  首先,這可能是因為Linux協議棧是作為核心一個子系統套件存在的,它無法脫離核心作為一個模組存在,這就意味著如果你改了其實現的細節,就必然要重新編譯核心並重啟系統,別看這麼簡單的一個操作,對於很多線上系統是吃不消的,這就跟Windows裝完軟體要重啟系統(重啟系統僅僅就是為了重新載入登錄檔,windows設計者是省事了,使用者煩死了!)一樣煩人,所以,人們自然而然地需要一種動態HOOK的機制,在裡面可以實現一些自己的邏輯。

  其次,Linux核心協議棧說實話真的扛不住高併發,大流量,特別是它是在20世紀90年代作為一個通用作業系統實現的,只是後來從Linux社群迸發的一種文化讓其逐漸深入各個專業的領域,比如大型伺服器,專用網路裝置等,這必然存在一個逐步進化的過程。一句話,Linux的協議棧不是為1Gbps/10Gbps/40Gbps這些網路設計的,要想支援它們,你就必須自己做點什麼。

 

新的分層方法


多人會把Linux協議棧的實現按照OSI模型或者TCP/IP模型分成對應的層次,比如什麼鏈路層,IP層,TCP層。其實這根本不對,Linux協議棧實現從鏈路層通用處理到IP層路由,並沒有經過什麼顯式的關卡一樣的門,僅僅支援一些函式呼叫而已。

記住,OSI模型也好,TCP/IP模型也罷,所謂的分層僅僅是邏輯檢視上的分層,好在讓人們便於理解以及便於界定軟體設計的邊界和分工,所以可以說,邏輯上分層這些層次之間都是隱式的門,然而在效能攸關的實現領域,顯式的門處在完全不同的位置!

  如果談優化,我們就必須要找到顯式的門,找到了,繞過它便是優化!

  所以說,我之前的那些想法,比如在Netfilter的PREROUTING上做更多的事,優化效果並不明顯,就是因為Netfilter並不是門,它也只是一些函式呼叫。

  那麼,什麼是門?所謂的門,就是那些開銷巨大,你必須花點代價才能過去的點。舉幾個例子,必須使用者態到核心態的系統呼叫,比如套接字處理的自旋鎖,比如記憶體分配,或者說現實中的深圳羅湖口岸,深圳布吉關,梅林關…

  按照以上的說法,我來重新把Linux協議棧來分層,有了這個新的層次,在哪裡做優化就顯而易見了(紅色區域開銷巨大,是為”門“):

我們看到資料包從接收一直到使用者態,主要經歷了兩個門,其中一個是skb分配,另一個是套接字鎖定,在之前那篇《SYNPROXY抵禦DDoS攻擊的原理和優化》文章中,我採用的方法顯然是繞開了套接字鎖定,抗DDoS的效能便得到了很大的提升,然而縱觀我幾乎所有的文章,基本上都是繞此門而優化的。因為這是一種便宜的方案。

 

繞過更低層的門


在2014年時,接觸過一段時間netmap,當時還調研了基於Tilera做網路處理加速,不管怎樣,這些都是跟DPDK類似的方案,DPDK應該都聽說過,Intel的一個被吵得火熱燙手的專用框架。

  既然大家都一樣,Intel是老大,自然就沒有Tilera什麼事了(它們的方案又貴,又晦澀),這就是DPDK被炒火的原因,Intel之類的公司,放個屁都是香的。

  其實,類似DPDK的加速方案原理都非常簡單,那就是完全繞開核心實現的協議棧,把資料包直接從網路卡拉到使用者態,依靠Intel自身處理器的一些專門優化,來高速處理資料包,你可知道在這類方案中,CPU可是專門處理資料包的,什麼核心態,使用者態,都無關緊要,採用map機制,專門的處理程式可以非常高效地在任意時間讀取並處理資料包,想想CPU的處理速度換算成pps是什麼概念吧,如果一個CPU什麼都不幹,專門處理資料包,那將是非常猛的線速處理了。

  DPDK沒什麼大不了的,就跟當年的EJB一樣,全靠廠商推動,依賴的是一攬子方案,並非一個樸素通用的框架。你給DPDK做個手術跑在ARM上試試,就算能跑,很多功能也都是廢的。

  總之,在skb還未分配的網路卡驅動層面做一些事情是必要的,至於怎麼做,做什麼,那花樣就多了去了。


附:澄清一個單位概念

注意,線速是一個指標單位,表徵處理效能而不是傳輸效能,並不是像在網線上跑一樣,如果那樣,直接叫光速或者70%光速好了…同樣總是引起混淆的單位是年一遇,百年一遇並不是講一百年遇到一次,而是百(年一遇),數值是百,單位是年一遇。

 

XDP

Linux eBPF和XDP高速處理資料包;使用EBPF編寫XDP網路過濾器;高效能ACL

介紹Calico eBPF資料平面:Linux核心網路、安全性和跟蹤(Kubernetes、kube-proxy)

eBPF.io eBPF文件:擴充套件的資料包過濾器(BPF)


釋一個名詞,XDP的意思是eXpress Data Path。它能做什麼呢?很簡單,下圖說明:

其中,最顯而易見的是,竟然可以在如此低的層面上把資料包丟棄或者回彈回去,如果面臨DDoS攻擊,採用這種方式的話,資料包就沒有必要上升到Netfilter層面再被丟棄了。說白了,XDP允許資料包在進入Linux協議棧之前就能受到判決。

  別的不管,我只管DDoS防護,現在的問題是XDP靠什麼機制知道這個資料包是不是要被丟棄的呢?

  答案太響亮了,竟然是eBPF

Linux核心一直是實現監視/可觀察性,網路和安全性的理想場所。不幸的是,這通常是不切實際的,因為它需要更改核心原始碼或載入核心模組,並導致彼此堆疊的抽象層。eBPF是一項革命性的技術,可以在Linux核心中執行沙盒程式,而無需更改核心原始碼或載入核心模組。通過使Linux核心可程式設計,基礎架構軟體可以利用現有的層,使其更加智慧和功能豐富,而無需繼續為系統增加額外的複雜性。

  1. Linux eBPF和XDP高速處理資料包;使用EBPF編寫XDP網路過濾器;高效能ACL
  2. 介紹Calico eBPF資料平面:Linux核心網路、安全性和跟蹤(Kubernetes、kube-proxy)
  3. eBPF.io eBPF文件:擴充套件的資料包過濾器(BPF)

  事實上,這相當於在網路卡驅動層面執行了一個eBPF程式,該程式決定資料包何去何從。最簡單的想法是,假設1000個IP地址是已知的異常地址,我們將其封裝在一個高效的查詢結構中,然後將這個結構包括查詢過程編譯成eBPF位元組碼並注入到網路卡,網路卡收到資料包後,執行該eBPF位元組碼,如果資料包源IP地址被找到,則丟棄!

 

這不就是我那個n+1模型以及iptables的bpf match中需要的效果嗎:

更加令人興奮的是,這一切竟然本來就是存在的現成的東西。推薦幾個連結:

以往,我們認為核心是確定的程式,我們能餵給它的只有資料,雖然Linux核心大部分都跑在馮諾依曼架構為主(如今基本都是混合架構)的機器上,但這種認知反而更像是哈佛架構,馮諾依曼機器本來就是程式和資料統一儲存的,現在,eBPF程式可以被灌入網路卡驅動了,這簡直就跟網路卡硬體的Firmware一樣為網路卡注入了新的功能。不管是你認為程式已經資料化了,還是這種方案真的迴歸了馮諾依曼模型,都無所謂,重要的是,它提升了效能。

  請注意,XDP並沒有對資料包做Kernel bypass,它只是提前做了一點預檢而已,目前它也只能有三種Action,繼續去掛號,直接殺掉,或者打道回府,看來這只是減少了掛號服務生的負擔…這與DPDK這種半道黃牛是完全不同的,DPDK完全可能把你拉到一個黑診所…不過,XDP思路非常清晰,後續的可能性誰也無法預估,說不定真有一天XDP會直接接管路由查詢甚至TCP握手處理呢。

  本節的最後,再一次提一下一個熟悉的朋友,那就是Cisco的ACL,我一直都覺得在Cisco的中低端裝置上,ACL的匹配就是按照XDP的方式做的,把使用者輸入的ACL規則編譯成eBPF之類的位元組碼,然後灌入到需要使能的網路卡上,我想象不出除了這種方式,還能有什麼更高效的方式,也希望Cisco的朋友能有機會告知究竟…

 

關於DDoS防禦


經,我做過一個模組,就是在核心裡記錄訪問本機次數最多的前幾個IP,然後把它們列入黑名單,認為它們是攻擊者。然而,這是錯的。最終並沒有揪出攻擊者,反而記錄訪問次數這件事卻差點耗盡CPU…

  收斂點說吧,這件事做的並不全錯,記錄前n個訪問最多的IP並阻止它們確實能起到一定的防禦效果,但是在做這件事之前,完全是有辦法做到更深層過濾的,那就是,把不請自來的資料包的源IP地址全部加入黑名單,這樣,nf_conntrack,iptables以及XDP三者聯動,將會是一個完美的DDoS防禦方案。

  1. nf_conntrack負責識別不請自來的ACK攻擊包
  2. 自研的記錄模組負責識別Syn flood攻擊包
  3. iptables LOG修改後負責將攻擊源加入黑名單
  4. XDP負責阻止黑名單裡的IP繼續訪問

iptables詳解(1):iptables概念

iptables詳解(2):路由表

 

怎麼做?


抱歉,什麼也做不了!

  目前XDP並不是支援所有的網路卡,我能接觸到的也就是Intel的網路卡,截止到4.10核心,XDP也就支援Mellanox(mlx4和mlx5)和QLogic,另外還有cavium的網路卡。

  這個局面讓我根本無法去動手實踐而只能紙上談兵…不過這好似解除安裝了我巨大的負擔,讓我滿懷著些許希望度過一個不用程式設計的週末,不然我可能又要在週末搞什麼XDP測試了…但我仍然滿懷希望,等待核心升級後在e1000e的驅動裡看到XDP的鉤子…

 

4. 無鎖佇列技術


【共享記憶體】基於共享記憶體的無鎖訊息佇列設計

DPDK無鎖佇列rte_ring相關程式碼及示例程式(rte_ring.h,rte_ring.c,main.c,makefile)

無鎖佇列的實現

DPDK ring庫:環形緩衝區的解剖

在報文捕獲的流程中,無鎖佇列是一個很重要的資料結構。生產者(網路卡)寫資料和消費者(使用者態程式)讀資料,不加鎖,能極大提升效率。

無鎖佇列實現主要依賴的技術有:

 

CAS原子指令操作


CAS(Compare and Swap,比較並替換)原子指令,用來保障資料的一致性。

指令有三個引數,當前記憶體值 V、舊的預期值 A、更新的值 B,當且僅當預期值 A和記憶體值 V相同時,將記憶體值修改為 B並返回true,否則什麼都不做,並返回false。

 

記憶體屏障


執行運算的時候,每個CPU核心從記憶體讀到各自的快取中,結束後再從快取更新到記憶體,這會引起執行緒間資料的不同步,故需要記憶體屏障強制把寫緩衝區或快取記憶體中的資料等寫回主記憶體。

主要分為讀屏障和寫屏障:讀屏障可以讓 cache中的資料失效,強制重新從主記憶體載入資料;

寫屏障能使cache 中的資料更新寫入主記憶體。

在實現 valotitle關鍵字中就用到了記憶體屏障,從而保證執行緒A對此變數的修改,其他執行緒獲取的值為最新的值。

 

 

5. 基於pfring/dpdk的應用


按照傳統的觀念,中間網路節點只能按照協議棧的層次一層一層地解析資料包,所謂路由器是三層裝置,交換機是二層裝置,防火牆分為二層防火牆和三層防火牆。

使用PF_RING/DPDK的裝置,它可以將資料包直接從網路卡的晶片DMA到你機器上的記憶體,然後你通過一個應用程式而不是核心協議棧來處理資料包。

至於說你的應用程式怎麼處置資料包,我來列舉幾個:

  • 1.深度解析資料包,按照各種你可以想到的粒度來解析會話,然後記錄審計資訊;
  • 2.提供高效能的入侵檢測功能;
  • 3.轉發資料包,按照路由器的方式。但是不再僅僅通過查詢路由表的方式進行IP路由,而是可以通過各種各樣的方式,轉發表完全由你自己定義,比如實現一個通用的SDN流表;
  • 4.根據上面第2點的含義,你可以決定哪些包被丟棄,這就是一個高效能的防火牆。

相比核心協議棧的序列解決方案,使用PF_RING/DPDK是一個更加高效的方案,不但高效,而且靈活。如果你擁有多核心的處理器,你甚至可以在使用者態並行處理資料包的各個層資訊。

 

參考

無鎖佇列的實現

PF_RING

PACKET_MMAP實現原理分析

【原創】圖解抓包

使用XDP(eXpress Data Path)防禦DDoS攻擊

linux報文高速捕獲技術對比--napi/libpcap/afpacket/pfring/dpdk/xdp

 

相關閱讀

Linux eBPF和XDP高速處理資料包;使用EBPF編寫XDP網路過濾器;高效能ACL

介紹Calico eBPF資料平面:Linux核心網路、安全性和跟蹤(Kubernetes、kube-proxy)​​​​​​​

eBPF.io eBPF文件:擴充套件的資料包過濾器(BPF)​​​​​​​

iptables詳解(1):iptables概念

iptables詳解(2):路由表

【共享記憶體】基於共享記憶體的無鎖訊息佇列設計

DPDK無鎖佇列rte_ring相關程式碼及示例程式(rte_ring.h,rte_ring.c,main.c,makefile)​​​​​​​》

無鎖佇列的實現

DPDK ring庫:環形緩衝區的解剖

相關文章