PF_RING实现分析

原文:http://bbs.chinaunix.net/thread-1943951-1-1.html

内核版本:Linux 2.6.30.9


PF_RING版本:4.1.0

最近看了一个PF_RING的实现,看了个大概,发上来大家讨论讨论,共同学习。

一、什么是PF_RING
PF_RING是一个第三方的内核数据包捕获接口,类似于libpcap,它的官方网址是: http://www.ntop.org/PF_RING.html

二、为什么需要PF_RING
一切为了效率,按照其官方网站上的测试数据,在Linux平台之上,其效率至少高于libpcap 50% - 60%,甚至是一倍。更好的是,PF_RING提供了一个修改版本的libpcap,使之建立在PF_RING接口之上。这样,原来使用libpcap的程序,就可以自然过渡了。

三、声明
1、这不是“零拷贝”,研究“零拷贝”的估计要失望,如果继续看下去的话;
2、这不是包截获接口,如果需要拦截、修改内核数据包,请转向Netfilter;
3、本文只分析了PF_RING最基础的部份。关于DNA、TNAPI,BPF等内容不包含在内。

四、源码的获取
svn co https://svn.ntop.org/svn/ntop/trunk/PF_RING/

最近好像全流行svn了。

五、编译和使用
接口分为两部份,一个是内核模块,一个是用户态的库
cd my_pf_ring_goes_here
cd kernel
make
sudo insmod ./pf_ring.ko
cd ../userland
make

在源码目录中,关于用户态的库有使用的现成的例子,很容易依葫芦画瓢。后文也会提到用户态库的实现的简单分析,可以两相比照,很容易上手。而且源码目录中有一个PDF文档,有详细的API介绍,建议使用前阅读。

六、实现分析初步
1、核心思路
A、在内核队列层注册Hook,获取数据帧。
B、在内核创建一个环形队列(这也是叫RING的原因),用于存储数据,并使用mmap映射到用户空间。这样,避免用户态的系统调用,也是提高性能的关键所在。
C、创建了一个新的套接字类型PF_RING,用户态通过它与内核通信。

