转自“峥嵘岁月”http://blog.csdn.net/joshua_yu/archive/2006/01/27/589460.aspx
也是做PF——RING的,半年以前就发了,汗,我都快看一年了,要坚持持续看阿~~~
Linux设备轮询机制分析
一、设备轮询机制的基本思想
所谓的设备轮询机制实际上就是利用网卡驱动程序提供的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;
if(!capable(CAP_NET_ADMIN))
return -EPERM;
if(sock->type != SOCK_RAW)
return -ESOCKTNOSUPPORT;
if(protocol != htons(ETH_P_ALL))
return -EPROTONOSUPPORT;
err = -ENOMEM;
sk = sk_alloc(PF_RING, GFP_KERNEL, 1
#if (LINUX_VERSION_CODE >=
KERNEL_VERSION(2,6,0))
, NULL
#endif
);
if (sk == NULL)
goto out;
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;
unsigned long order;
unsigned long ring_memory;
FlowSlotInfo *slots_info;
char *ring_slots; //Basically it points to
ring_memory+sizeof(FlowSlotInfo)
u_int pktToSample, sample_rate;
struct sk_filter *bpfFilter;
atomic_t num_ring_slots_waiters;
wait_queue_head_t ring_slots_waitqueue;
rwlock_t ring_index_lock;
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)
+ sizeof(u_short)
+ bucket_len ;
tot_mem = sizeof(FlowSlotInfo) + num_slots*the_slot_len;
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)
|| ((!enable_tx_capture) &&
(!recv_packet)))
return(0);
read_lock(&ring_mgmt_lock);
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);
rc = 1;
}
ptr = ptr->next;
}
read_unlock(&ring_mgmt_lock);
if(transparent_mode) rc = 0;
if(rc != 0)
dev_kfree_skb(skb);
return(rc);
}
如果已经发现某个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++;
if(pfr->bpfFilter != NULL)
{
…
}
if(pfr->sample_rate > 1)
{
…
}
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)
- sizeof(struct pcap_pkthdr)
- 1 ;
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;
hdr->len = skb->len+displ;
memcpy(&bucket[sizeof(struct pcap_pkthdr)],
skb->data-displ, hdr->caplen);
pfr->slots_info->tot_insert++;
theSlot->slot_state = 1;
} else {
pfr->slots_info->tot_lost++;
write_unlock(&pfr->ring_index_lock);
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套接字的实现原理与零拷贝是一致的。