一、设备轮询机制的基本思想
所谓的设备轮询机制实际上就是利用网卡驱动程序提供的NAPI机制加快网卡处理数据包的速度,因为在大流量的网络环境当中,标准的网卡中断加上逐层的数据拷贝和系统调用会占用大量的CPU资源,而真正用于处理这些数据的资源却很少。
一个基本的想法是对于大流量网络,如果发现一个DMA传输中断(这表明一个网络数据通过DMA通道到达了DMA缓冲区),则首先关闭网卡的中断模式,而对于随后的数据全部采用轮询方式进行接收,这样大大降低了网卡的中断次数,如果轮询发现没有数据包可收或者已经接收了一定数量的数据包,则打开网卡的中断模式,依次类推。
这种方法被证明在某种情况下能够大大提高网络处理能力,但是在某种情况下会降低处理能力,因此并不是普遍适用的好的处理方法。另外,如果内核网络层数据包的处理方式仍然采用标准的方法(逐层拷贝并且产生一次系统调用,libpcap库就是采用这种标准方式),那么总体效率还是很低。必须考虑采用其它的优化措施降低网络层传输的内存拷贝次数及避免频繁的系统调用。
为了达到上述的目标,提出了基于PF_RING套接字的设备轮询机制,另外还可以采用内核补丁RTIRQ,即实时中断机制。
二、PF_RING套接字的实现
PF_RING套接字是作者为了减少网络层传输中的内存拷贝即避免频繁的系统调用而设计的一种新的套接字类型,这种套接字采用模块方式动态加载。
为了能够使得内核支持这种新的套接字类型,必须使用特定的内核补丁,该补丁增加了两个文件ring.h即ring_packet.c,分别定义了使用ring套接字的各种数据结构以及ring套接字的定义及处理函数。
该模块采用模块方式加载,模块初始化函数ring_init()在ring_packet.c中定义:
static int __init ring_init(void)
{
ring_table = NULL;
sock_register(&ring_family_ops);
set_ring_handler(my_ring_handler);
return 0;
}
该函数调用sock_register()函数将PF_RING套接字协议族(Linux自身提供多种套接字协议族,比如INET,UNIX域套接字,APPLETALK,X.25等,每一种套接字协议族都由一个net_proto_family结构描述,该结构的关键成员是协议族序号以及create()方法,用来创建一个此种类型的套接字)注册到系统的全局套接字协议族数组net_family中,以便用户层调用sock()函数创建PF_RING套接字时,系统能够从net_family数组中找到相应的记录和create()方法创建这种套接字。
PF_RING套接字的create()方法注册为ring_create()函数:
static int ring_create(struct socket *sock, int protocol) {
struct sock *sk;
struct ring_opt *pfr;
int err;
/*如果想创建PF_RING类型的套接字,必须拥有ROOT权限,并且套接字类型是SOCK_RAW,并且必须接收所有的以太网数据类型*/
if(!capable(CAP_NET_ADMIN))
return -EPERM;
if(sock->type != SOCK_RAW)
return -ESOCKTNOSUPPORT;
if(protocol != htons(ETH_P_ALL))
return -EPROTONOSUPPORT;
err = -ENOMEM;
/*分配一个BSD套接字,并且将套接字家族类型赋值为PF_RING*/
sk = sk_alloc(PF_RING, GFP_KERNEL, 1
#if (LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,0))
, NULL
#endif
);
if (sk == NULL)
goto out;
/*套接字操作符集合,这个集合定义了套接字的各种操作函数,包括connect,bind,mmap,poll等,实际的使用当中就是调用这些函数完成套接字的各种操作的*/
sock->ops = &ring_ops;
sock_init_data(sock, sk);
#if (LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,0))
sk_set_owner(sk, THIS_MODULE);
#endif
err = -ENOMEM;
/*这里利用sock结构中提供的一个协议私有数据sk_protinfo,这段数据用
struct ring_opt {
struct net_device *ring_netdev;
/* 这里的order变量指的是2的阶乘,这是内存物理页面的数量,如果order为3的话,那么就是2的三次方个物理页面,而对于Linux系统而言,一个物理页面就是4K大小个字节。 */
unsigned long order;
/* 环形缓冲区的插槽,ring_memory为内存的起始地址,用来进行内存映射的(mmap),这段内存的前面第一个物理页面是环形插槽结构FlowSlotInfo,紧接着跟着的就是用来存放数据的一个个插槽结构 */
unsigned long ring_memory;
FlowSlotInfo *slots_info; /* Basically it points to ring_memory */
char *ring_slots; //Basically it points to ring_memory+sizeof(FlowSlotInfo)
/* 数据包抽样 */
u_int pktToSample, sample_rate;
/* BPF的过滤器,用来过滤掉不想抓的数据包,提高网络处理速度 */
struct sk_filter *bpfFilter;
/* struct sock_fprog fprog; */
/* 环形缓冲区锁 */
atomic_t num_ring_slots_waiters;
wait_queue_head_t ring_slots_waitqueue;
rwlock_t ring_index_lock;
/* Indexes (Internal) */
u_int insert_page_id, insert_slot_id;
}*/
pfr = ring_sk(sk) = kmalloc(sizeof(*pfr), GFP_KERNEL);
if (!pfr) {
sk_free(sk);
goto out;
}
memset(pfr, 0, sizeof(*pfr));
init_waitqueue_head(&pfr->ring_slots_waitqueue);
pfr->ring_index_lock = RW_LOCK_UNLOCKED;
atomic_set(&pfr->num_ring_slots_waiters, 0);
#if (LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,0))
sk->sk_family = PF_RING;
sk->sk_destruct = ring_sock_destruct;
#else
sk->family = PF_RING;
sk->destruct = ring_sock_destruct;
sk->num = protocol;
#endif
/*将分配的套接字插入到自己维护的套接字列表当中,这是一个单向列表。*/
ring_insert(sk);
return(0);
out:
#if (LINUX_VERSION_CODE < KERNEL_VERSION(2,6,0))
MOD_DEC_USE_COUNT;
#endif
return err;
}
三、RING套接字的使用
如果需要使用RING套接字,则需要在用户层调用sock()函数,并且传递PF_RING标志、SOCK_RAW以及ETH_IP_ALL三个参数,这三个参数必须完全匹配。sock()函数执行成功则将PF_RING套接字描述符返回给用户,用户就可以利用这个描述符操作相应的设备轮询机制了。
接下来的一个重要操作是调用bind函数,以前的文档已经分析过了bind()函数调用,切入内核以后调用的sys_bind()函数,最终实际调用的是相应套接字协议族自己定义的bind方法,而对于PF_RING套接字协议族来说,就是调用ring_bind()函数,事实上,ring_bind()函数最终调用packet_ring_bind()函数完成bind的工作。这个函数的作用就是为套接字描述符创建一个环形共享缓冲区,然后绑定到一个设备上。
static int packet_ring_bind(struct sock *sk, struct net_device *dev){
u_int the_slot_len;
u_int32_t tot_mem;
struct ring_opt *pfr = ring_sk(sk);
struct page *page, *page_end;
if(!dev) return(-1);
**************************************
* *
* FlowSlotInfo *
* *
************************************* <-+
* FlowSlot * |
************************************* |
* FlowSlot * |
************************************* +- num_slots
* FlowSlot * |
************************************* |
* FlowSlot * |
************************************* <-+
the_slot_len = sizeof(u_char) /* flowSlot.slot_state */
+ sizeof(u_short) /* flowSlot.slot_len */
+ bucket_len /* flowSlot.bucket */;
tot_mem = sizeof(FlowSlotInfo) + num_slots*the_slot_len;
/*
根据上面计算得到的内存使用的总量,判断需要想内核申请多少个物理页面,因为在内核当中申请物理页面是以2的order次方计算的,如果希望申请8个页面,那么order就是3,而调用申请物理页面的函数时,传递的就是order参数而不是实际的页面数量。
而且申请物理页面的操作也不总是能够成功,因此,需要不断的尝试申请物理页面,直到申请到足够的页面数量。
*/
for(pfr->order = 0;(PAGE_SIZE << pfr->order) < tot_mem; pfr->order++) ;
while((pfr->ring_memory = __get_free_pages(GFP_ATOMIC, pfr->order)) == 0)
if(pfr->order-- == 0)
break;
if(pfr->order == 0) {
return(-1);
}
tot_mem = PAGE_SIZE << pfr->order;
memset((char*)pfr->ring_memory, 0, tot_mem);
/* 要求系统不要将申请到的物理页面交换出去,应该始终驻留在内存当中 */
page_end = virt_to_page(pfr->ring_memory + (PAGE_SIZE << pfr->order) - 1);
for(page = virt_to_page(pfr->ring_memory); page <= page_end; page++)
SetPageReserved(page);
/*初始化缓冲区信息*/
pfr->slots_info = (FlowSlotInfo*)pfr->ring_memory;
pfr->ring_slots = (char*)(pfr->ring_memory+sizeof(FlowSlotInfo));
pfr->slots_info->version = RING_FLOWSLOT_VERSION;
pfr->slots_info->slot_len = the_slot_len;
pfr->slots_info->tot_slots = (tot_mem-sizeof(FlowSlotInfo))/the_slot_len;
pfr->slots_info->tot_mem = tot_mem;
pfr->slots_info->sample_rate = sample_rate;
pfr->insert_page_id = 1, pfr->insert_slot_id = 0;
/*
缓冲区的网络设备指针指向需要使用的设备,这时,整个套接字及环形缓冲区已经准备就绪,能够开始使用了。
*/
pfr->ring_netdev = dev;
return(0);
}
这时套接字及其共享环形缓冲区已经准备就绪,用户接下来需要做的就是将这个环形缓冲区映射到用户层,这样用户就能够在用户层操纵这个缓冲区,包括读写每一个slot中的数据了。
如果需要映射内存,需要在用户层调用mmap()系统调用,并且传递申请到的PF_RING套接字描述符给这个函数。
ring_buffer = (char *)mmap(NULL, memSlotsLen,
PROT_READ|PROT_WRITE,
MAP_SHARED, ring_fd, 0);
四、利用RING套接字传输数据
当用户创建了一个RING套接字并且进行了绑定和内存映射以后,就可以开始使用这个套接字在内核和用户态进行“零拷贝”数据传输了。
前面的内核阅读文档已经提到,从网卡驱动程序到内核传递数据的关键函数是netif_rx()以及netif_receive_skb()。其中前者用于普通的中断方式,而后者用于NAPI设备轮询传输。
不论采用何种传输模式,都可以采用RING套接字方式传输数据,实现方法就是在两个关键函数的起始位置插入RING套接字处理函数的调用。在ring_packet.c中定义了一个处理函数my_ring_handler(),每当有网络数据通过netif_rx()以及netif_receive_skb()向上层协议传递的时候,都会首先经过这个函数的处理:
static int my_ring_handler(struct sk_buff *skb, u_char recv_packet) {
struct sock *skElement;
int rc = 0;
struct ring_list *ptr;
/*当前我们只处理接收数据包,而不理睬外发的数据*/
if((!skb) /* Invalid skb */
|| ((!enable_tx_capture) && (!recv_packet))) /*
An outgoing packet is about to be sent out
but we decided not to handle transmitted
packets.
*/
return(0);
read_lock(&ring_mgmt_lock);
/*在ring套接字列表中查找是否有某些ring套接字准备在当前设备上接收数据,如果有,则将当前skb加入这个套接字的环形缓冲区中,如果没有任何套接字准备从当前设备接收数据,则释放skb然后直接返回。*/
ptr = ring_table;
while(ptr != NULL) {
struct ring_opt *pfr;
skElement = ptr->sk;
pfr = ring_sk(skElement);
if((pfr != NULL)
&& (pfr->ring_slots != NULL)
&& (pfr->ring_netdev == skb->dev)) {
add_skb_to_ring(skb, pfr, recv_packet);
/* DO NOT DISABLE THE MAIN NETWORK INTERFACE !!!! */
rc = 1; /* Ring found: we've done our job */
}
ptr = ptr->next;
}
read_unlock(&ring_mgmt_lock);
if(transparent_mode) rc = 0;
if(rc != 0)
dev_kfree_skb(skb); /* Free the skb */
return(rc); /* 0 = packet not handled */
}
如果已经发现某个ring套接字需要处理当前skb,则调用add_skb_to_ring()将skb加入套接字的环形缓冲区中:
static void add_skb_to_ring(struct sk_buff *skb, struct ring_opt *pfr, u_char recv_packet) {
FlowSlot *theSlot;
int idx, displ;
if(recv_packet)
displ = SKB_DISPLACEMENT;
else
displ = 0;
write_lock(&pfr->ring_index_lock);
/*将接收数据包的计数器加一*/
pfr->slots_info->tot_pkts++;
/* 利用BPF 过滤器过滤掉不需要接收的数据,这里暂时不分析*/
if(pfr->bpfFilter != NULL) {
…
}
/* 进行数据采样,这里暂时不分析*/
if(pfr->sample_rate > 1) {
…
}
/*获取当前数据的插入位置,如果当前插入位置上的slot状态为0,表示当前slot处于未使用(就绪)状态,则执行插入操作*/
idx = pfr->slots_info->insert_idx;
theSlot = get_insert_slot(pfr);
if((theSlot != NULL) && (theSlot->slot_state == 0)) {
struct pcap_pkthdr *hdr;
unsigned int bucketSpace;
char *bucket;
/* 刷新插入索引,如果索引值已经超过最大插槽数量,则需要进行循环 */
idx++;
if(idx == pfr->slots_info->tot_slots)
pfr->slots_info->insert_idx = 0;
else
pfr->slots_info->insert_idx = idx;
write_unlock(&pfr->ring_index_lock);
bucketSpace = pfr->slots_info->slot_len
#ifdef RING_MAGIC
- sizeof(u_char)
#endif
- sizeof(u_char) /* flowSlot.slot_state */
- sizeof(struct pcap_pkthdr)
- 1 /* 10 */ /* safe boundary */;
bucket = &theSlot->bucket;
hdr = (struct pcap_pkthdr*)bucket;
if(skb->stamp.tv_sec == 0) do_gettimeofday(&skb->stamp);
hdr->ts.tv_sec = skb->stamp.tv_sec, hdr->ts.tv_usec = skb->stamp.tv_usec;
hdr->caplen = skb->len+displ;
if(hdr->caplen > bucketSpace)
hdr->caplen = bucketSpace;
/*上面计算了当前插槽的容量,必须能够容纳当前skb,如果不能容纳,则必须将skb切断以适合缓冲区大小,然后将skb的数据部分拷贝到缓冲区当中。如果用户在创建一个ring套接字时指定了bucket_len,就会在bind操作时根据这个值确定环形缓冲区每一个slot的大小。那么进行拷贝时就会拷贝相应数量的数据,为了加快处理速度同时只需要数据包的头部信息(例如libpcap只看每个数据包的前64个字节),就将bucket_len设置较小的值。注意这里实际上并没有真正实现“零拷贝”,因为还是进行了一次内存拷贝操作*/
hdr->len = skb->len+displ;
memcpy(&bucket[sizeof(struct pcap_pkthdr)], skb->data-displ, hdr->caplen);
/*刷新当前slot插槽的信息,将状态置为使用中*/
pfr->slots_info->tot_insert++;
theSlot->slot_state = 1;
} else {
pfr->slots_info->tot_lost++;
write_unlock(&pfr->ring_index_lock);
/* wakeup in case of poll() */
if(waitqueue_active(&pfr->ring_slots_waitqueue))
wake_up_interruptible(&pfr->ring_slots_waitqueue);
}
五、后记
这部分内容是很久以后才补充的,因为工作一忙,我就立刻去救火,所有非工作必须的工作就一古脑的丢光了,现在只能匆匆收尾了,如果以后有这个需要,再来完善它吧。
当我分析PF_RING套接字时,是在2.4内核上打上补丁的,利用一个特制的精简版Libpcap与PF_RING套接字结合使用,在网络抓包的测试中取得了相当不错的效果,512字节以上的TCP和UDP数据几乎可以达到前兆线速,而64字节抓包情况下,由于我们当时的smartbits在 500M 流量下自己就丢包了,所以只测试了 500M 极限情况,同样也是线速,后来我才用了内核零拷贝技术,修改了e1000网卡的驱动程序,测试以后的结果也不比PF_RING套接字强多少,从上面的分析中知道,其实PF_RING套接字的实现原理与零拷贝是一致的。