在前面文章《LTE模组可以被VPP直接接管喽!!!》中介绍使用af-packet插件将linux 内核接口映射到vpp中,并通过vpp dhcp client插件实现lte拨号上网的功能,本文主要介绍af packet实现机制,对阅读代码有所帮助。
Linux中的AF_PACKET套接字允许应用程序接收和发送原始数据包。这个特定于linux应用程序绑定到AF_PACKET套接字,并允许DPDK或VPP应用程序通过内核发送和接收原始数据包。为了提高Rx和Tx性能,该实现使用PACKET_MMAP机制,它提供了一个在用户空间和内核之间共享的环形缓冲区,用于发送和接收数据包。这样有助于减少用户空间和内核之间系统调用内存拷贝。
PACKET MMAP通过提供映射到用户空间的大小可配置的环形缓冲区接收数据包和发送数据包分为两个环形缓冲区,两个缓冲区可关联同一个socket(这种情况下,发送缓冲区的起始地址必须紧跟在接收缓冲区的结束地址之后)。
下面是内核packet_mmap文档中关于PACKET_MMAP设置的一些描述:
用户态应用程序可以通过下面系统调用代码来设置PACKET_MMAP接收和发送缓冲区的参数:
代码语言:javascript复制- Capture process #发送缓存区
setsockopt(fd, SOL_PACKET, PACKET_RX_RING, (void *) &req, sizeof(req))
- Transmission process #接收缓冲区
setsockopt(fd, SOL_PACKET, PACKET_TX_RING, (void *) &req, sizeof(req))
上面调用中最重要的参数是req形参,该参数必须具有如下结构:
代码语言:javascript复制struct tpacket_req
{
Unsigned int tp_block_size;/*连续块的最小大小*/
Unsigned int tp_block_nr;/*数据块数量*/
Unsigned int tp_frame_size;/*帧的大小*/
Unsigned int tp_frame_nr;/*总帧数*/
};
这个结构被定义在include/linux/if_packet.h中,在捕获进程中mmap一个不可交换(unswappable)内存的环形缓冲区。通过被映射的内存,捕获进程就可以无需系统调用就可以访问到捕获的报文和报文相关的元信息,像时间戳等。
环形缓冲区分为多个block,每个block是一块物理上连续的内存区域,按照页面大小对齐,即必须是页面大小的整数倍。每个frame必须放在一个block中,每个block保存整数个frame。每个block可以存放tp_block_size/tp_frame_size个数据frame。block的总数是tp_block_nr。其实tp_frame_nr是多余的,因为我们可以计算出来:
代码语言:javascript复制frames_per_block = tp_block_size/tp_frame_size
实际上,packet_set_ring检查下面的条件是否正确:
代码语言:javascript复制frames_per_block * tp_block_nr == tp_frame_nr
代码语言:javascript复制下面我们可以一个例子:
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 |
--------- --------- --------- ---------
代码语言:javascript复制
代码语言:javascript复制每个frame必须放在一个block中,每个block保存整数个frame,也就是说一个frame不能跨越两个block。VPP默认使用V3版本。
代码语言:javascript复制下面是TPACKET_v3相关数据结构:
代码语言:javascript复制/* 创建TPACKET_V3环形缓冲区时对应的配置参数结构
* 备注:tpacket_req3结构是tpacket_req结构的超集,实际可以统一使用本结构去设置所有版本的环形缓冲区,V1/V2版本会自动忽略多余的字段
*/
struct tpacket_req3 {
unsigned int tp_block_size; // 每个连续内存块的最小尺寸(必须是 PAGE_SIZE * 2^n )
unsigned int tp_block_nr; // 内存块数量
unsigned int tp_frame_size; // 每个帧的大小(虽然V3中的帧长是可变的,但创建时还是会传入一个最大的允许值)
unsigned int tp_frame_nr; // 帧的总个数(必须等于 每个内存块中的帧数量*内存块数量)
unsigned int tp_retire_blk_tov; // 内存块的寿命(ms),超时后即使内存块没有被数据填入也会被内核停用,0意味着不设超时
unsigned int tp_sizeof_priv; // 每个内存块中私有空间大小,0意味着不设私有空间
unsigned int tp_feature_req_word;// 标志位集合(目前就支持1个标志 TP_FT_REQ_FILL_RXHASH)
}
// 开启PACKET_MMAP时libpcap对V1、V2、V3版本环形缓冲区的统一管理结构
union thdr {
struct tpacket_hdr *h1;
struct tpacket2_hdr *h2;
struct tpacket_block_desc *h3;
void *raw;
}
// TPACKET_V3环形缓冲区每个帧的头部结构
struct tpacket3_hdr {
__u32 tp_next_offset; // 指向同一个内存块中的下一个帧
__u32 tp_sec; // 时间戳(s)
__u32 tp_nsec; // 时间戳(ns)
__u32 tp_snaplen; // 捕获到的帧实际长度
__u32 tp_len; // 帧的理论长度
__u32 tp_status; // 帧的状态
__u16 tp_mac; // 以太网MAC字段距离帧头的偏移量
__u16 tp_net;
union {
struct tpacket_hdr_variant1 hv1; // 包含vlan信息的子结构
};
__u8 tp_padding[8];
}
接收数据包:内核收到数据包后将其存入接收环形缓冲区中,poll( )轮询到有数据包后,用户层根据frame的状态(TP_STATUS_USER)判断数据包能否处理。若进行处理,则将对应状态由TP_STATUS_USER改为TP_STATUS_KERNEL告诉内核这块缓冲区对应的数据包已经被处理,可以继续存放新的数据包;
发送数据包:用户产生需要发送的数据包后,从发送环形缓冲区遍历寻找一个可用状态(TP_STATUS_AVAILABLE)的frame将数据包存入后,状态置为TP_STATUS_SEND_REQUEST。通过poll( )轮询发送缓冲区,当有需要发送的数据包时,通过sendto( )函数提醒内核从缓冲区进行发送。
默认情况不设置时,头结构体对应struct tpacket_hdr ,其中tp_mac可以看出就是对应到链路层的偏移量,tp_net偏移到除去链路层的数据后的偏移量。而tp_len的长度跟SOCK_RAW,SOCK_DGRAM 模式有关。SOCK_RAW 对应从上图tp_mac开始到最后整个数据包,SOCK_DGRAM 则对应tp_net到最后数据包。