目录

1 分片位置

2 ip_fragment()代码分析


1 分片位置

分段是网络层的一个重要任务,网络层需要对两方面的IP数据包进行分段:

  1. 本地产生的数据包;
  2. 转发的数据包;

这两种数据包的长度如果超过了出口设备的MTU(或者PMTU),则网络层必须先对数据包进行分段,使其适配出口设备的MTU。

IPv4使用 ip_fragment() 处理分段,在设计时,要求该函数能够处理所有的情况,但是在实现过程中,充分考虑了实际可能的情况,对某些场景的处理进行了优化,下面分情况介绍。

对于本机发送的数据包,TCP在组织skb数据时,本身就会考虑MTU的限制,它会尽可能的保证每个skb携带的数据不会超过MTU,就是为了避免网络层再进行分段,因为分段对TCP性能的影响较大。因为TCP就帮忙做了很多事情,所以对于TCP发送场景,应该是很少有机会执行分片的。考虑UDP,它并不会向TCP一样保证skb长度,但是由于UDP往往是调用ip_append_data()组织skb数据的,该函数在组织skb过程中,会将属于同一个IP报文的所有分片都组织成skb列表(非第一个分片都放在第一个分片skb的frag_list中),这样网络层在执行分片时将会节省很多工作量。

对于转发的数据包,则无法向本地发送一样,提前做很多的工作,网络层必须依靠自己来兼容所有可能的情况。同样的,天有不测风云,对于一些特殊的异常场景,本机发送的数据包也有可能并没有按照预期情况组织,这时网络层也要能够兼容处理。

综上,网络层在实现分段时,设计了快速路径和慢速路径两个流程来分别对应上面的两种情况。通常本地产生的UDP大包都会调用 ip_push_pending_frames 将发包队列的skb整合到 frag_list 上,走快速路径分发出去。

2 ip_fragment()代码分析

