数据包 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,感谢他修正了我的语法/拼写错误