摘抄自 https://www.xuebuyuan.com/2179173.html 学步园
PACKET_MMAP实现原理分析
2014年10月06日 ⁄ 综合 ⁄ 共 4737字 ⁄ 字号 小 中 大 ⁄ 评论关闭
PACKET_MMAP实现原理分析
samonr4l | 更新时间:2011-08-11 16:56:32 | 点击数:155自动刷新
PACKET_MMAP实现的代码都在net/packet/af_packet.c中,其中一些宏、结构等定义在include/linux/if_packet.h中。
PACKET_MMAP的实现原理
PACKET_MMAP在内核空间中分配一块内核缓冲区,然后用户空间程序调用mmap映射到用户空间。将接收到的skb拷贝到那块内核缓冲区中,这样用户空间的程序就可以直接读到捕捉的数据包了。
假如没有开启PACKET_MMAP,只是依靠AF_PACKET非常的低效。它有缓冲区的限制,并且每捕捉一个报文就需要一个系统调用,假如为了获得packet的时间戳就需要两个系统调用了(获得时间戳还需要一个系统调用,libpcap就是这样做的)。
PACKET_MMAP非常高效,它提供一个映射到用户空间的大小可配置的环形缓冲区。这种方式,读取报文只需要等待报文就可以了,大部分情况下不需要系统调用(实在poll也是一次系统调用)。通过内核空间和用户空间共享的缓冲区还可以起到减少数据拷贝的作用。
当然为了进步捕捉的性能,不仅仅只是PACKET_MMAP。假如你在捕捉一个高速网络中的数据,你应该检查NIC是否支持一些中断负载缓和机制或者是NAPI,确定开启这些措施。
PACKET_MMAP减少了系统调用,不用recvmsg就可以读取到捕捉的报文,相比原始套接字+recvfrom的方式,减少了一次拷贝和一次系统调用。
[setup]:
socket()------> 捕捉socket的创建 setsockopt()------> 环形缓冲区的分配 mmap()------> 将分配的缓冲区映射到用户空间中
[capture]
poll()------> 等待新进的报文
[shutdown]
close------> 销毁捕捉socket和所有相关的资源
接下来的这些内容,翻译自Document/networking/packet_mmap.txt,但是根据需要有所删减
假如mode设置为SOCK_RAW,链路层信息也会被捕捉;假如mode设置为SOCK_DGRAM,那么对应接口的链路层信息捕捉就不会被支持,内核会提供一个虚假的头部。
销毁socket和开释相关的资源,可以直接调用一个简单的close()系统调用就可以了。
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 */
};
这个结构被定义在include/linux/if_packet.h中,在捕捉进程中建立一个不可交换(unswappable)内存的环形缓冲区。通过被映射的内存,捕捉进程就可以无需系统调用就可以访问到捕捉的报文和报文相关的元信息,像时间戳等。
捕捉frame被划分为多个block,每个block是一块物理上连续的内存区域,有tp_block_size/tp_frame_size个frame。block的总数是tp_block_nr。实在tp_frame_nr是多余的,由于我们可以计算出来:
每个frame必须放在一个block中,每个block保存整数个frame,也就是说一个frame不能跨越两个block。
- 映射和使用环形缓冲区
在用户空间映射缓冲区可以直接使用方便的mmap()函数。固然那些buffer在内核中是由多个block组成的,但是映射后它们在用户空间中是连续的。
假如tp_frame_size能够整除tp_block_size,那么每个frame都将会是tp_frame_size长度;假如不是,那么tp_block_size/tp_frame_size个frame之间就会有空隙,那是由于一个frame不会跨越两个block。
这里我们只关心前两个,TP_STATUS_KERNEL和TP_STATUS_USER。假如status为TP_STATUS_KERNEL,表示这个frame可以被kernel使用,实际上就是可以将存放捕捉的数据存放在这个frame中;假如status为TP_STATUS_USER,表示这个frame可以被用户空间使用,实际上就是这个frame中存放的是捕捉的数据,应该读出来。
内核将所有的frame的status初始化为TP_STATUS_KERNEL,当内核接受到一个报文的时候,就选一个frame,把报文放进往,然后更新它的状态为TP_STATUS_USER(这里假设不出现其他题目,也就是忽略其他的状态)。用户程序读取报文,一旦报文被读取,用户必须将frame对应的status设置为0,也就是设置为TP_STATUS_KERNEL,这样内核就可以再次使用这个frame了。
先检查状态值,然后再对frame进行轮循,这样就可以避免竞争条件了(假如status已经是TP_STATUS_USER了,也就是说在调用poll前已经有了一个报文到达。这个时候再调用poll,并且之后不再有新报文到达的话,那么之前的那个报文就无法读取了,这就是所谓的竞争条件)。
在libpcap-1.0.0中是这么设计的:
pcap-linux.c中的pcap_read_linux_mmap:
//假如frame的状态在poll前已经为TP_STATUS_USER了,说明已经在poll前已经有一个数据包被捕捉了,假如poll后不再有数据包被捕捉,那么这个报文不会被处理,这就是所谓的竞争情况。
if ((handle->md.timeout >= 0) && !pcap_get_ring_frame(handle, TP_STATUS_USER)) { struct pollfd pollinfo; int ret; pollinfo.fd = handle->fd; pollinfo.events = POLLIN; do { /* poll() requires a negative timeout to wait forever */ ret = poll(&pollinfo, 1, (handle->md.timeout
0)? handle->md.timeout: -1); if ((ret < 0) && (errno != EINTR)) { return -1; } ...... } while (ret < 0); }
//依次处理捕捉的报文
while ((pkts < max_packets) || (max_packets <= 0)) { ...... //假如frame的状态为TP_STA