資料包 MMAP

摘要

本文件介紹了 PACKET 套接字介面提供的 mmap() 功能。這種套接字型別用於

  1. 使用 tcpdump 等工具捕獲網路流量,

  2. 傳輸網路流量,或任何其他需要原始網路介面訪問許可權的場景。

操作指南可在以下連結找到:

請將您的意見傳送至:

為何使用 PACKET_MMAP

非 PACKET_MMAP 捕獲過程(普通 AF_PACKET)效率非常低下。它使用非常有限的緩衝區,並且捕獲每個資料包都需要一次系統呼叫,如果想獲取資料包的時間戳(像 libpcap 總是那樣做),則需要兩次系統呼叫。

另一方面,PACKET_MMAP 效率非常高。PACKET_MMAP 提供了一個大小可配置的迴圈緩衝區,該緩衝區對映在使用者空間中,可用於傳送或接收資料包。這樣,讀取資料包只需等待它們,大多數情況下無需發出單個系統呼叫。至於傳輸,可以透過一次系統呼叫傳送多個數據包以獲得最高頻寬。透過在核心和使用者之間使用共享緩衝區,還可以最大限度地減少資料包複製。

使用 PACKET_MMAP 提高捕獲和傳輸過程的效能固然很好,但這並非全部。至少,如果您以高速捕獲(這與 CPU 速度相關),您應該檢查您的網路介面卡的裝置驅動程式是否支援某種中斷負載緩解,或者(更好的是)是否支援 NAPI,並確保其已啟用。對於傳輸,請檢查您的網路裝置使用和支援的 MTU(最大傳輸單元)。CPU IRQ 將網路介面卡固定(CPU IRQ pinning)也可能是一個優勢。

如何使用 mmap() 改善捕獲過程

從使用者的角度來看,您應該使用更高階的 libpcap 庫,它是一個事實上的標準,幾乎所有作業系統(包括 Win32)都支援。

Packet MMAP 支援已整合到 libpcap 的 1.3.0 版本左右;TPACKET_V3 支援在 1.5.0 版本中新增。

如何直接使用 mmap() 改善捕獲過程

從系統呼叫的角度來看,使用 PACKET_MMAP 涉及以下過程:

[setup]     socket() -------> creation of the capture socket
            setsockopt() ---> allocation of the circular buffer (ring)
                              option: PACKET_RX_RING
            mmap() ---------> mapping of the allocated buffer to the
                              user process

[capture]   poll() ---------> to wait for incoming packets

[shutdown]  close() --------> destruction of the capture socket and
                              deallocation of all associated
                              resources.

套接字的建立和銷燬都很簡單,無論是否使用 PACKET_MMAP,操作方式都相同。

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

其中,`mode` 對於原始介面是 `SOCK_RAW`,可以捕獲鏈路層資訊;對於加工介面是 `SOCK_DGRAM`,不支援鏈路層資訊捕獲,並且核心會提供一個鏈路層偽頭部。

套接字和所有相關資源的銷燬透過簡單呼叫 `close(fd)` 完成。

與不使用 PACKET_MMAP 類似,可以使用一個套接字進行捕獲和傳輸。這可以透過一次 mmap() 呼叫對映分配的 RX 和 TX 緩衝區環來實現。詳見“迴圈緩衝區(環)的對映和使用”。

接下來我將描述 PACKET_MMAP 的設定及其約束,以及使用者程序中迴圈緩衝區的對映和使用。

如何直接使用 mmap() 改善傳輸過程

傳輸過程與捕獲類似,如下所示:

[setup]         socket() -------> creation of the transmission socket
                setsockopt() ---> allocation of the circular buffer (ring)
                                  option: PACKET_TX_RING
                bind() ---------> bind transmission socket with a network interface
                mmap() ---------> mapping of the allocated buffer to the
                                  user process

[transmission]  poll() ---------> wait for free packets (optional)
                send() ---------> send all packets that are set as ready in
                                  the ring
                                  The flag MSG_DONTWAIT can be used to return
                                  before end of transfer.

[shutdown]      close() --------> destruction of the transmission socket and
                                  deallocation of all associated resources.

套接字的建立和銷燬同樣簡單,與上一段中描述的捕獲方式相同。

int fd = socket(PF_PACKET, mode, 0);

如果我們只想透過此套接字進行傳輸,協議可以選擇設定為 0,這可以避免昂貴的 `packet_rcv()` 呼叫。在這種情況下,您還需要使用 `sll_protocol = 0` 繫結 (2) TX_RING。否則,例如使用 `htons(ETH_P_ALL)` 或任何其他協議。

