内核版本:3.4.39

之前因工作原因接触到了IPv4 报文重组这个话题,一直以来对这个重组流程不是很清楚,所以很多功能的实现都避开了分片报文的处理,一方面是因为重组比较复杂,另一方面是经验不多无从下手,最近几周抽空详细看了下内核源码关于IPv4重组的流程,这里简要说明下,有描述不对的地方还请指出。

先简单描述下ipv4重组的流程:内核在传输层(L3层)收到分片报文后在传递给L4(TCP/UDP)之前会将分片报文重组,重组之前有一系列的操作,首次是检查分片报文队列所占内核空间是否超过阈值,超过的话就把旧的分片队列释放到阈值一下,然后根据分片报文的五元组(IP源地址、目的地址、协议类型、ID和user)得到一个hash值,然后去分片hash表中查找对应的hash分片队列,如果分片队列不存在或者不匹配就新建一个新的,得到分片队列指针后根据报文的偏移值将报文插入到分片队列中合适的位置,这个过程中可能需要处理分片重叠问题。

分片队列的结构图如下, ip4_frags是一个全局变量,hash是一个hash数组,里面挂着hash队列,队列里的元素是ipq(分片队列),分片队列之间通过链表链接起来,fragment是skb指针,分片报文就挂在这里。lru_list指针指向一个lru(Least Recently Used,最近最少使用)队列,每当分片队列收到一个报文都会重新刷新自己在lru队列位置(插入到尾部),这样当内核分片占用空间过大的时候,直接释放lru队列排在前面的元素就可以了。

Linux IPv4分片队列组织图

接下来就一步步分析重组的整个流程,有点长,但是很完整,哈哈。

/**  Deliver IP Packets to the higher protocol layers.*  IP层传递给L4层(TCP/UDP)的入口函数*/
int ip_local_deliver(struct sk_buff *skb)
{/**    Reassemble IP fragments.*//* 如果是分片报文,就调用ip_defrag 处理分片,这个函数如果重组成功* 就返回0和重组好的报文,然后继续往下走,最终调用ip_local_deliver_finish, 如果重组* 没有完成或者重组失败报文被丢弃则直接返回。*/if (ip_is_fragment(ip_hdr(skb))) {if (ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER))return 0;}return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN, skb, skb->dev, NULL,ip_local_deliver_finish);
}

分片报文根据IP头域的不同有三种,分别是第一个分片,最后一个分片以及中间的部分。

第一个分片它的MF标志位为1并且片偏移为0,因为是第一个分片,起始偏移位置为0.

最后一个分片,MF标志位为0并且片偏移不为0,MF为0表示没有后续分片了。

中间的分片MF标志位为1并且片偏移不为0.

ip_is_fragment就是判断如果IP头中分片标志位MF和片偏移有一个不为0就当作分片报文。

static inline bool ip_is_fragment(const struct iphdr *iph)
{return (iph->frag_off & htons(IP_MF | IP_OFFSET)) != 0;
}

ip_defrag的第二个参数这里填写的是IP_DEFRAG_LOCAL_DELIVER,表示是由IP层重组的,因为内核里需要对报文进行重组的地方不止IP层,其它诸如netfilter也会重组报文,可选的值如下

/* 重组的用户(user),定义在ip.h */
enum ip_defrag_users {IP_DEFRAG_LOCAL_DELIVER,IP_DEFRAG_CALL_RA_CHAIN,IP_DEFRAG_CONNTRACK_IN,__IP_DEFRAG_CONNTRACK_IN_END   = IP_DEFRAG_CONNTRACK_IN + USHRT_MAX,IP_DEFRAG_CONNTRACK_OUT,__IP_DEFRAG_CONNTRACK_OUT_END    = IP_DEFRAG_CONNTRACK_OUT + USHRT_MAX,IP_DEFRAG_CONNTRACK_BRIDGE_IN,__IP_DEFRAG_CONNTRACK_BRIDGE_IN = IP_DEFRAG_CONNTRACK_BRIDGE_IN + USHRT_MAX,IP_DEFRAG_VS_IN,IP_DEFRAG_VS_OUT,IP_DEFRAG_VS_FWD,IP_DEFRAG_AF_PACKET,IP_DEFRAG_MACVLAN,
};

