绑定完请刷新页面
取消
刷新

分享好友

×
取消 复制
Linux内核IP Queue机制的分析(三)——ip_queue内核模块的分析(3)
2020-05-25 10:23:22

(三)ip_queue报文入队处理函数的注册
   在上面分析的模块注册代码中,IP Queue的报文入队处理函数的注册是通过调用nf_register_queue_handler()来实现的。因此,有必要了解一下该函数的源码,源码位于nf_queue.c中:

  1. /* return EBUSY when somebody else is registered, return EEXIST if the
  2. * same handler is registered, return 0 in case of success. */
  3. int nf_register_queue_handler(int pf, struct nf_queue_handler *qh)
  4. {      
  5.         int ret;
  6.         /*IP协议族的值必须在当前指定的范围内*/
  7.         if (pf >= NPROTO)
  8.                 return -EINVAL;

  9.         write_lock_bh(&queue_handler_lock);
  10.         /*该queue handler已经被注册过了*/
  11.         if (queue_handler[pf] == qh)
  12.                 ret = -EEXIST;
  13.         /*该协议族已经被注册了handler了*/
  14.         else if (queue_handler[pf])
  15.                 ret = -EBUSY;
  16.         /*将该协议的queue hanler指向参数qh*/
  17.         else {
  18.                 queue_handler[pf] = qh;
  19.                 ret = 0;
  20.         }
  21.         write_unlock_bh(&queue_handler_lock);

  22.         return ret;
  23. }
复制代码


该函数的代码比较简单,先来了解一下函数的两个参数:
(1)pf:IP协议族的值,PF_INET和PF_INET6分别代表IPv4和IPv6。
(2)qh:NF中对报文进行Queue的结构体,定义如下(netfilter.h):

  1. /* Packet queuing */
  2. struct nf_queue_handler {
  3.         int (*outfn)(struct sk_buff *skb, struct nf_info *info,
  4.                      unsigned int queuenum, void *data);
  5.         void *data;
  6.         char *name;
  7. };
复制代码


由此可见,该结构体主要包含一个函数指针,用于处理NF框架需要Queue的报文。data应该是用来保存一些私有数据,name则是该queue handler的名称。
代码中已经包含了该函数源码的简单注释,这里再对该函数进行一下简单的总结:
(1) 每个协议族只能注册一个queue handler;
(2) 随后报文的处理中,可以根据报文的协议族,就可以找到报文进行Queue时的处理函数queue_handler[pf]->outfn()。
        在IP Queue模块中,queue handler的注册如下所示:

  1. status = nf_register_queue_handler(PF_INET, &nfqh);
复制代码


        可见,注册的是IPv4协议族的报文处理函数,而nfqh结构体的定义如下:

  1. static struct nf_queue_handler nfqh = {
  2.         .name        = "ip_queue",
  3.         .outfn        = &ipq_enqueue_packet,
  4. };
复制代码


这里,IP Queue处理报文的函数终于闪亮登场了,我们前面啰嗦了半天,主要就是想理顺一下思路,顺理成章的引出该函数。
(四)入队函数ipq_enqueue_packet —— 发送数据包到用户空间
        第二部分已经分析过了该函数会在什么条件下被触发。这里详细分析该函数的实现,代码在ip_queue.c中。
        分析代码之前,我们先了解一下IP Queue对数据包队列管理的核心数据结构:

  1. struct ipq_queue_entry {
  2.         struct list_head list;
  3.         struct nf_info *info;
  4.         struct sk_buff *skb;
  5. };
复制代码


所有被Queue到用户空间的数据包都会有这样一个结构体。其中,
该数据结构的第1个元素是个双向链表结构,用于将所有queue的数据包用双向链表连
接起来,实现队列管理。
第2个元素同样是一个结构体,其定义在netfilter.h中:

  1. /* Each queued (to userspace) skbuff has one of these. */
  2. struct nf_info
  3. {
  4.         /* The ops struct which sent us to userspace. */
  5.         struct nf_hook_ops *elem;
  6.         
  7.         /* If we're sent to userspace, this keeps housekeeping info */
  8.         int pf;
  9.         unsigned int hook;
  10.         struct net_device *indev, *outdev;
  11.         int (*okfn)(struct sk_buff *);
  12. };
复制代码


这个结构体应该很容易看出他的作用,就是记录下当数据包被Queue时所在的hook函数的相关信息,包括hook的操作结构、协议号、hook点等相关信息。当用户空间下发了对数据包的处理结果时,内核就需要参考这个结构对数据包进行进一步的处理。具体的我们会在后面数据包回注函数中分析。
第3个元素是skb本身,也就是回注函数中要处理的数据包。