將套接字繫結到您的網路介面是強制性的(零複製),以便了解迴圈緩衝區中使用的幀的頭部大小。

與捕獲一樣,每個幀包含兩部分:

   --------------------
   | struct tpacket_hdr | Header. It contains the status of
   |                    | of this frame
   |--------------------|
   | data buffer        |
   .                    .  Data that will be sent over the network interface.
   .                    .
   --------------------

bind() associates the socket to your network interface thanks to
sll_ifindex parameter of struct sockaddr_ll.

Initialization example::

   struct sockaddr_ll my_addr;
   struct ifreq s_ifr;
   ...

   strscpy_pad (s_ifr.ifr_name, "eth0", sizeof(s_ifr.ifr_name));

   /* get interface index of eth0 */
   ioctl(this->socket, SIOCGIFINDEX, &s_ifr);

   /* fill sockaddr_ll struct to prepare binding */
   my_addr.sll_family = AF_PACKET;
   my_addr.sll_protocol = htons(ETH_P_ALL);
   my_addr.sll_ifindex =  s_ifr.ifr_ifindex;

   /* bind socket to eth0 */
   bind(this->socket, (struct sockaddr *)&my_addr, sizeof(struct sockaddr_ll));

A complete tutorial is available at:
https://web.archive.org/web/20220404160947/https://sites.google.com/site/packetmmap/

預設情況下,使用者應將資料放置在:

frame base + TPACKET_HDRLEN - sizeof(struct sockaddr_ll)

因此,無論您選擇何種套接字模式(SOCK_DGRAM 或 SOCK_RAW),使用者資料的起始位置都將是:

frame base + TPACKET_ALIGN(sizeof(struct tpacket_hdr))

如果您希望將使用者資料放置在幀開頭(例如,與 SOCK_RAW 模式下的有效載荷對齊)的自定義偏移量處,您可以設定 `tp_net`(對於 SOCK_DGRAM)或 `tp_mac`(對於 SOCK_RAW)。為了使其工作,必須事先透過 `setsockopt()` 和 `PACKET_TX_HAS_OFF` 選項啟用此功能。

PACKET_MMAP 設定

從使用者級程式碼設定 PACKET_MMAP 是透過如下呼叫完成的:

  • 捕獲過程

    setsockopt(fd, SOL_PACKET, PACKET_RX_RING, (void *) &req, sizeof(req))
    
  • 傳輸過程

    setsockopt(fd, SOL_PACKET, PACKET_TX_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 */
};

此結構定義在 `/usr/include/linux/if_packet.h` 中,並建立了一個不可交換記憶體的迴圈緩衝區(環)。在捕獲過程中進行對映,允許讀取捕獲的幀和相關元資訊(如時間戳),而無需系統呼叫。

幀被分組為塊。每個塊是記憶體中物理上連續的區域,幷包含 `tp_block_size/tp_frame_size` 個幀。塊的總數是 `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_size` 時您需要考慮一些細節。參見“迴圈緩衝區(環)的對映和使用”。

PACKET_MMAP 設定約束

在 2.4.26(對於 2.4 分支)和 2.6.5(對於 2.6 分支)之前的核心版本中,PACKET_MMAP 緩衝區在 32 位架構中只能容納 32768 個幀,在 64 位架構中只能容納 16384 個幀。

塊大小限制

如前所述,每個塊都是記憶體中連續的物理區域。這些記憶體區域是透過呼叫 `__get_free_pages()` 函式分配的。顧名思義,此函式分配記憶體頁,第二個引數是“order”或頁數的二次冪,即(對於 PAGE_SIZE == 4096)order=0 ==> 4096 位元組,order=1 ==> 8192 位元組,order=2 ==> 16384 位元組等。`__get_free_pages` 分配區域的最大大小由 `MAX_PAGE_ORDER` 宏確定。更精確地說,限制可以計算為:

PAGE_SIZE << MAX_PAGE_ORDER

In a i386 architecture PAGE_SIZE is 4096 bytes
In a 2.4/i386 kernel MAX_PAGE_ORDER is 10
In a 2.6/i386 kernel MAX_PAGE_ORDER is 11

因此,在 2.4/2.6 核心中使用 i386 架構時,`get_free_pages` 分別可以分配高達 4MB 或 8MB 的記憶體。

使用者空間程式可以包含 `/usr/include/sys/user.h` 和 `/usr/include/linux/mmzone.h` 以獲取 `PAGE_SIZE` 和 `MAX_PAGE_ORDER` 的宣告。

頁面大小也可以透過 `getpagesize(2)` 系統呼叫動態確定。

塊數量限制

要了解 PACKET_MMAP 的約束,我們必須檢視用於儲存指向每個塊的指標的結構。