2、模块初始化
模块源码只有一个文件,在目录树kernel/pf_ring.c,嗯,还有一个头文件,在kernel/linux下
  1. static int __init ring_init(void)
  2. {
  3.   int i, rc;

  4.   printk("[PF_RING] Welcome to PF_RING %s ($Revision: 4012 $)\n"
  5.          "(C) 2004-09 L.Deri <[email]deri@ntop.org[/email]>\n", RING_VERSION);
  6.        
  7.         //注册名为PF_RING的新协议
  8.   if((rc = proto_register(&ring_proto, 0)) != 0)
  9.     return(rc);


ring_proto的定义为
  1. #if(LINUX_VERSION_CODE > KERNEL_VERSION(2,6,11))
  2. static struct proto ring_proto = {
  3.   .name = "PF_RING",
  4.   .owner = THIS_MODULE,
  5.   .obj_size = sizeof(struct ring_sock),
  6. };
  7. #endif


初始化四个链表,它们的作用,后文会分析到:
  1.         //初始化四个链表
  2.   INIT_LIST_HEAD(&ring_table);                                        /* List of all ring sockets. */
  3.   INIT_LIST_HEAD(&ring_cluster_list);                        /* List of all clusters */
  4.   INIT_LIST_HEAD(&ring_aware_device_list);                /* List of all devices on which PF_RING has been registered */
  5.   INIT_LIST_HEAD(&ring_dna_devices_list);                /* List of all dna (direct nic access) devices */


device_ring_list是一个指针数组,它的每一个元素对应一个网络设备,后文也会分析它的使用:
  1. /*
  2.   For each device, pf_ring keeps a list of the number of
  3.   available ring socket slots. So that a caller knows in advance whether
  4.   there are slots available (for rings bound to such device)
  5.   that can potentially host the packet
  6. */
  7.   for (i = 0; i < MAX_NUM_DEVICES; i++)
  8.     INIT_LIST_HEAD(&device_ring_list[i]);

  1.         //为新协议注册sock
  2.   sock_register(&ring_family_ops);


ring_family_ops定义为
  1. static struct net_proto_family ring_family_ops = {
  2.   .family = PF_RING,
  3.   .create = ring_create,
  4.   .owner = THIS_MODULE,
  5. };

这样,当用户空间创建PF_RING时,例如,
  1. fd = socket(PF_RING, SOCK_RAW, htons(ETH_P_ALL));

ring_create将会被调用

  1.   //注册通知链表  
  2.   register_netdevice_notifier(&ring_netdev_notifier);

  3.   /* 工作模式语法检查 */
  4.   if(transparent_mode > driver2pf_ring_non_transparent)
  5.     transparent_mode = standard_linux_path;


PF_RING一共有三种工作模式:
  1. typedef enum {
  2.   standard_linux_path = 0, /* Business as usual */
  3.   driver2pf_ring_transparent = 1, /* Packets are still delivered to the kernel */
  4.   driver2pf_ring_non_transparent = 2 /* Packets not delivered to the kernel */
  5. } direct2pf_ring;

第一种最简单,这里仅分析第一种

  1.         //输出工作信息参数
  2.   printk("[PF_RING] Ring slots       %d\n", num_slots);
  3.   printk("[PF_RING] Slot version     %d\n",
  4.          RING_FLOWSLOT_VERSION);
  5.   printk("[PF_RING] Capture TX       %s\n",
  6.          enable_tx_capture ? "Yes [RX+TX]" : "No [RX only]");
  7.   printk("[PF_RING] Transparent Mode %d\n",
  8.          transparent_mode);
  9.   printk("[PF_RING] IP Defragment    %s\n",
  10.          enable_ip_defrag ? "Yes" : "No");
  11.   printk("[PF_RING] Initialized correctly\n");


num_slots为槽位总数,系统采用数组来实现双向环形队列,它也就代表数组的最大元素。
版本信息:不用多说了。不过我的版本在2.6.18及以下都没有编译成功,后来使用2.6.30.9搞定之。
enable_tx_capture:是否启用发送时的数据捕获,对于大多数应用而言,都是在接收时处理。
enable_ip_defrag:为用户提供一个接口,是否在捕获最重组IP分片。

  1.         //创建/proc目录
  2.   ring_proc_init();
  3.   
  4.   //注册设备句柄
  5.   register_device_handler();

  6.   pfring_enabled = 1;                //工作标志
  7.   return 0;
  8. }


register_device_handler注册了一个协议,用于数据包的获取:
  1. /* Protocol hook */
  2. static struct packet_type prot_hook;

  3. void register_device_handler(void) {
  4.         //只有在第一种模式下,才用这种方式接收数据
  5.   if(transparent_mode != standard_linux_path) return;

  6.   prot_hook.func = packet_rcv;
  7.   prot_hook.type = htons(ETH_P_ALL);
  8.   dev_add_pack(&prot_hook);
  9. }

  10. void register_device_handler(void) {
  11.   if(transparent_mode != standard_linux_path) return;

  12.   prot_hook.func = packet_rcv;
  13.   prot_hook.type = htons(ETH_P_ALL);
  14.   dev_add_pack(&prot_hook);
  15. }


2、创建套接字
Linux的套按字的内核接口,使用了两个重要的数据结构:
struct socket和struct sock,这本来并没有什么,不过令人常常迷惑的是,前者常常被缩写为sock,即:
struct socket *sock;
这样,“sock”就容易造成混淆了。还好,后者常常被缩写为sk……
我这里写sock指前者,sk指后者,如果不小心写混了,请参考上下文区分

关于这两个结构的含义,使用等等,可以参考相关资料以获取详细信息,如《Linux情景分析》。我的个人网站 www.skynet.org.cn上也分析了Linux socket的实现。可以参考。这里关于socket的进一步信息,就不详细分析了。