OK,我们下面就开始如对函数的分析。

  1. static int
  2. ipq_enqueue_packet(struct sk_buff *skb, struct nf_info *info,
  3.                    unsigned int queuenum, void *data)
  4. {
  5.         int status = -EINVAL;
  6.         struct sk_buff *nskb;
  7.         struct ipq_queue_entry *entry;

  8.         /*判断用户配置的模式,可以为拷贝元数据或者整个数据包的信息*/
  9.         if (copy_mode == IPQ_COPY_NONE)
  10.                 return -EAGAIN;
  11.         /*为即将入队的数据包分配一个struct ipq_queue_entry 结构体,用于实现对数据包的管理,我们称之为queue管理结构体*/
  12.         entry = kmalloc(sizeof(*entry), GFP_ATOMIC);
  13.         if (entry == NULL) {
  14.                 printk(KERN_ERR "ip_queue: OOM in ipq_enqueue_packet()\n");
  15.                 return -ENOMEM;
  16.         }
  17.         /*将该数据包对应的相关信息保存到管理结构体中*/
  18.         entry->info = info;
  19.         entry->skb = skb;
  20.         
  21. /*构建用于发往用户空间的消息*/
  22.         nskb = ipq_build_packet_message(entry, &status);
  23.         if (nskb == NULL)
  24.                 goto err_out_free;
  25.                 
  26.         write_lock_bh(&queue_lock);
  27.         
  28.         /如果用户空间没有开启进程,等待接收消息的话,就释放该消息/
  29.         if (!peer_pid)
  30.                 goto err_out_free_nskb; 
  31.         /*如果当前队列中的数据包总数超过了设置的大值,则是放该消息,并且增加丢弃数据包的统计计数*/
  32.         if (queue_total >= queue_maxlen) {
  33.                 queue_dropped++;
  34.                 status = -ENOSPC;
  35.                 if (net_ratelimit())
  36.                           printk (KERN_WARNING "ip_queue: full at %d entries, "
  37.                                   "dropping packets(s). Dropped: %d\n", queue_total,
  38.                                   queue_dropped);
  39.                 goto err_out_free_nskb;
  40.         }

  41.         /* 将消息发送给用户空间 */ 
  42.         status = netlink_unicast(ipqnl, nskb, peer_pid, MSG_DONTWAIT);
  43.         if (status < 0) {
  44.                 queue_user_dropped++;
  45.                 goto err_out_unlock;
  46.         }
  47.         /*成功发送到用户空间之后,将该数据包的queue管理结构添加到全局队列链表中*/
  48.         __ipq_enqueue_entry(entry);

  49.         write_unlock_bh(&queue_lock);
  50.         return status;

  51. err_out_free_nskb:
  52.         kfree_skb(nskb); 
  53.         
  54. err_out_unlock:
  55.         write_unlock_bh(&queue_lock);

  56. err_out_free:
  57.         kfree(entry);
  58.         return status;
  59. }
复制代码


