Linux设备轮询机制分析

http://blog.csdn.net/joshua_yu/archive/2006/01/27/589460.aspx


一、 设备轮询机制的基本思想

所谓的设备轮询机制实际上就是利用网卡驱动程序提供的 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 套接字的实现原理与零拷贝是一致的。

你可能感兴趣的:(linux,工作,网络,struct,null,insert)