目前,此結構是一個使用 `kmalloc` 動態分配的向量,稱為 `pg_vec`,其大小限制了可分配的塊數量:

+---+---+---+---+
| x | x | x | x |
+---+---+---+---+
  |   |   |   |
  |   |   |   v
  |   |   v  block #4
  |   v  block #3
  v  block #2
 block #1

`kmalloc` 從預定大小的記憶體池中分配任意數量的物理連續位元組記憶體。這個記憶體池由 slab 分配器維護,slab 分配器最終負責進行分配,因此它限制了 `kmalloc` 可以分配的最大記憶體量。

在 2.4/2.6 核心和 i386 架構中,限制是 131072 位元組。`kmalloc` 使用的預定大小可以在 `/proc/slabinfo` 的“size-<bytes>”條目中檢視。

在 32 位架構中,指標長 4 位元組,因此指向塊的指標總數為:

131072/4 = 32768 blocks

PACKET_MMAP 緩衝區大小計算器

定義

<size-max>

是 `kmalloc` 可分配的最大大小(參見 `/proc/slabinfo`)

<pointer size>

取決於架構 -- sizeof(void *)

<page size>

取決於架構 -- `PAGE_SIZE` 或 `getpagesize(2)`

<max-order>

是用 `MAX_PAGE_ORDER` 定義的值

<frame size>

它是幀捕獲大小的上限(稍後詳述)

根據這些定義,我們將推匯出:

<block number> = <size-max>/<pointer size>
<block size> = <pagesize> << <max-order>

因此,最大緩衝區大小是:

<block number> * <block size>

並且,幀的數量是:

<block number> * <block size> / <frame size>

假設以下引數,適用於 2.6 核心和 i386 架構:

<size-max> = 131072 bytes
<pointer size> = 4 bytes
<pagesize> = 4096 bytes
<max-order> = 11

以及 <frame size> 的值為 2048 位元組。這些引數將產生:

<block number> = 131072/4 = 32768 blocks
<block size> = 4096 << 11 = 8 MiB.

因此緩衝區將具有 262144 MiB 的大小。它可以容納 262144 MiB / 2048 位元組 = 134217728 幀。

實際上,i386 架構不可能實現這種緩衝區大小。請記住,記憶體是在核心空間中分配的,對於 i386 核心,記憶體大小限制為 1GiB。

所有記憶體分配直到套接字關閉後才釋放。記憶體分配以 `GFP_KERNEL` 優先順序進行,這基本上意味著分配可以等待並交換其他程序的記憶體,以便分配所需的記憶體,因此通常可以達到限制。

其他約束

如果您檢視原始碼,您會發現我在此處繪製的“幀”不僅僅是鏈路層幀。每個幀的開頭有一個名為 `struct tpacket_hdr` 的頭部,在 PACKET_MMAP 中用於儲存鏈路層幀的元資訊,例如時間戳。因此,我們在此處繪製的“幀”實際上是以下內容(來自 `include/linux/if_packet.h`):

/*
  Frame structure:

  - Start. Frame must be aligned to TPACKET_ALIGNMENT=16
  - struct tpacket_hdr
  - pad to TPACKET_ALIGNMENT=16
  - struct sockaddr_ll
  - Gap, chosen so that packet data (Start+tp_net) aligns to
    TPACKET_ALIGNMENT=16
  - Start+tp_mac: [ Optional MAC header ]
  - Start+tp_net: Packet data, aligned to TPACKET_ALIGNMENT=16.
  - Pad to align to TPACKET_ALIGNMENT=16
*/

以下是在 `packet_set_ring` 中檢查的條件:

  • `tp_block_size` 必須是 `PAGE_SIZE` 的倍數 (1)

  • `tp_frame_size` 必須大於 `TPACKET_HDRLEN`(顯而易見)

  • `tp_frame_size` 必須是 `TPACKET_ALIGNMENT` 的倍數

  • `tp_frame_nr` 必須精確等於 `frames_per_block*tp_block_nr`

請注意,`tp_block_size` 應選擇為 2 的冪,否則會浪費記憶體。

迴圈緩衝區(環)的對映和使用

緩衝區在使用者程序中的對映透過常規的 `mmap` 函式完成。即使迴圈緩衝區由幾個物理上不連續的記憶體塊組成,它們在使用者空間中也是連續的,因此只需要一次 `mmap` 呼叫:

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

如果 `tp_frame_size` 是 `tp_block_size` 的約數,幀將以 `tp_frame_size` 位元組的間隔連續排列。否則,每 `tp_block_size/tp_frame_size` 個幀之間會有一個間隙。這是因為一個幀不能跨越兩個塊。

