数据包 MMAP

摘要

本文档介绍了 PACKET 套接字接口提供的 mmap() 功能。这种类型的套接字用于

  1. 使用 tcpdump 等实用程序捕获网络流量,

  2. 传输网络流量,或任何其他需要对网络接口进行原始访问的程序。

操作指南可以在以下网址找到:

请将您的意见发送至

为什么要使用 PACKET_MMAP

非 PACKET_MMAP 捕获过程(普通的 AF_PACKET)效率非常低。它使用非常有限的缓冲区,并且需要一个系统调用来捕获每个数据包,如果想要获取数据包的时间戳(像 libpcap 始终做的那样),则需要两个系统调用。

另一方面,PACKET_MMAP 效率非常高。PACKET_MMAP 提供了一个大小可配置的循环缓冲区,该缓冲区映射到用户空间,可用于发送或接收数据包。这样,读取数据包只需要等待它们到达,大多数时候不需要发出任何系统调用。关于传输,可以通过一个系统调用发送多个数据包以获得最高的带宽。通过使用内核和用户之间的共享缓冲区,还可以最大限度地减少数据包复制。

使用 PACKET_MMAP 来提高捕获和传输过程的性能是很好的,但这并不是全部。至少,如果您以高速率(这与 CPU 速度有关)进行捕获,则应检查您的网卡设备驱动程序是否支持某种中断负载缓解或(更好的是)是否支持 NAPI,还要确保它已启用。对于传输,请检查您的网络设备使用的 MTU(最大传输单元)和支持的 MTU。您的网卡 CPU IRQ 绑定也可能是一个优势。

如何使用 mmap() 来改善捕获过程

从用户的角度来看,您应该使用更高级别的 libpcap 库,该库实际上是一个标准,几乎可以在包括 Win32 在内的所有操作系统上移植。

数据包 MMAP 支持在 1.3.0 版本左右集成到 libpcap 中;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() 的昂贵调用。在这种情况下,您还需要将 TX_RING 与 sll_protocol = 0 设置绑定 (2)。否则,可以是 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”或 2 的幂次方页数,即(对于 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

因此,get_free_pages 在 i386 架构的 2.4/2.6 内核中最多可以分配 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 分配器维护,该分配器最终负责进行分配,并因此施加 kmalloc 可以分配的最大内存。

在 2.4/2.6 内核和 i386 架构中,限制为 131072 字节。可以在 /proc/slabinfo 的“size-<bytes>”条目中检查 kmalloc 使用的预定大小。

在 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 个帧之间将存在一个间隙。这是因为一个帧不能跨越两个块。

要使用一个套接字进行捕获和传输,必须通过一次 mmap 调用来映射 RX 和 TX 缓冲区环。

...
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,则表示该帧已准备好供内核使用。如果不是 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() 来检查缓冲区是否可用

(状态 == 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 位清理,因此这也适用于具有 32 位用户空间的 64 位内核等。

  • 时间戳分辨率为纳秒而不是微秒

  • 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 <[email protected]>
*  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,修复了我的语法/拼写错误