您的位置:首页 > 理论基础 > 计算机网络

高性能网络数据捕获:netsniff-ng源码分析

2012-03-10 11:51 656 查看

作者:Melo Chale 转载请注明出处!新浪微博:@melochale 腾讯微博:@melochale

1.简介

随着Internet的高速发展,网络传输的数据量越来越大,各种网络数据的类型也错综复杂,特别是在主干网络上尤为更甚。运营商在带宽增加的同时没有正比的增加相应的收益,所以他们需要对各种网络数据进行分析之后,在网关或者服务器上进行优化后再传输,提高带宽利用率。要对数据进行分析,首先要对网络数据进行捕获。现今对数据捕获的软件或者库也很多,比如linux平台的libpcap库,而本设计主要是非基于libpcap平台的开源软件netsniff-ng设计的,它使用了零拷贝技术高效的捕获网络数据,而根据需求,在此只讨论网络数据的接收部分,不考虑发送部分,其实基本原理是一样的。

2.netsniff-ng介绍


netsniff-ng是一个高性能的网络嗅探器,支持数据的捕获,重放,过滤等,是一个linux平台系统级的应用程序,说它是系统级的,是因为自己分析并构造协议栈来处理网络的原始数据。它的高效来源于它的零拷贝技术,通过这个技术,应用程序不再需要从内核空间拷贝数据到用户空间。具体信息可以查看它的官网。

3.高性能数据捕获的思想

网络数据接收流程原理(驱动层描述)

网络设备的核心处理模块是一个被称作DMA(DirectMemory
Access)的控制器,DMA模块能够协助处理器处理数据收发。对于数据发送来说,它能够将组织好的数据自动发出,无需处理器干预;对于数据接收来说,它能够将收到的数据以一定的格式组织起来,通知处理器,并等待处理器来取。
DMA模块收发数据的单元被称为BD(BufferDescription,缓存描述符,实际的操作系统中,比如Linux,BD表就是sk_buf数据结构)每个包都会被分成若干个帧,而每个帧则被保存在一个BD中。BD结构通常包含有以下字段:
typedef struct {
void *bufptr;    /* 保存当前 BD 对应缓存的起始地址  */
int length;      /* 保存缓存中存储的数据包长度      */
int sc;           /* 保存当前 BD 的状态信息          */
} BD_STRUCT;

所有的BD就组成了一张BD表,如图1所示,是接收数据全景原理图。



图1数据接收原理图
图中各步骤的具体含义描述如下:
(1) 处理器初始化BD表;
(2) 处理器初始化网络设备;
(3) MAC模块从网络中接收数据;
(4) MAC模块通知DMA模块来取数据;
(5) DMA模块从BD表中取出合适的BD;
(6) MAC模块将数据发送至当前BD对应的缓存内(主要是sk_buf指向的缓存内);
(7) 网络设备通知处理器开始接收数据(以中断方式或轮询方式);
(8) 协议层从当前的BD缓存内取走数据。
其中步骤(3) ~ (6)是硬件自动完成的,不需要软件的干预,如此可以节省处理器的工作量。
当网络设备接收到数据时,DMA模块会自动将数据保存起来并通知处理器来取,处理器通过中断或者轮询方式发现有数据接收进来后,再将数据保存到sk_buff缓冲区中,并通过socket接口读出来。在内存映射中,sk_buf缓冲区被直接映射到用户空间,不需要应用程序调用read()来做拷贝。流程图如图2所示。



图2

a. 网络设备接收到数据后,DMA 模块搜索接收 BD 表,取出空闲的 BD,并将数据自动保存到该 BD 的缓存中,修改 BD 为就绪态,并同时触发中断(该步骤可选)。
b. 处理器可以通过中断或者轮询的方式检查接收 BD 表的状态,无论采用哪种方式,它们都需要实现以下步骤。
(1) 从接收 BD 表中取出一个空闲的 BD。

(2) 如果当前 BD 为就绪态,检查当前 BD 的数据状态,更新数据接收统计。

(3) 从 BD 中取出数据保存在 sk_buff 的缓冲区中。

(4) 更新 BD 的状态为空闲态。

(5) 移动接收 BD 表的指针指向下一个 BD。

c. 用户调用 read 之类的读函数,从 sk_buff 缓冲区中读出数据,同时释放该缓冲区。

4.主要数据结构

mode数据结构

mode保存用户操作的功能结构,程序运行时根据用户的输入进行初始化操作初始化,根据程序的帮助文档可以大体知道每个变量的含义,见注释。