/**  This IP datagram is too large to be sent in one piece.  Break it up into*   smaller pieces (each of size equal to IP header plus*   a block of the data of the original IP data part) that will yet fit in a*   single device frame, and queue such a frame for sending.*/
int ip_fragment(struct sk_buff *skb, int (*output)(struct sk_buff*))
{struct iphdr *iph;int raw = 0;int ptr;struct net_device *dev;struct sk_buff *skb2;unsigned int mtu, hlen, left, len, ll_rs, pad;int offset;__be16 not_last_frag;struct rtable *rt = (struct rtable*)skb->dst;int err = 0;dev = rt->u.dst.dev;/** Point into the IP datagram header.*/iph = ip_hdr(skb);// 一旦进入该函数,说明skb过大,需要进行IP分片,但是又设置了DF标记或者本身是抑制分片的,// 那么发送失败,向源端发送ICMP报文,这里主要是为forward数据所判断。if (unlikely((iph->frag_off & htons(IP_DF)) && !skb->local_df)) {IP_INC_STATS(IPSTATS_MIB_FRAGFAILS);icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED, htonl(ip_skb_dst_mtu(skb)));kfree_skb(skb);return -EMSGSIZE;}// hlen保存IP首部长度hlen = iph->ihl * 4;// mtu代表每个IP片段能够容纳的L4载荷,所以需要在MTU基础上去掉IP首部的开销mtu = dst_mtu(&rt->u.dst) - hlen;// 设置IPCB(skb)->flags |= IPSKB_FRAG_COMPLETE;/* When frag_list is given, use it. First, check its validity:* some transformers could create wrong frag_list or break existing* one, it is not prohibited. In this case fall back to copying.** LATER: this step can be merged to real generation of fragments,* we can switch to copy when see the first bad fragment.*//** 如果4层将数据包分片了,那么就会把这些数据包放到skb的frag_list链表中,* 因此这里首先先判断frag_list链表是否为空,为空的话将会进行slow 分片*/if (skb_shinfo(skb)->frag_list) {struct sk_buff *frag;/** 取得第一个数据报的len.当sk_write_queue队列被flush后,* 除了第一个切好包的另外的包都会加入到frag_list中(接口ip_push_pending_frames),* 而这里需要得到的第一个包(也就是本身这个sk_buff)的长度。* first_len是第一个skb中所有数据的总长度,包括线性缓冲区和frags[]数组*/int first_len = skb_pagelen(skb);int truesizes = 0;/** 接下来的判断都是为了确定能进行fast分片。分片不能被共享,* 这是因为在fast path 中,需要加给每个分片不同的ip头(而并* 不会复制每个分片)。因此在fast path中是不可接受的。而在* slow path中,就算有共享也无所谓,因为他会复制每一个分片,* 使用一个新的buff。   *//** 判断第一个包长度是否符合一些限制(包括mtu,mf位等一些限制).* 如果第一个数据报的len没有包含mtu的大小这里之所以要把第一个* 切好片的数据包单独拿出来检测,是因为一些域是第一个包所独有* 的(比如IP_MF要为1)。这里由于这个mtu是不包括hlen的mtu,因此* 需要减去一个hlen。  */// 1. 条件1说明高层协议切割的skb的长度还是太长了;// 2. 第一个片段长度必须是8字节对齐的(IP片段偏移量是8字节对齐决定的);// 3. 偏移量在下面的分段过程中才会进行设置,这里不应该有值;// 4. skb不能是被共享的,因为快速路径上不会进行skb拷贝,而是直接修改skb;// 上述4个条件有任何一个不满足,那么就用慢速路径完成分片if (first_len - hlen > mtu || ((first_len - hlen) & 7) ||(iph->frag_off & htons(IP_MF|IP_OFFSET)) || skb_cloned(skb)){goto slow_path;}// 遍历frag_list列表,检查是否所有的分片是否符合快速分片处理for (frag = skb_shinfo(skb)->frag_list; frag; frag = frag->next) {// 这4个条件和前面对第一个片段的检查类似if (frag->len > mtu || ((frag->len & 7) && frag->next) || skb_headroom(frag) < hlen)goto slow_path;if (skb_shared(frag))goto slow_path;BUG_ON(frag->sk);// 设置skb的属主if (skb->sk) {sock_hold(skb->sk);frag->sk = skb->sk;frag->destructor = sock_wfree;truesizes += frag->truesize;}}// 对第一个分片的特殊处理err = 0;offset = 0;frag = skb_shinfo(skb)->frag_list;skb_shinfo(skb)->frag_list = NULL;skb->data_len = first_len - skb_headlen(skb);skb->truesize -= truesizes;skb->len = first_len;iph->tot_len = htons(first_len);iph->frag_off = htons(IP_MF);ip_send_check(iph);// 循环处理后面所有的分片for (;;) {if (frag) {frag->ip_summed = CHECKSUM_NONE;skb_reset_transport_header(frag);__skb_push(frag, hlen);skb_reset_network_header(frag);// 拷贝IP首部memcpy(skb_network_header(frag), iph, hlen);iph = ip_hdr(frag);iph->tot_len = htons(frag->len);ip_copy_metadata(frag, skb);if (offset == 0)ip_options_fragment(frag);offset += skb->len - hlen;iph->frag_off = htons(offset>>3);if (frag->next != NULL)iph->frag_off |= htons(IP_MF);/* Ready, complete checksum */ip_send_check(iph);}// 继续发送过程,先发送首片ip的skb自己,后面继续发送frag。err = output(skb);if (!err)IP_INC_STATS(IPSTATS_MIB_FRAGCREATES);if (err || !frag)break;// 继续处理下一个分片skb = frag;frag = skb->next;skb->next = NULL;}// 一切正常,分片过程从这里返回if (err == 0) {IP_INC_STATS(IPSTATS_MIB_FRAGOKS);return 0;}// 分片或者发送过程失败了,释放所有的skb分片while (frag) {skb = frag->next;kfree_skb(frag);frag = skb;}// 快速路径结束IP_INC_STATS(IPSTATS_MIB_FRAGFAILS);return err;}//end of (skb_shinfo(skb)->frag_list)slow_path:// left保存整个IP报文中剩余需要分段的报文长度,在下面分段过程中会逐渐减小,// 直到为0说明分段过程结束left = skb->len - hlen;      /* Space per frame */// ptr指向L4载荷的偏移,初始值指向L4报文的开头ptr = raw + hlen;     /* Where to start from *//* for bridged IP traffic encapsulated inside f.e. a vlan header,* we need to make room for the encapsulating header*/// L2的特殊使用,处理桥接、VLAN、PPPOE相关MTU,这里认为pad=0即可pad = nf_bridge_pad(skb);// link layer reserved space,即链路层保留长度,是指应该在skb线性缓冲区的首部// 应该为L2保留的长度,主要包括mac层首部ll_rs = LL_RESERVED_SPACE_EXTRA(rt->u.dst.dev, pad);mtu -= pad;// offset为偏移量,不包括DF、MF标记offset = (ntohs(iph->frag_off) & IP_OFFSET) << 3;// 顾名思义,标识是否是IP报文的最后一个片段,因为最后一个片段的MF标记是0not_last_frag = iph->frag_off & htons(IP_MF);// 循环创建新的skb2,然后拷贝数据,完成分段(left==0)while (left > 0) {// 调整len为本轮循环分片中能够容纳的L4报文载荷长度len = left;/* IF: it doesn't fit, use 'mtu' - the data space left */if (len > mtu)len = mtu;// IP报文首部的片偏移字段格式决定了非最后一个IP片段的偏移量必须是8字节对齐的if (len < left)   {len &= ~7;}// 分配skb2,长度包括三部分:len代表的L4报文部分;hlen代表的IP报文首部;ll_rs代表的L2报文首部if ((skb2 = alloc_skb(len+hlen+ll_rs, GFP_ATOMIC)) == NULL) {NETDEBUG(KERN_INFO "IP: frag: no memory for new fragment!\n");err = -ENOMEM;goto fail;}// 首先设置skb2中的各个字段ip_copy_metadata(skb2, skb);skb_reserve(skb2, ll_rs);skb_put(skb2, len + hlen);skb_reset_network_header(skb2);skb2->transport_header = skb2->network_header + hlen;// 设置owner,内存的消耗将会记录到owner的账上if (skb->sk)skb_set_owner_w(skb2, skb->sk);// 先从源skb的线性缓冲区将IP报文首部拷贝到skb2的线性缓冲区,因为内存上是连续的,// 所以直接使用memcpy拷贝即可skb_copy_from_linear_data(skb, skb_network_header(skb2), hlen);// 下面的数据拷贝就复杂了一些,因为慢速路径要能够处理任何可能的skb的数据组织方式。// skb_copy_bits()将从skb->data开始偏移的ptr位置开始拷贝数据,共拷贝len字节到// skb2传输层开始的位置,注意这里在处理ptr偏移会考虑页缓冲区和frag_list两种情况if (skb_copy_bits(skb, ptr, skb_transport_header(skb2), len))BUG();// 分段完成,left减去1个片段的长度left -= len;// 填充IP首部,主要是选项和偏移字段iph = ip_hdr(skb2);iph->frag_off = htons((offset >> 3));// ip_options_fragment()会在第一个IP片段的基础上将后面片段不需要的IP选项删除,// 这样后面的IP片段直接继承即可(前面已经拷贝),无需重新设定选项,所以这里只// 在第一个片段分片过程中调用ip_options_fragment()if (offset == 0)ip_options_fragment(skb);//不是最后一个包,因此设置mf位 if (left > 0 || not_last_frag)iph->frag_off |= htons(IP_MF);// 更新ptr和offset,为下一个分片做好准备ptr += len;offset += len;// IP首部的total字段表示的是IP片段的长度iph->tot_len = htons(len + hlen);// 计算IP首部校验和ip_send_check(iph);// 调用发送接口继续发送过程err = output(skb2);if (err)goto fail;IP_INC_STATS(IPSTATS_MIB_FRAGCREATES);}// 原有的IP报文都已经被分割成一个个新的skb,所以处理结束后,原来的skb需要释放kfree_skb(skb);IP_INC_STATS(IPSTATS_MIB_FRAGOKS);return err;
fail:kfree_skb(skb);IP_INC_STATS(IPSTATS_MIB_FRAGFAILS);return err;
}