要使用一個套接字進行捕獲和傳輸,RX 和 TX 緩衝區環的對映必須透過一次 `mmap` 呼叫完成:

...
setsockopt(fd, SOL_PACKET, PACKET_RX_RING, &foo, sizeof(foo));
setsockopt(fd, SOL_PACKET, PACKET_TX_RING, &bar, sizeof(bar));
...
rx_ring = mmap(0, size * 2, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
tx_ring = rx_ring + size;

RX 必須是第一個,因為核心將 TX 環記憶體緊隨 RX 之後對映。

在每個幀的開頭有一個狀態欄位(參見 `struct tpacket_hdr`)。如果此欄位為 0,表示該幀已準備好供核心使用;如果不是,則存在使用者可以讀取的幀,並且適用以下標誌:

捕獲過程

來自 `include/linux/if_packet.h`

#define TP_STATUS_COPY          (1 << 1)
#define TP_STATUS_LOSING        (1 << 2)
#define TP_STATUS_CSUMNOTREADY  (1 << 3)
#define TP_STATUS_CSUM_VALID    (1 << 7)

TP_STATUS_COPY

此標誌表示幀(及相關的元資訊)由於大於 `tp_frame_size` 而被截斷。此資料包可以透過 `recvfrom()` 完全讀取。

為了使其工作,必須事先透過 `setsockopt()` 和 `PACKET_COPY_THRESH` 選項啟用此功能。

可緩衝以供 `recvfrom` 讀取的幀數量受限,與普通套接字類似。請參閱 `socket(7)` 手冊頁中的 `SO_RCVBUF` 選項。

TP_STATUS_LOSING

表示自上次透過 `getsockopt()` 和 `PACKET_STATISTICS` 選項檢查統計資料以來,發生了資料包丟失。

TP_STATUS_CSUMNOTREADY

目前用於出站 IP 資料包,其校驗和將在硬體中完成。因此,在讀取資料包時,我們不應嘗試檢查校驗和。

TP_STATUS_CSUM_VALID

此標誌表示資料包的傳輸頭部校驗和至少已在核心端驗證。如果未設定此標誌,則只要未設定 `TP_STATUS_CSUMNOTREADY`,我們就可以自行檢查校驗和。

為了方便,還有以下定義:

#define TP_STATUS_KERNEL        0
#define TP_STATUS_USER          1

核心將所有幀初始化為 `TP_STATUS_KERNEL`,當核心接收到資料包時,它會將其放入緩衝區並至少使用 `TP_STATUS_USER` 標誌更新狀態。然後使用者可以讀取資料包,一旦資料包被讀取,使用者必須將狀態欄位清零,以便核心可以再次使用該幀緩衝區。

使用者可以使用 `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);

先檢查狀態值再輪詢幀不會導致競態條件。

傳輸過程

這些定義也用於傳輸。

#define TP_STATUS_AVAILABLE        0 // Frame is available
#define TP_STATUS_SEND_REQUEST     1 // Frame will be sent on next send()
#define TP_STATUS_SENDING          2 // Frame is currently in transmission
#define TP_STATUS_WRONG_FORMAT     4 // Frame format is not correct

首先,核心將所有幀初始化為 `TP_STATUS_AVAILABLE`。要傳送資料包,使用者填充可用幀的資料緩衝區,將 `tp_len` 設定為當前資料緩衝區大小,並將其狀態欄位設定為 `TP_STATUS_SEND_REQUEST`。這可以在多個幀上完成。一旦使用者準備好傳輸,它會呼叫 `send()`。然後,所有狀態等於 `TP_STATUS_SEND_REQUEST` 的緩衝區都被轉發到網路裝置。核心會用 `TP_STATUS_SENDING` 更新已傳送幀的每個狀態,直到傳輸結束。

在每次傳輸結束時,緩衝區狀態返回 `TP_STATUS_AVAILABLE`。

header->tp_len = in_i_size;
header->tp_status = TP_STATUS_SEND_REQUEST;
retval = send(this->socket, NULL, 0, 0);

使用者也可以使用 `poll()` 檢查緩衝區是否可用:

(status == TP_STATUS_SENDING)

struct pollfd pfd;
pfd.fd = fd;
pfd.revents = 0;
pfd.events = POLLOUT;
retval = poll(&pfd, 1, timeout);

有哪些 TPACKET 版本可用以及何時使用它們?

int val = tpacket_version;
setsockopt(fd, SOL_PACKET, PACKET_VERSION, &val, sizeof(val));
getsockopt(fd, SOL_PACKET, PACKET_VERSION, &val, sizeof(val));

其中 'tpacket_version' 可以是 `TPACKET_V1`(預設)、`TPACKET_V2`、`TPACKET_V3`。

TPACKET_V1
  • 如果未透過 `setsockopt(2)` 另行指定,則為預設值

  • RX_RING, TX_RING 可用

TPACKET_V1 --> TPACKET_V2
  • 由於 `TPACKET_V1` 結構中使用了 `unsigned long`,使其支援 64 位,因此在 64 位核心和 32 位使用者空間等環境下也能工作。

  • 時間戳精度從微秒變為納秒

  • RX_RING, TX_RING 可用

  • 資料包可用的 VLAN 元資料資訊(`TP_STATUS_VLAN_VALID`, `TP_STATUS_VLAN_TPID_VALID`),位於 `tpacket2_hdr` 結構中

    • `tp_status` 欄位中設定 `TP_STATUS_VLAN_VALID` 位表示 `tp_vlan_tci` 欄位具有有效的 VLAN TCI 值

    • `tp_status` 欄位中設定 `TP_STATUS_VLAN_TPID_VALID` 位表示 `tp_vlan_tpid` 欄位具有有效的 VLAN TPID 值

  • 如何切換到 TPACKET_V2

    1. 將 `struct tpacket_hdr` 替換為 `struct tpacket2_hdr`

    2. 查詢頭部長度並儲存

    3. 將協議版本設定為 2,照常設定環

    4. 要獲取 `sockaddr_ll`,請使用 (void *)hdr + TPACKET_ALIGN(hdrlen) 而不是 (void *)hdr + TPACKET_ALIGN(sizeof(struct tpacket_hdr))

TPACKET_V2 --> TPACKET_V3
  • RX_RING 的靈活緩衝區實現
    1. 塊可以配置為非靜態幀大小

    2. 讀/輪詢是塊級別的(而不是資料包級別的)

    3. 添加了輪詢超時,以避免在空閒鏈路上使用者空間無限期等待

    4. 增加了使用者可配置的選項

      4.1 `block::timeout` 4.2 `tpkt_hdr::sk_rxhash`

  • 使用者空間中可用的 RX 雜湊資料

  • TX_RING 語義在概念上與 TPACKET_V2 相似;使用 `tpacket3_hdr` 而不是 `tpacket2_hdr`,使用 `TPACKET3_HDRLEN` 而不是 `TPACKET2_HDRLEN`。在當前實現中,`tpacket3_hdr` 中的 `tp_next_offset` 欄位必須設定為零,表明環不包含變長幀。`tp_next_offset` 值非零的資料包將被丟棄。

AF_PACKET 扇出模式

在 AF_PACKET 扇出模式中,資料包接收可以在程序之間進行負載均衡。這也可以與資料包套接字上的 `mmap(2)` 結合使用。

當前實現的扇出策略有:

  • `PACKET_FANOUT_HASH`: 透過 skb 的資料包雜湊排程到套接字

  • `PACKET_FANOUT_LB`: 透過輪詢排程到套接字

  • `PACKET_FANOUT_CPU`: 透過資料包到達的 CPU 排程到套接字

  • `PACKET_FANOUT_RND`: 透過隨機選擇排程到套接字

  • `PACKET_FANOUT_ROLLOVER`: 如果一個套接字已滿,則滾動到另一個

  • `PACKET_FANOUT_QM`: 透過 skb 記錄的 `queue_mapping` 排程到套接字

David S. Miller 提供的最小示例程式碼(嘗試諸如“./test eth0 hash”、“./test eth0 lb”等命令)

#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <sys/ioctl.h>

#include <unistd.h>

#include <linux/if_ether.h>
#include <linux/if_packet.h>

#include <net/if.h>

static const char *device_name;
static int fanout_type;
static int fanout_id;

#ifndef PACKET_FANOUT
# define PACKET_FANOUT                      18
# define PACKET_FANOUT_HASH         0
# define PACKET_FANOUT_LB           1
#endif

static int setup_socket(void)
{
        int err, fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_IP));
        struct sockaddr_ll ll;
        struct ifreq ifr;
        int fanout_arg;

        if (fd < 0) {
                perror("socket");
                return EXIT_FAILURE;
        }

        memset(&ifr, 0, sizeof(ifr));
        strcpy(ifr.ifr_name, device_name);
        err = ioctl(fd, SIOCGIFINDEX, &ifr);
        if (err < 0) {
                perror("SIOCGIFINDEX");
                return EXIT_FAILURE;
        }

        memset(&ll, 0, sizeof(ll));
        ll.sll_family = AF_PACKET;
        ll.sll_ifindex = ifr.ifr_ifindex;
        err = bind(fd, (struct sockaddr *) &ll, sizeof(ll));
        if (err < 0) {
                perror("bind");
                return EXIT_FAILURE;
        }

        fanout_arg = (fanout_id | (fanout_type << 16));
        err = setsockopt(fd, SOL_PACKET, PACKET_FANOUT,
                        &fanout_arg, sizeof(fanout_arg));
        if (err) {
                perror("setsockopt");
                return EXIT_FAILURE;
        }

        return fd;
}

static void fanout_thread(void)
{
        int fd = setup_socket();
        int limit = 10000;

        if (fd < 0)
                exit(fd);

        while (limit-- > 0) {
                char buf[1600];
                int err;

                err = read(fd, buf, sizeof(buf));
                if (err < 0) {
                        perror("read");
                        exit(EXIT_FAILURE);
                }
                if ((limit % 10) == 0)
                        fprintf(stdout, "(%d) \n", getpid());
        }

        fprintf(stdout, "%d: Received 10000 packets\n", getpid());

        close(fd);
        exit(0);
}

int main(int argc, char **argp)
{
        int fd, err;
        int i;

        if (argc != 3) {
                fprintf(stderr, "Usage: %s INTERFACE {hash|lb}\n", argp[0]);
                return EXIT_FAILURE;
        }

        if (!strcmp(argp[2], "hash"))
                fanout_type = PACKET_FANOUT_HASH;
        else if (!strcmp(argp[2], "lb"))
                fanout_type = PACKET_FANOUT_LB;
        else {
                fprintf(stderr, "Unknown fanout type [%s]\n", argp[2]);
                exit(EXIT_FAILURE);
        }

        device_name = argp[1];
        fanout_id = getpid() & 0xffff;

        for (i = 0; i < 4; i++) {
                pid_t pid = fork();

                switch (pid) {
                case 0:
                        fanout_thread();

                case -1:
                        perror("fork");
                        exit(EXIT_FAILURE);
                }
        }

        for (i = 0; i < 4; i++) {
                int status;

                wait(&status);
        }

        return 0;
}

AF_PACKET TPACKET_V3 示例

AF_PACKET 的 TPACKET_V3 環形緩衝區可以透過自己的記憶體管理配置為使用非靜態幀大小。它基於塊,輪詢工作在塊級別而非像 TPACKET_V2 及其前身那樣在環級別。

據說 TPACKET_V3 帶來了以下優勢:

  • CPU 使用率降低 ~15% - 20%

  • 資料包捕獲率提高 ~20%

  • 資料包密度增加約 2 倍

  • 埠聚合分析

  • 非靜態幀大小,可捕獲整個資料包有效載荷

因此,它似乎是與資料包扇出結合使用的良好選擇。

Daniel Borkmann 基於 Chetan Loke 的 lolpcap 提供的最小示例程式碼(使用 `gcc -Wall -O2 blob.c` 編譯,然後嘗試諸如“./a.out eth0”等命令)

/* Written from scratch, but kernel-to-user space API usage
* dissected from lolpcap:
*  Copyright 2011, Chetan Loke <loke.chetan@gmail.com>
*  License: GPL, version 2.0
*/

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <assert.h>
#include <net/if.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <poll.h>
#include <unistd.h>
#include <signal.h>
#include <inttypes.h>
#include <sys/socket.h>
#include <sys/mman.h>
#include <linux/if_packet.h>
#include <linux/if_ether.h>
#include <linux/ip.h>

#ifndef likely
# define likely(x)          __builtin_expect(!!(x), 1)
#endif
#ifndef unlikely
# define unlikely(x)                __builtin_expect(!!(x), 0)
#endif

struct block_desc {
        uint32_t version;
        uint32_t offset_to_priv;
        struct tpacket_hdr_v1 h1;
};

struct ring {
        struct iovec *rd;
        uint8_t *map;
        struct tpacket_req3 req;
};

static unsigned long packets_total = 0, bytes_total = 0;
static sig_atomic_t sigint = 0;

static void sighandler(int num)
{
        sigint = 1;
}

static int setup_socket(struct ring *ring, char *netdev)
{
        int err, i, fd, v = TPACKET_V3;
        struct sockaddr_ll ll;
        unsigned int blocksiz = 1 << 22, framesiz = 1 << 11;
        unsigned int blocknum = 64;

        fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
        if (fd < 0) {
                perror("socket");
                exit(1);
        }

        err = setsockopt(fd, SOL_PACKET, PACKET_VERSION, &v, sizeof(v));
        if (err < 0) {
                perror("setsockopt");
                exit(1);
        }

        memset(&ring->req, 0, sizeof(ring->req));
        ring->req.tp_block_size = blocksiz;
        ring->req.tp_frame_size = framesiz;
        ring->req.tp_block_nr = blocknum;
        ring->req.tp_frame_nr = (blocksiz * blocknum) / framesiz;
        ring->req.tp_retire_blk_tov = 60;
        ring->req.tp_feature_req_word = TP_FT_REQ_FILL_RXHASH;

        err = setsockopt(fd, SOL_PACKET, PACKET_RX_RING, &ring->req,
                        sizeof(ring->req));
        if (err < 0) {
                perror("setsockopt");
                exit(1);
        }

        ring->map = mmap(NULL, ring->req.tp_block_size * ring->req.tp_block_nr,
                        PROT_READ | PROT_WRITE, MAP_SHARED | MAP_LOCKED, fd, 0);
        if (ring->map == MAP_FAILED) {
                perror("mmap");
                exit(1);
        }

        ring->rd = malloc(ring->req.tp_block_nr * sizeof(*ring->rd));
        assert(ring->rd);
        for (i = 0; i < ring->req.tp_block_nr; ++i) {
                ring->rd[i].iov_base = ring->map + (i * ring->req.tp_block_size);
                ring->rd[i].iov_len = ring->req.tp_block_size;
        }

        memset(&ll, 0, sizeof(ll));
        ll.sll_family = PF_PACKET;
        ll.sll_protocol = htons(ETH_P_ALL);
        ll.sll_ifindex = if_nametoindex(netdev);
        ll.sll_hatype = 0;
        ll.sll_pkttype = 0;
        ll.sll_halen = 0;

        err = bind(fd, (struct sockaddr *) &ll, sizeof(ll));
        if (err < 0) {
                perror("bind");
                exit(1);
        }

        return fd;
}

static void display(struct tpacket3_hdr *ppd)
{
        struct ethhdr *eth = (struct ethhdr *) ((uint8_t *) ppd + ppd->tp_mac);
        struct iphdr *ip = (struct iphdr *) ((uint8_t *) eth + ETH_HLEN);

        if (eth->h_proto == htons(ETH_P_IP)) {
                struct sockaddr_in ss, sd;
                char sbuff[NI_MAXHOST], dbuff[NI_MAXHOST];

                memset(&ss, 0, sizeof(ss));
                ss.sin_family = PF_INET;
                ss.sin_addr.s_addr = ip->saddr;
                getnameinfo((struct sockaddr *) &ss, sizeof(ss),
                            sbuff, sizeof(sbuff), NULL, 0, NI_NUMERICHOST);

                memset(&sd, 0, sizeof(sd));
                sd.sin_family = PF_INET;
                sd.sin_addr.s_addr = ip->daddr;
                getnameinfo((struct sockaddr *) &sd, sizeof(sd),
                            dbuff, sizeof(dbuff), NULL, 0, NI_NUMERICHOST);

                printf("%s -> %s, ", sbuff, dbuff);
        }

        printf("rxhash: 0x%x\n", ppd->hv1.tp_rxhash);
}

static void walk_block(struct block_desc *pbd, const int block_num)
{
        int num_pkts = pbd->h1.num_pkts, i;
        unsigned long bytes = 0;
        struct tpacket3_hdr *ppd;

        ppd = (struct tpacket3_hdr *) ((uint8_t *) pbd +
                                    pbd->h1.offset_to_first_pkt);
        for (i = 0; i < num_pkts; ++i) {
                bytes += ppd->tp_snaplen;
                display(ppd);

                ppd = (struct tpacket3_hdr *) ((uint8_t *) ppd +
                                            ppd->tp_next_offset);
        }

        packets_total += num_pkts;
        bytes_total += bytes;
}

static void flush_block(struct block_desc *pbd)
{
        pbd->h1.block_status = TP_STATUS_KERNEL;
}

static void teardown_socket(struct ring *ring, int fd)
{
        munmap(ring->map, ring->req.tp_block_size * ring->req.tp_block_nr);
        free(ring->rd);
        close(fd);
}

int main(int argc, char **argp)
{
        int fd, err;
        socklen_t len;
        struct ring ring;
        struct pollfd pfd;
        unsigned int block_num = 0, blocks = 64;
        struct block_desc *pbd;
        struct tpacket_stats_v3 stats;

        if (argc != 2) {
                fprintf(stderr, "Usage: %s INTERFACE\n", argp[0]);
                return EXIT_FAILURE;
        }

        signal(SIGINT, sighandler);

        memset(&ring, 0, sizeof(ring));
        fd = setup_socket(&ring, argp[argc - 1]);
        assert(fd > 0);

        memset(&pfd, 0, sizeof(pfd));
        pfd.fd = fd;
        pfd.events = POLLIN | POLLERR;
        pfd.revents = 0;

        while (likely(!sigint)) {
                pbd = (struct block_desc *) ring.rd[block_num].iov_base;

                if ((pbd->h1.block_status & TP_STATUS_USER) == 0) {
                        poll(&pfd, 1, -1);
                        continue;
                }

                walk_block(pbd, block_num);
                flush_block(pbd);
                block_num = (block_num + 1) % blocks;
        }

        len = sizeof(stats);
        err = getsockopt(fd, SOL_PACKET, PACKET_STATISTICS, &stats, &len);
        if (err < 0) {
                perror("getsockopt");
                exit(1);
        }

        fflush(stdout);
        printf("\nReceived %u packets, %lu bytes, %u dropped, freeze_q_cnt: %u\n",
            stats.tp_packets, bytes_total, stats.tp_drops,
            stats.tp_freeze_q_cnt);

        teardown_socket(&ring, fd);
        return 0;
}

PACKET_QDISC_BYPASS

如果需要以類似於 `pktgen` 的方式向網路載入大量資料包,您可以在套接字建立後設置以下選項:

int one = 1;
setsockopt(fd, SOL_PACKET, PACKET_QDISC_BYPASS, &one, sizeof(one));

這會產生副作用,透過 `PF_PACKET` 傳送的資料包將繞過核心的 `qdisc` 層,直接強制推送到驅動程式。這意味著資料包不被緩衝,`tc` 規則被忽略,可能發生更多的丟包,並且此類資料包對其他 `PF_PACKET` 套接字也不再可見。因此,請注意;通常,這對於系統各種元件的壓力測試非常有用。

預設情況下,`PACKET_QDISC_BYPASS` 是停用的,需要在 `PF_PACKET` 套接字上明確啟用。

PACKET_TIMESTAMP

`PACKET_TIMESTAMP` 設定確定了 `mmap(2)` 對映的 RX_RING 和 TX_RING 中資料包元資訊中時間戳的來源。如果您的網絡卡能夠在硬體中對資料包進行時間戳記,您可以請求使用這些硬體時間戳。注意:您可能需要使用 `SIOCSHWTSTAMP` 啟用硬體時間戳的生成(參見時間戳中的相關資訊)。

`PACKET_TIMESTAMP` 接受與 `SO_TIMESTAMPING` 相同的整數位欄位

int req = SOF_TIMESTAMPING_RAW_HARDWARE;
setsockopt(fd, SOL_PACKET, PACKET_TIMESTAMP, (void *) &req, sizeof(req))

對於 `mmap(2)` 對映的環形緩衝區,此類時間戳儲存在 `tpacket{,2,3}_hdr` 結構的 `tp_sec` 和 `tp_{n,u}sec` 成員中。要確定報告了哪種時間戳,`tp_status` 欄位與以下可能的位進行二進位制或運算:

TP_STATUS_TS_RAW_HARDWARE
TP_STATUS_TS_SOFTWARE

... 它們等同於其 `SOF_TIMESTAMPING_*` 對應項。對於 RX_RING,如果兩者都沒有設定(即 `PACKET_TIMESTAMP` 未設定),那麼在 `PF_PACKET` 的處理程式碼中會呼叫軟體回退(精度較低)。

獲取 TX_RING 的時間戳工作方式如下:i) 填充環形幀,ii) 呼叫 `sendto()`,例如在阻塞模式下,iii) 等待相關幀的狀態更新或幀移交給應用程式,iv) 遍歷幀以獲取各個硬體/軟體時間戳。

只有 (!) 在啟用傳輸時間戳時,這些位才會與 `TP_STATUS_AVAILABLE` 進行二進位制或運算,因此您必須在應用程式中檢查這一點(例如,第一步檢查 `!(tp_status & (TP_STATUS_SEND_REQUEST | TP_STATUS_SENDING))` 以檢視幀是否屬於應用程式,然後第二步從 `tp_status` 中提取時間戳型別)!

如果您不關心它們,因此將其停用,那麼檢查 `TP_STATUS_AVAILABLE` 或 `TP_STATUS_WRONG_FORMAT` 就足夠了。如果在 TX_RING 部分只設置了 `TP_STATUS_AVAILABLE`,那麼 `tp_sec` 和 `tp_{n,u}sec` 成員不包含有效值。對於 TX_RING,預設情況下不生成時間戳!

有關硬體時間戳的更多資訊,請參見 `include/linux/net_tstamp.h` 和時間戳

其他細節

致謝

Jesse Brandeburg,感謝他修正了我的語法/拼寫錯誤