这里的创建套接字,内核已经在系统调用过程中,准备好了sock,主要就是分析sk,并为sk指定一系列的操作函数,如bind、mmap、poll等等。
如前所述,套接字的创建,是通过调用ring_create函数来完成的:
  1. static int ring_create(
  2. #if(LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,24))
  3.                        struct net *net,
  4. #endif
  5.                        struct socket *sock, int protocol)
  6. {
  7.   struct sock *sk;
  8.   struct ring_opt *pfr;
  9.   int err;

  10. #if defined(RING_DEBUG)
  11.   printk("[PF_RING] ring_create()\n");
  12. #endif

  13.   /* 权限验证 ? */
  14.   if(!capable(CAP_NET_ADMIN))
  15.     return -EPERM;

  16.   //协议簇验证
  17.   if(sock->type != SOCK_RAW)
  18.     return -ESOCKTNOSUPPORT;

  19.   //协议验证
  20.   if(protocol != htons(ETH_P_ALL))
  21.     return -EPROTONOSUPPORT;

  22.   err = -ENOMEM;

  23.   // 分配sk
  24.   // options are.
  25. #if(LINUX_VERSION_CODE <= KERNEL_VERSION(2,6,11))
  26.   sk = sk_alloc(PF_RING, GFP_KERNEL, 1, NULL);
  27. #else
  28. #if(LINUX_VERSION_CODE < KERNEL_VERSION(2,6,24))
  29.     // BD: API changed in 2.6.12, ref:
  30.     // [url]http://svn.clkao.org/svnweb/linux/revision/?rev=28201[/url]
  31.     sk = sk_alloc(PF_RING, GFP_ATOMIC, &ring_proto, 1);
  32. #else
  33.     sk = sk_alloc(net, PF_INET, GFP_KERNEL, &ring_proto);
  34. #endif
  35. #endif

  36.   //分配失败
  37.   if(sk == NULL)
  38.     goto out;
  39.   
  40.   //这里很重要,设定sock的ops,即对应用户态的bind、connect诸如此类操作的动作
  41.   sock->ops = &ring_ops;

  42.   //初始化sock结构(即sk)各成员,并设定与套接字socket(即sock)的关联
  43.   sock_init_data(sock, sk);
  44. #if(LINUX_VERSION_CODE <= KERNEL_VERSION(2,6,11))
  45.   sk_set_owner(sk, THIS_MODULE);
  46. #endif

  47.   err = -ENOMEM;

  48. #define ring_sk_datatype(__sk) ((struct ring_opt *)__sk)
  49. #define ring_sk(__sk) ((__sk)->sk_protinfo)

  50. //作者喜欢用小写的宏
  51. //这里分配一个struct ring_opt结构,这个结构比较重要,其记录了ring的选项信息。
  52. 在sk中,使用sk_protinfo成员指向之,这样就建立了sock->sk->ring_opt的关联。可以通过套接字很容易获取Ring的信息。
  53.   ring_sk(sk) = ring_sk_datatype(kmalloc(sizeof(*pfr), GFP_KERNEL));
  54.   //分配失败
  55.   if(!(pfr = ring_sk(sk))) {
  56.     sk_free(sk);
  57.     goto out;
  58.   }

  59.   //初始化各成员
  60.   memset(pfr, 0, sizeof(*pfr));
  61. //激活标志
  62.   pfr->ring_active = 0;        /* We activate as soon as somebody waits for packets */
  63.   //通道ID
  64.   pfr->channel_id = RING_ANY_CHANNEL;
  65.   //RING的每个槽位的桶的大小,其用来存储捕获的数据帧,这个值,用户态也可以使用setsocketopt来调整
  66.   pfr->bucket_len = DEFAULT_BUCKET_LEN;
  67. //过滤器hash桶
  68.   pfr->handle_hash_rule = handle_filtering_hash_bucket;
  69.   //初始化等待队列
  70.   init_waitqueue_head(&pfr->ring_slots_waitqueue);
  71.   //初始化RING的锁
  72.   rwlock_init(&pfr->ring_index_lock);
  73.   rwlock_init(&pfr->ring_rules_lock);
  74.   //初始化使用计数器
  75.   atomic_set(&pfr->num_ring_users, 0);
  76.   INIT_LIST_HEAD(&pfr->rules);
  77.   //设定协议簇
  78.   sk->sk_family = PF_RING;
  79. //设定sk的destuct函数
  80.   sk->sk_destruct = ring_sock_destruct;

  81.   //sk入队
  82.   ring_insert(sk);

  83. #if defined(RING_DEBUG)
  84.   printk("[PF_RING] ring_create() - created\n");
  85. #endif

  86.   return(0);
  87. out:
  88.   return err;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值