接下来就看下ip_defrag函数,该函数是个包裹函数,本身不处理分片,它接收一个分片skb缓存和user字段,然后调用具体的分片处理函数去处理,重组成功返回0和重组好的skb,没有重组成功或者重组失败就返回一个非零值。

/* Process an incoming IP datagram fragment. */
int ip_defrag(struct sk_buff *skb, u32 user)
{struct ipq *qp;struct net *net;net = skb->dev ? dev_net(skb->dev) : dev_net(skb_dst(skb)->dev);/* snmp mib 统计数据 */IP_INC_STATS_BH(net, IPSTATS_MIB_REASMREQDS);/* Start by cleaning up the memory. *//* 首先判断当前分片队列所占内存是否超过阈值,如果超过的话* 需要主动去释放一些分片,因为内存有限,分片报文在重组好之前* 是一直放在内存里,不能无限度的存放。*/if (atomic_read(&net->ipv4.frags.mem) > net->ipv4.frags.high_thresh)ip_evictor(net);/* Lookup (or create) queue header *//* 这里根据分片五元组(源地址、目的地址、IP ID,protocol, user)去查找分片队列* ip_find函数查找成功就返回对应的分片队列,查找失败就新建一个分片队列,* 如果分配失败的话就返回NULL;*/if ((qp = ip_find(net, ip_hdr(skb), user)) != NULL) {int ret;spin_lock(&qp->q.lock);/* 这里是分片队列排队的地方,报文的排队,重组都在这里执行,下面* 再来分析该函数。*/ret = ip_frag_queue(qp, skb);spin_unlock(&qp->q.lock);/* 这是一个包裹函数,减少分片队列的引用计数,如果没人引用该* 队列就调用inet_frag_destroy释放队列所占资源。*/ipq_put(qp);return ret;}IP_INC_STATS_BH(net, IPSTATS_MIB_REASMFAILS);/* 创建分片队列失败,释放掉skb并返回ENOMEM */kfree_skb(skb);return -ENOMEM;
}
EXPORT_SYMBOL(ip_defrag);

我们首先来看下ip_evictor(net)这个函数,

/* Memory limiting on fragments.  Evictor trashes the oldest* fragment queue until we are back under the threshold.* 分片内存限制处理,将分片所占用空间保持到低阈值一下,* 主要调用inet_frag_evicor来处理*/
static void ip_evictor(struct net *net)
{int evicted;evicted = inet_frag_evictor(&net->ipv4.frags, &ip4_frags);if (evicted)IP_ADD_STATS_BH(net, IPSTATS_MIB_REASMFAILS, evicted);
}

继续分析inet_frag_evictor函数,该函数主要用来释放分片队列所占用空间:

int inet_frag_evictor(struct netns_frags *nf, struct inet_frags *f)
{struct inet_frag_queue *q;int work, evicted = 0;/* 首先得到需要释放的内存空间大小,* 用当前所占空间总额减去低阈值得到,这个值可以通过proc文件系统配置。*/work = atomic_read(&nf->mem) - nf->low_thresh;while (work > 0) {/* 先获取分片哈希表的读锁,如果lru链表为空就跳出 */read_lock(&f->lock);if (list_empty(&nf->lru_list)) {read_unlock(&f->lock);break;}/* 增加分片队列引用计数,释放分片哈希表读锁 */q = list_first_entry(&nf->lru_list,struct inet_frag_queue, lru_list);atomic_inc(&q->refcnt);read_unlock(&f->lock);/* 占用分片队列锁,如果还没有设置frag_complete标志位的话,* 调用inet_frag_kill去设置,该函数主要是将当前分片队列从分片哈希表中* 移除并且从lru链表中移除,这样就不会在使用了。*/spin_lock(&q->lock);if (!(q->last_in & INET_FRAG_COMPLETE))inet_frag_kill(q, f);spin_unlock(&q->lock);/* 如果分片队列这时无人引用的话,调用inet_frag_destroy 释放分片缓存* 所占用空间,下面再分析该函数 。*/if (atomic_dec_and_test(&q->refcnt))inet_frag_destroy(q, f, &work);evicted++;}return evicted;
}
EXPORT_SYMBOL(inet_frag_evictor);

看下inet_frag_kill函数,这个函数主要做些资源回收前的收尾工作:

