打印

Linux kernel的网络编程之:packet_mmap

[复制链接]
408|16
手机看帖
扫描二维码
随时随地手机跟帖
沙发
keer_zu|  楼主 | 2023-9-19 18:00 | 只看该作者
Packet MMAP
摘要

这个文件记录了数据包套接字接口可用的mmap()功能。这种类型的套接字用于
  • 使用tcpdump等实用程序捕获网络流量,
  • 传输网络流量,或任何其他需要原始访问网络接口的流量。


详情请浏览:

https://sites.google.com/site/packetmmap/

请将您的意见发送至




使用特权

评论回复
板凳
keer_zu|  楼主 | 2023-9-19 18:01 | 只看该作者
为什么使用PACKET_MMAP

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

另一方面,PACKET_MMAP非常高效。PACKET_MMAP提供了一个大小可配置的圆形缓冲区,它映射在用户空间中,可用于发送或接收数据包。这种方式读取数据包只需要等待它们,大多数时候不需要发出单个系统调用。在传输方面,可以通过一个系统调用发送多个数据包,以获得最高的带宽。通过在内核和用户之间使用共享缓冲区,还可以最大限度地减少数据包副本。

使用PACKET_MMAP可以提高捕获和传输过程的性能,但这并不是全部。至少,如果您正在高速捕获(相对于cpu速度而言),您应该检查您的网络接口卡的设备驱动程序是否支持某种中断负载缓解,或者(甚至更好)如果它支持NAPI,也要确保启用它。如果要传输,请检查网络中设备使用和支持的最大传输单元MTU (Maximum transmission Unit)。CPU IRQ固定您的网络接口卡也可以是一个优势。

使用特权

评论回复
地板
keer_zu|  楼主 | 2023-9-19 18:07 | 只看该作者
如何使用mmap()来改善捕获过程

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

大约在1.3.0版本时,包MMAP支持被集成到libpcap中;1.5.0版本中增加了对TPACKET_V3的支持

使用特权

评论回复
5
keer_zu|  楼主 | 2023-9-19 18:17 | 只看该作者
如何使用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));
对于可以捕获链路级信息的原始接口,模式为SOCK_RAW;对于不支持捕获链路级信息的熟接口,模式为SOCK_DGRAM,并且内核提供了链路级伪头。
套接字和所有相关资源的销毁是通过调用close(fd)来完成的。
与不使用PACKET_MMAP类似,可以使用一个套接字进行捕获和传输。这可以通过使用单个mmap()调用映射分配的RX和TX缓冲区环来完成。参见“圆形缓冲区(环)的映射和使用”。
接下来,我将描述PACKET_MMAP设置及其约束,以及用户进程中循环缓冲区的映射和该缓冲区的使用。



使用特权

评论回复
6
keer_zu|  楼主 | 2023-9-19 18:21 | 只看该作者
本帖最后由 keer_zu 于 2023-9-19 18:23 编辑

如何使用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。否则,例如,hons (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://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选项启用它。

使用特权

评论回复
7
keer_zu|  楼主 | 2023-9-19 18:27 | 只看该作者
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时必须考虑一些细节。参见“圆形缓冲区(环)的映射和使用”。

使用特权

评论回复
8
keer_zu|  楼主 | 2023-9-20 14:14 | 只看该作者
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_ORDER宏决定。更精确地说,极限可以计算为:


PAGE_SIZE << MAX_ORDER

In a i386 architecture PAGE_SIZE is 4096 bytes
In a 2.4/i386 kernel MAX_ORDER is 10
In a 2.6/i386 kernel MAX_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_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


使用特权

评论回复
9
keer_zu|  楼主 | 2023-9-20 14:20 | 只看该作者
PACKET_MMAP缓冲区大小计算器


定义:


从这些定义我们可以推导出:

<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的幂,否则会浪费内存。


使用特权

评论回复
10
keer_zu|  楼主 | 2023-9-20 14:24 | 只看该作者
环形缓冲区(环)的映射和使用
用户进程中缓冲区的映射是用常规的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,则表示该帧已准备好供内核使用,如果不是,则存在用户可以读取的帧,并应用以下标志:


捕获过程
从包括/ 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)

为方便起见,还有以下定义:
#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);
首先检查状态值,然后轮询帧不会导致竞争条件。