linux协议栈 IPv4之发送过程中的分段处理ip_fragment()相关推荐

  1. linux内核协议栈 IPv4之发送接口 ip_queue_xmi()【TCP使用】

    ip_queue_xmi() 接口主要由tcp使用,主要就干了两件事: 如果还没有查询过路由,那么查路由: 构造IP首部,然后调用IPv4协议内部的接口继续发送: 注意:该函数并没有处理分段,所以调用 ...

  2. Linux 系统 uos / deepin 系统安装过程中 最全常用命令及问题 总结

    本博客已暂停更新,为了您更好的阅读,请转至新博客https://www.whbwiki.com/1296.html 此教程用来解决uos/deepin系统安装过程中的一系列问题,其他linux发行版适 ...

  3. WSL(windows subsystem for linux)安装错误:安装过程中遇到错误,但可以继续安装。组件: ‘WSL 内核‘ 错误代码: 0x80072f78解决方法

    文章目录 问题来源 解决 参考 问题来源 使用管理员身份打开powershell,输入 wsl --install 之后等待安装: 结果,出现如下问题: PS C:\WINDOWS\system32& ...

  4. 高通android 7.0彩信发送过程中使用到的google pdu

    对于彩信与数据库的交互操作,google并没有将这部分代码放在Mms中,而是放在framework中的pdu部分. 具体代码路径是在:opt\telephony\src\java\com\google ...

  5. Mosquitto 在Linux服务器上的部署过程中以及踩过的坑

    1. wget http://mosquitto.org/files/source/mosquitto-1.4.9.tar.gz 2.tar zxfv mosquitto-1.4.9.tar.gz 2 ...

  6. 测试录制的电话拨码声音信号在发送过程中的问题

    简 介: 对比了实际录制的手机音频信号的波形以及频谱,可以看到,录制双音频信号出现了较大的变化: 拨码的幅值降低了很多,需要放大20倍才能够与录制的双音频相当: 双音频中的高频区的分量很弱,几乎看不到 ...

  7. oracle oui25031 linux,搭建Oracle 10g RAC过程中出现 OUI-25031 的解决办法

    解决办法如下: OUI-25031错误处理:手动配置VIPCA 手动修改vipca设置(两节点都要执行) [root@HHDB20 ~]# cd /u01/app/Oracle/crs/bin/ [r ...

  8. 拆解 Linux 网络包发送过程

    半年前我以源码的方式描述了网络包的接收过程.之后不断有粉丝提醒我还没聊发送过程呢.好,安排! 在开始今天的文章之前,我先来请大家思考几个小问题. 问1:我们在查看内核发送数据消耗的 CPU 时,是应该 ...

  9. 图解分析 Linux 网络包发送过程

    大家好,下面的文章转发一个鹅厂同学的文章,这篇文章从应用到内核,写的非常不错,希望大家分析某个技术也可以从这方面入手. ----- 大家好,我是飞哥! 半年前我以源码的方式描述了网络包的接收过程.之后 ...