void inet_frag_kill(struct inet_frag_queue *fq, struct inet_frags *f)
{/* 停止分片队列定时器,这个定时器用来防止长时间占用内存 */if (del_timer(&fq->timer))atomic_dec(&fq->refcnt);/* frag_complete一般是重组完成的时候或者释放分片队列的时候去设置,* 这里判断如果没有设置的话,就设置该标志位同时调用fq_unlink函数* 去处理链表移除的事情,包括哈希表和lru链表。*/if (!(fq->last_in & INET_FRAG_COMPLETE)) {fq_unlink(fq, f);atomic_dec(&fq->refcnt);fq->last_in |= INET_FRAG_COMPLETE;}
}
EXPORT_SYMBOL(inet_frag_kill);

fq_unlink的原型:

static inline void fq_unlink(struct inet_frag_queue *fq, struct inet_frags *f)
{write_lock(&f->lock);/* 从哈希分片队列中移除 */hlist_del(&fq->list);/* 从lru链表中移除 */list_del(&fq->lru_list);/* 减少排队的分片队列个数 */fq->net->nqueues--;write_unlock(&f->lock);
}

再来看下实际的分片队列资源回收处理函数 inet_frag_destroy,看这名字就知道

/* 释放分片队列所占资源 */
void inet_frag_destroy(struct inet_frag_queue *q, struct inet_frags *f,int *work)
{struct sk_buff *fp;struct netns_frags *nf;/* 正常情况下删除分片队列前都会置上该标志位并且分片队列的定时器* 应该停止,这里检查下,有异常就告警*/WARN_ON(!(q->last_in & INET_FRAG_COMPLETE));WARN_ON(del_timer(&q->timer) != 0);/* Release all fragment data. * 先释放所有的skb分片缓存*/fp = q->fragments;nf = q->net;while (fp) {struct sk_buff *xp = fp->next;/* 实际的释放函数 */frag_kfree_skb(nf, f, fp, work);fp = xp;}/* qsize 是分片结构体 struct ipq的大小 */if (work)*work -= f->qsize;atomic_sub(f->qsize, &nf->mem);/* 分片队列释放的回调处理函数* ipv4 这个函数是 ip4_frag_free,ipfrag_init中初始化。*/if (f->destructor)f->destructor(q);/* 最后释放分片队列所占内存 */kfree(q);
}
EXPORT_SYMBOL(inet_frag_destroy);

实际的skb释放函数由frag_kfree_skb完成,这个函数就是释放分片skb缓存,然后从当前所占的内存空减去释放的大小

/* 释放分片队列的skb buffer */
static inline void frag_kfree_skb(struct netns_frags *nf, struct inet_frags *f,struct sk_buff *skb, int *work)
{/* 一种情况下是分片队列已经重组完成,这时候需要释放,work 指针为空 * 还有一种情况是当内核分片队列所占内存空间过大,这时候内核需要主动* 释放一些旧的分片队列,这时候work指针就表示需要释放的空间大小*/if (work)*work -= skb->truesize;/* 从分片所占用的总的内存数量中减去当前释放的skb缓存大小 */atomic_sub(skb->truesize, &nf->mem);/* 如果存在私有的释放回调函数的话,这时候调用,* ip4_frags 这个指针为空*/if (f->skb_free)f->skb_free(skb);  /* 最后调用kfree_skb释放 skb buffer */   kfree_skb(skb);
}

至此,分片处理的第一步已经完成,即保持分片所占用内存空间不超过阈值,再往下则是真正的处理过程,包括分片队列的查找、插入和重组。这个过程的分析放在下篇博客里。