该函数成功执行之后,就将数据包相关的信息发送到用户空间,同时将该数据包对应的queue管理结构加到全局链表中。
下面对于ipq_enqueue_packet函数中调用的几个重要函数做一些分析。
首先是构建消息的函数ipq_build_packet_message。

  1. static struct sk_buff *
  2. ipq_build_packet_message(struct ipq_queue_entry *entry, int *errp)
  3. {
  4.         unsigned char *old_tail;
  5.         size_t size = 0;
  6.         size_t data_len = 0;
  7.         struct sk_buff *skb;
  8.         struct ipq_packet_msg *pmsg;
  9.         struct nlmsghdr *nlh;

  10.         read_lock_bh(&queue_lock);
  11.         /*根据用户配置的copy模式,确定发给用户空间消息的长度*/
  12.         switch (copy_mode) {
  13.         /*对于初始模式和拷贝元数据的模式,消息应该包括netlink的消息头和ipq的消息头,长度为两个消息头长度之和,即sizeof(struct nlmsghdr) + sizeof(struct ipq_packet_msg),并考虑对齐的因素。这里直接调用netlink封装的宏NLMSG_SPACE来计算长度。该宏本身已经包含了netlink消息头的长度*/
  14.         case IPQ_COPY_META:
  15.         case IPQ_COPY_NONE:
  16.                 /*消息长度为netlink的消息头和ipq的消息头的长度之和*/
  17.                 size = NLMSG_SPACE(sizeof(*pmsg));
  18.                 data_len = 0;
  19.                 break;
  20.         
  21.         case IPQ_COPY_PACKET:
  22.                 if (entry->skb->ip_summed == CHECKSUM_HW &&
  23.                     (*errp = skb_checksum_help(entry->skb,
  24.                                                entry->info->outdev == NULL))) {
  25.                         read_unlock_bh(&queue_lock);
  26.                         return NULL;
  27.                 }
  28.                 /*如果用户需要拷贝整个数据包的内容,那么如果配置的长度为0或者超出数据包的实际长度,则以数据包的实际长度进行拷贝*/
  29.                 if (copy_range == 0 || copy_range > entry->skb->len)
  30.                         data_len = entry->skb->len;
  31.                 else
  32.                         data_len = copy_range;
  33.                 /*消息长度为netlink的消息头、ipq的消息头的长度以及要拷贝数据包的长度之和*/
  34.                 size = NLMSG_SPACE(sizeof(*pmsg) + data_len);
  35.                 break;
  36.         
  37.         default:
  38.                 *errp = -EINVAL;
  39.                 read_unlock_bh(&queue_lock);
  40.                 return NULL;
  41.         }

  42.         read_unlock_bh(&queue_lock);
  43.         /*为构建的消息申请内存,同样适用skb结构体,消息的内容应该在skb->data和skb->tail之间*/
  44.         skb = alloc_skb(size, GFP_ATOMIC);
  45.         if (!skb)
  46.                 goto nlmsg_failure;
  47.         /*记录一下新分配的skb中的tail指针的地址,此时应该skb->tail指向skb->data*/
  48.         old_tail= skb->tail;
  49.         /*填充netlink消息头*/
  50.         nlh = NLMSG_PUT(skb, 0, 0, IPQM_PACKET, size - sizeof(*nlh));
  51.         /*获取netlink消息体的起始地址,即ipq的消息头*/
  52.         pmsg = NLMSG_DATA(nlh);
  53.         memset(pmsg, 0, sizeof(*pmsg));

  54.         /*将相关的ipq消息记录到消息头中*/
  55.                 /*将skb对应的queue管理结构体的地址作为packet_id*/
  56.         pmsg->packet_id       = (unsigned long )entry; 
  57.         pmsg->data_len        = data_len;
  58.         pmsg->timestamp_sec   = entry->skb->tstamp.off_sec;
  59.         pmsg->timestamp_usec  = entry->skb->tstamp.off_usec;
  60.         pmsg->mark            = entry->skb->nfmark;
  61.         pmsg->hook            = entry->info->hook;
  62.         pmsg->hw_protocol     = entry->skb->protocol;
  63.         
  64.         /*记录被queue数据包对应的输入设备名称*/
  65.         if (entry->info->indev)
  66.                 strcpy(pmsg->indev_name, entry->info->indev->name);
  67.         else
  68.                 pmsg->indev_name[0] = '\0';
  69.         /*记录被queue数据包对应的输出设备名称*/
  70.         if (entry->info->outdev)
  71.                 strcpy(pmsg->outdev_name, entry->info->outdev->name);
  72.         else
  73.                 pmsg->outdev_name[0] = '\0';

  74.         /*记录被queue数据包对应的链路层的协议类型及地址长度*/
  75.         if (entry->info->indev && entry->skb->dev) {
  76.                 pmsg->hw_type = entry->skb->dev->type;
  77.                 if (entry->skb->dev->hard_header_parse)
  78.                         pmsg->hw_addrlen =
  79.                                 entry->skb->dev->hard_header_parse(entry->skb,
  80.                                                                    pmsg->hw_addr);
  81.         }
  82.         /* data_len != 0 说明需要拷贝数据包的原始数据。skb_copy_bits 函数的实现不再分析,其主要功能就是将从skb->data+offset开始的数据拷贝data_len个字节到pmsg->payload中,即ipq消息的载荷。另外一个需要注意的问题,如果skb挂载的有分片包,则skb_copy_bits也会按照顺序拷贝分片包中的数据*/
  83.         if (data_len)
  84.                 if (skb_copy_bits(entry->skb, 0, pmsg->payload, data_len))
  85.                         BUG();
  86.         /*设置netlink消息的长度*/
  87.         nlh->nlmsg_len = skb->tail - old_tail;
  88.         return skb;

  89. nlmsg_failure:
  90.         if (skb)
  91.                 kfree_skb(skb);
  92.         *errp = -EINVAL;
  93.         printk(KERN_ERR "ip_queue: error creating packet message\n");
  94.         return NULL;
  95. }
复制代码


其次,对于netlink_unicast函数,我们这里不具体分析,它的作用就是将内核封装好的netlink消息发送到用户态。
后分析一下__ipq_enqueue_entry 函数。

  1. static inline void
  2. __ipq_enqueue_entry(struct ipq_queue_entry *entry)
  3. {
  4.        list_add(&entry->list, &queue_list);
  5.        queue_total++;
  6. }
复制代码


该函数的功能很简答,就是将当前skb的queue管理结构添加到全局的queue管理链表queue_list中,并增加队列的统计计数。该链表记录了所有发往用户空间,且并未收到用户空间下发处理结果的数据包。
至此,数据包的入队处理函数已经分析完毕。一切顺利执行的话,现在用户态已经接收到关于该数据包的消息。

--未完待续  

文章来源CU社区:Linux内核IP Queue机制的分析(三)——ip_queue内核模块的分析

分享好友

分享这个小栈给你的朋友们,一起进步吧。

内核源码
创建时间:2020-05-18 13:36:55
内核源码精华帖内容汇总
展开
订阅须知

• 所有用户可根据关注领域订阅专区或所有专区

• 付费订阅:虚拟交易,一经交易不退款;若特殊情况,可3日内客服咨询

• 专区发布评论属默认订阅所评论专区(除付费小栈外)

技术专家

查看更多
  • 飘絮絮絮丶
    专家
戳我,来吐槽~