struct mode {
char *device_in;	//输入的网络接口或者读入的数据包名,如eth0或者xxx.pcap
char *device_out;	//对应device_in,为输出
char *filter;	//是否启用过滤选项,如只显示ICMP包
int cpu;	//指定绑定的CPU
int dump;	//选择终端输出或是文件输出
/* dissector */
int link_type;	//
int print_mode;	//打印类型
/* 0 for automatic, > 0 for manual */
unsigned int reserve_size;
int packet_type;
bool randomize;		//指定为随机模式
bool promiscuous;	//网口为混杂模式,接收所有网卡包
enum pcap_ops_groups pcap;	//包类型
unsigned long kpull;
int jumbo_support;	//是否指定大容量帧数
};


ring数据结构

最重要的数据结构莫过于ring,难点也在此处,ring用于接收到的网络数据包的内存信息,它是mmap()函数从内核缓冲区映射到用户空间,然后由用户空间程序直接操作数据,减少了内核缓冲区到socket缓冲去再到用户缓冲区的两次数据拷贝(个人理解,可能存在偏颇),包括内存大小及内存分配,这个数据结构里面包含了零拷贝思想以及操作内存的核心思想;

struct ring {
struct iovec *frames;	//数据帧,不同于网络术语的“帧”
struct tpacket_req layout;	//frames和block的块数页数统计
struct sockaddr_ll s_ll;	//常见的socket数据结构
uint8_t *mm_space;	//映射的内存空间的起始地址
size_t mm_len;	//所有block的字节长度

} __cacheline_aligned;	//字节对齐属性

变量frames的首个void*指针被mmap()函数初始化,赋值为其返回值,表示一个数据帧信息。

tpacket_hdr数据结构

包括了tpacket_hdr数据结构,下面会详细解析这个结构。

struct frame_map {	//每个frame的头部信息
struct tpacket_hdr tp_h __attribute__((aligned(TPACKET_ALIGNMENT)));
struct sockaddr_ll s_ll __attribute__((aligned(TPACKET_ALIGNMENT)));
};

数据包头部信息的数据结构tpaket_hdr如下:

struct tpacket_hdr {
unsigned long	tp_status; //帧状态
unsigned int	tp_len;		//帧的总长度
unsigned int	tp_snaplen;
unsigned short	tp_mac;		//Ethernet_hdr相对于tpacket_hdr的偏移量
unsigned short	tp_net;
unsigned int	tp_sec;		//格林尼治时间的秒数
unsigned int	tp_usec;	//格林尼治时间的微秒数,和tp_sec组合成时间戳
};

数据会根据每个帧状态的值tp_status检测这个帧是否是可以用,下面是它的一些取值和含义:

#define TP_STATUS_KERNEL        0 // Frame is available
#define TP_STATUS_USER          1 // Frame will be sent on next send()
#define TP_STATUS_COPY          2 // Frame is currently in transmission
#define TP_STATUS_LOSING        4 // Indicate a transmission error

由mmap()函数返回的地址加上tp_mac,即可以获得一个以太网头部的首地址,而这个首地址可以强制转化成下面以太网头部的数据结构,它包含在linux/if_ether.h中,也可以自己定义:

struct ethhdr {
unsigned char	h_dest[ETH_ALEN];	/* destination eth addr	*/
unsigned char	h_source[ETH_ALEN];	/* source ether addr	*/
__be16		h_proto;		/* packet type ID field	*/
} __attribute__((packed));

其中h_dest和h_source是mac的目的地址和源地址,h_proto是下一个头部数据结构的类型,这个类型可以是arp、rarp、ipv4、ipv6等,根据RFC文件的定义值可以获知,下面是RFC定义的值,据字面意思可以理解其值。

#define ARP 	0x0806
#define RARP 	0x8035
#define IPV4	0x0800
#define IPV6	0x86DD
#define VLAN	0x8100


protocol数据结构

程序一开始构造自己的哈希函数和协议数据结构,通过该值查询哈希表获取下一个头部的协议类型,所以下面就是协议类型的数据结构:

struct protocol {
unsigned int key;
size_t offset;
void (*print_full)(uint8_t *packet, size_t len);
void (*print_less)(uint8_t *packet, size_t len);
void (*print_pay_ascii)(uint8_t *packet, size_t len);
void (*print_pay_hex)(uint8_t *packet, size_t len);
void (*print_pay_none)(uint8_t *packet, size_t len);
void (*print_all_cstyle)(uint8_t *packet, size_t len);
void (*print_all_hex)(uint8_t *packet, size_t len);
struct protocol *next;
void (*process)(uint8_t *packet, size_t len);
void (*proto_next)(uint8_t *packet, size_t len,
struct hash_table **table,
unsigned int *key, size_t *off);
};