最新文章

  1. 【转】PHP获取重定向URL的几种方法
  2. 将C/C++程序的变量数据导入到MATLAB中的方法
  3. Spring和shiro整合 logout 配置方式
  4. Java 网络 socket 编程
  5. 磁盘里竟然还有这个东西!多亏这个1.5M大小的神器工具发现了它
  6. 人工智能和机器学习的前世今生
  7. Ubuntu被曝严重漏洞:切换系统语言+输入几行命令,就能获取root权限(仅支持ubuntu桌面版、提权)
  8. 来吧,1分钟带你玩转Kafka
  9. SpringBoot 利用过滤器Filter修改请求url地址
  10. SM系列国密算法(转)
  11. RHEL/Centos下VSFTPD服务器搭建
  12. Qt:Exception at 0xeefde9, code:0x0000005: read access violation at: 0x0, flags = 0x0(first chance)
  13. php微信公众平台关注后欢迎语的设置,关注公众号的欢迎语怎么设置?公众号欢迎语怎么加链接?...
  14. Cadence制作flash焊盘时找不到
  15. 无线鼠标,滚轮不灵,迟钝多转卡怎么办
  16. Spring DI和AOP简介(一)
  17. esp8266基于arduino一键配网掉电保存WIFI账号密码
  18. 微信隐藏的功能和技巧
  19. python Linux下的安装
  20. 大数据分析软件都有哪些平台?

热门文章

  1. 8DOER: Dual Cross-Shared RNN for Aspect Term-Polarity Co-Extraction(2020.10.22)
  2. 倭黑猩猩机器人_科学美国人60秒:倭黑猩猩妈妈监管儿子的私生活
  3. 洛伦兹力的matlab求解,问:由安培力推导洛伦兹力的过程?
  4. 有人不理解,有人不屑,到底什么是UXD
  5. ubuntu可爱的玩具:小猫咪 oneko
  6. ghost linux引导修复工具,GhostBSD 19.10 发布,UEFI多重引导的修复
  7. Field error in object ‘xxx‘ on field ‘xxx‘: rejected value [xxx]
  8. javaMailSender 发送邮件设置昵称
  9. 长沙理工大学第十二届ACM大赛-重现赛
  10. 自定义控件---继承ViewGroup类方式(循序渐进之第3步效果----图片左右拖动+RadioGroup切换效果)