使用特权

评论回复
11
keer_zu|  楼主 | 2023-9-20 14:26 | 只看该作者
传输的过程
这些定义也用于传输:
#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);



使用特权

评论回复
12
keer_zu|  楼主 | 2023-9-20 14:37 | 只看该作者
本帖最后由 keer_zu 于 2023-9-20 14:39 编辑

有哪些可用的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. 用结构体tpacket2_hdr代替结构体tpacket_hdr
2. 查询标题len并保存
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值不为零的数据包将被丢弃。


使用特权

评论回复
13
keer_zu|  楼主 | 2023-9-20 14:41 | 只看该作者
AF_PACKET fanout模式


在AF_PACKET fanout模式下,报文接收可以在进程之间进行负载均衡。这也可以与包套接字上的mmap(2)结合使用。
目前实施的fanout策略有:
  • PACKET_FANOUT_HASH:根据skb的数据包哈希值调度到socket
  • PACKET_FANOUT_LB:轮询调度到套接字
  • PACKET_FANOUT_CPU:根据到达的CPU数据包调度socket
  • PACKET_FANOUT_RND:随机选择调度到套接字
  • PACKET_FANOUT_ROLLOVER:如果一个套接字已满,则切换到另一个套接字
  • PACKET_FANOUT_QM:按记录队列映射的skbs调度到套接字


由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;
}


使用特权

评论回复
14
keer_zu|  楼主 | 2023-9-20 14:44 | 只看该作者
AF_PACKET TPACKET_V3示例
AF_PACKET的TPACKET_V3环缓冲区可以通过自己的内存管理配置为使用非静态帧大小。它基于块,其中轮询以每个块为基础,而不是像TPACKET_V2及其前身那样以每个环为基础。
据说TPACKET_V3带来以下好处:

  • 减少15% - 20%的cpu使用
  • 数据包捕获率提高~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;
}


使用特权

评论回复
15
keer_zu|  楼主 | 2023-9-20 14:45 | 只看该作者
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套接字上显式启用。


使用特权

评论回复
16
keer_zu|  楼主 | 2023-9-20 14:49 | 只看该作者
PACKET_TIMESTAMP
PACKET_TIMESTAMP设置决定了mmap(2)、RX_RING和TX_RINGs的数据包元信息中时间戳的来源。如果您的NIC能够在硬件中对数据包进行时间戳,则可以请求使用这些硬件时间戳。注意:您可能需要使用SIOCSHWTSTAMP启用硬件时间戳的生成(请参阅Timestamping中的相关信息)。
PACKET_TIMESTAMP接受与SO_TIMESTAMPING相同的整数位字段:
int req = SOF_TIMESTAMPING_RAW_HARDWARE;
setsockopt(fd, SOL_PACKET, PACKET_TIMESTAMP, (void *) &req, sizeof(req))
对于mmap(2)ed环缓冲区,这样的时间戳存储在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)遍历帧以拾取单个hw/sw时间戳。
只有(!)如果启用了传输时间戳,那么这些位将与二进制|与TP_STATUS_AVAILABLE组合在一起,所以你必须在你的应用程序中检查(例如!(tp_status & (TP_STATUS_SEND_REQUEST | TP_STATUS_SENDING))在第一步中查看帧是否属于应用程序,然后可以在第二步中从tp_status提取时间戳的类型)!
如果您不关心它们,那么将其禁用,检查TP_STATUS_AVAILABLE resp。TP_STATUS_WRONG_FORMAT就足够了。如果在TX_RING部分中只设置了TP_STATUS_AVAILABLE,则tp_sec和tp_{n,u}sec成员不包含有效值。对于TX_RINGs,默认情况下不生成时间戳!
有关硬件时间戳的更多信息,请参见include/linux/net_tstamp.h和Timestamping。

使用特权

评论回复
17
keer_zu|  楼主 | 2023-9-20 14:50 | 只看该作者
杂项部分
包套接字可以很好地与Linux套接字过滤器一起工作,因此您可能还想了解Linux套接字过滤,即伯克利包过滤(BPF)。
谢谢
杰西·勃兰登堡,谢谢你帮我纠正了语法和拼写错误

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

1314

主题

12271

帖子

53

粉丝