tcp/ip 协议栈Linux源码分析一 IPv4分片报文重组分析一相关推荐

  1. tcp/ip 协议栈Linux源码分析三 IPv4分片报文重组分析三

    继续上篇,上次讲到了分片队列的查找操作,剩下的就是分片队列插入和重组两个部分了,这个也是分片重组的关键部分. 将收到的分片插入到分片队列是由函数inet_frag_queue()函数完成,这个函数比较 ...

  2. tcp/ip 协议栈Linux源码分析二 IPv4分片报文重组分析二

    继续接着上篇讲,之前我们说过,收到分片报文后首先会检查分片报文所占内存是否过大,如果超过阈值的话就要调用ip_evictor函数去释放一些旧的分片队列,关于如何释放分片队列资源上一篇已经总结完成,接下 ...

  3. tcp/ip 协议栈Linux源码分析五 IPv6分片报文重组分析一

    做防火墙模块的时候遇到过IPv6分片报文处理的问题是,当时的问题是netfilter无法基于传输层的端口拦截IPv6分片报文,但是IPv4的分片报文可以.分析了内核源码得知是因为netfilter的连 ...

  4. tcp/ip 协议栈Linux源码分析四 IPv4分片 ip_fragment函数分析

    内核版本:3.4.39 很多项目涉及到IP分片的时候都是绕过去了,感觉分片挺难的.但是老这么做也不行啊,抽空分析了内核的分片处理函数ip_fragment,也不是特别复杂,感觉挺简单的,看来事情只有实 ...

  5. linux源码分析之cpu初始化 kernel/head.s,linux源码分析之cpu初始化

    linux源码分析之cpu初始化 kernel/head.s 收藏 来自:http://blog.csdn.net/BoySKung/archive/2008/12/09/3486026.aspx l ...

  6. tcp/ip 协议栈Linux内核源码分析15 udp套接字接收流程二

    内核版本:3.4.39 上篇我们分析了UDP套接字如何接收数据的流程,最终它是在内核套接字的接收队列里取出报文,剩下的问题就是谁会去写入这个队列,当然,这部分工作由内核来完成,本篇剩下的文章主要分析内 ...

  7. TCP拥塞控制算法BBR源码分析

      BBR是谷歌与2016年提出的TCP拥塞控制算法,在Linux4.9的patch中正式加入.该算法一出,瞬间引起了极大的轰动.在CSDN上也有众多大佬对此进行分析讨论,褒贬不一.   本文首先对源 ...

  8. tcp/ip 协议栈Linux内核源码分析12 udp套接字发送流程一

    内核版本:3.4.39 因为过往的开发工作中既包括内核网络层模块的开发,又包括应用层程序的开发,所以对于网络数据的通信有那么一些了解.但是对于网络通信过程中,内核和应用层之间接口是如何运作的不是很清楚 ...

  9. tcp/ip 协议栈Linux内核源码分析14 udp套接字接收流程一

    内核版本:3.4.39 前面两篇文章分析了UDP套接字从应用层发送数据到内核层的处理流程,这里继续分析相反的流程,看看数据是怎么从内核送到应用层的. 与发送类似,内核也提供了多个接收数据的系统调用接口 ...

最新文章

  1. Dynamo 以及一致性哈希简介
  2. Science长文综述:通过空间斑图形成避免复杂系统崩溃
  3. python3 Number List 元组 字典 用法区分和总结
  4. 图像识别中卷积神经网络“卷积”的作用
  5. java IO(输入输出) 对象的序列化和反序列化
  6. 机器学习面试题(part2)
  7. WPF的binding
  8. figma下载_我如何使用Figma,CSS Grid和CSS Flexbox构建登录页面
  9. *【牛客 - 318B】签到题(单调栈,水题)
  10. matlab 刻度间隔,matlab – 地图的主要和次要刻度?
  11. NumPy基本操作快速熟悉
  12. PhpStorm 中切换PHP版本
  13. Kubernetes知识体系-从入门到精通
  14. C++ RTTI 简介
  15. python能自学成功吗-Python学习自学效果好吗?|老男孩Python人工智能培训
  16. 大整数相乘python fft_Python带你理解用于信号同步的CAZAC序列
  17. 昂达v891w可以用u盘linux,安卓、Win8随便用 昂达V891w双系统平板测试(转载)
  18. html怎么置顶图像,css怎么固定图片位置不变?
  19. c语言 printf 输出 long 整型
  20. [WARNING]: Could not match supplied host pattern, ignoring: servers

热门文章

  1. 常识:佛前三炷香是什么意思
  2. 鸿合一体机触屏没反应怎么办_无线鼠标没反应,我来教您无线鼠标没反应怎么办?...
  3. shiro权限管理_重量级课程发布~企业权限管理平台(SpringBoot2.0+Shiro+Vue)
  4. 【Paper】2019_Consensus Control of Multiple AUVs Recovery System Under Switching Topologies and Time D
  5. 【数理知识】标量函数、二次型函数、矩阵、正定负定半正定半负定
  6. 1.12 梯度的数值逼近-深度学习第二课《改善深层神经网络》-Stanford吴恩达教授
  7. 【问题】最近遇到的不大不小的arduino库使用问题
  8. deepin终端配置为英文
  9. 二维平面内无人机的路径规划——势场法-改进
  10. PHP代码静态分析工具PHPStan