其中key即为改协议首部的类型定义值,另外还有下面还有几个。Offset字段被设置成每个协议层的头部长度,以便每次读取完头部数据后,增加这个偏移量,获取下一层数据;proto_next函数指针获得下一层的key,off,和哈希表;其他是一些打印函数。

#define TCP			0x06
#define UDP			0x11
#define ICMP		        0x01



5.数据捕获

内存分配

分配内存大小,内存分配模式如下图所示。



图3
根据用户自定义或者初始化自动分配,分配的大小和数量已经在tpacket_req数据结构中。如上图所示,分配很多个block,每个block包含若干个frame,block一定是frame的整数倍,根据分配的大小和数量初始化每个指向frame的指针,下面aloc_rx_ring_frames()函数既是初始化每个指向frame的指针。
void alloc_rx_ring_frames(struct ring *ring)
{
int i;
size_t len = ring->layout.tp_frame_nr * sizeof(*ring->frames);
ring->frames = xmalloc_aligned(len, 64);
memset(ring->frames, 0, len);
for (i = 0; i < ring->layout.tp_frame_nr; ++i) {
ring->frames[i].iov_len = ring->layout.tp_frame_size;
ring->frames[i].iov_base = ring->mm_space +
(i * ring->layout.tp_frame_size);
}
}

创建环形缓冲区
当创建好一个原始的socket后,通过传递tpacket_req结构给通用套接字选项函数setsockopt()设置为一个环形的缓冲区,选项字段一定要是PACKET_RX_RING。
void create_rx_ring(int sock, struct ring *ring)
{
……
ret = setsockopt(sock, SOL_PACKET, PACKET_RX_RING, &ring->layout,
sizeof(ring->layout));
……
}

映射内存到用户空间

环形缓冲区设置,即图1的最后一个block指向头一个block。然后根据下面mmap()函数映射它的首地址到用户空间,这样的好处是防止再做多余的拷贝操作,降低数据捕获的性能。

void mmap_rx_ring(int sock, struct ring *ring)
{
……
ring->mm_space = mmap(0, ring->mm_len, PROT_READ | PROT_WRITE,
MAP_SHARED, sock, 0);
……
}

操作数据

操作数据的函数是下面dissector_main()函数,参数是packet是ring->mm_space +hdr->tp_mac的地址,len是这个frame的长度,start是表示开始的protocol,一般是一个以太网ethhdr数据结构,end表示此地址开始是真正的数据,不再是各层协议的头部数据。各操作已做注释。

static void dissector_main(uint8_t *packet, size_t len,
struct protocol *start, struct protocol *end)
{
……
while (proto != NULL) {	//循环读取各层协议首部
len -= off;		//每个frame的长度读取各协议首部后减去该首部,表示剩余数据
packet += off;	//与len相反,表示每个frame的偏移量
if (unlikely(!proto->process))
break;
off = proto->offset;	//off是每个首部的数据大小
if (!off)
off = len;
proto->process(packet, off);
if (unlikely(!proto->proto_next))
break;

/*获取下一个首部的key类型,table哈希表,以及它的偏移量off*/
proto->proto_next(packet, len, &table, &key, &off);
if (unlikely(!table))
break;
proto = lookup_hash(key, table); //根据上面获得的key和table获取该协议
while (proto && key != proto->key)	//循环到下一层协议,若是数据,跳出
proto = proto->next;
}
len -= off;
packet += off;
if (end != NULL)
if (likely(end->process))
end->process(packet, len);		//打印出数据
tprintf_flush();
}

下图是一个tcp的数据段frame具体细节构图。



图4
整个数据是一个frame,该frame等于ring->frames->iovec[x],是图4分配的内存中一个block的一个frame放大图。mm_space是内存映射函数mmap()获得,该指针操作的是原始的sk_buf保存的数据。

清理工作

最后执行清理工作,释放所有分配的内存。下面函数是释放哈希表。

void dissector_cleanup_ethernet(void)
{
free_hash(ð_lay2);
free_hash(ð_lay3);
free_hash(ð_lay4);
……
}

重设套接字选项,取消映射munmap(),释放所有的frame。



































































                                            
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: