文章目录

  • 数据结构
    • sk_buff校验和字段
    • net_device校验和字段
  • 接收报文的校验和计算
    • udp4_csum_init()
    • udp_lib_checksum_complete()
    • 小结
  • 发送报文的校验和计算
    • skb的封装: ip_generic_getfrag()
    • udp_push_pending_frames()
      • udp4_hwcsum_outgoing()
      • udp_csum_outgoing()
    • 小结

协议设计上,UDP的校验和功能是可选的,Linux实现时,UDP的校验和功能默认是开启的,不过应用程序可以通过选项SO_NO_CHECK设置该能力。

校验和的计算本身是协议自己的事情,和硬件无关,但是往往为了更加的高效,可能硬件提供了部分或者全部的校验和功能,这就导致代码实现中,校验和相关的逻辑显得有些复杂。这篇笔记分析了UDP的校验和实现细节。

关于校验和API的使用见笔记linux网络校验和计算API。

数据结构

在sk_buff和net_device两个结构中,为校验和的计算增加了特定的字段。

sk_buff校验和字段

#define CHECKSUM_NONE 0
#define CHECKSUM_UNNECESSARY 1
#define CHECKSUM_COMPLETE 2
#define CHECKSUM_PARTIAL 3struct sk_buff
{union {__wsum      csum;struct {__u16  csum_start;__u16    csum_offset;};};__u8 ip_summed:2,
}

联合体中哪个成员有效取决于ip_summed的值,ip_summed共两个bit,可取四个值,它们在发送和接收过程中表示的含义还有所不同。

接收过程中,ip_summed字段包含了网络设备硬件告诉L4软件当前校验和的状态,各值含义如下:

  • CHECKSUM_NONE:硬件没有提供校验和,可能是硬件不支持,也可能是硬件校验出错但是并未丢弃数据包,这时L4软件需要自己进行校验和计算;
  • CHECKSUM_UNNECESSARY:硬件已经进行了完整的校验,软件无需再进行检查。这时L4软件会跳过校验和检查;
  • CHECKSUM_COMPLETE:硬件已经计算了L4报头和其payload部分的校验和,并将计算结果保存在了skb->csum中,L4软件只需要再计算伪报头即可;

发送过程中,ip_summed字段记录了L4软件想要告诉网络设备硬件关于当前数据包的校验和状态信心。各值含义如下:

  • CHECKSUM_NONE:L4软件已经对数据包进行了完整的校验,或者该数据包不需要校验。总之这种情况下网络设备硬件无需做任何校验和计算;
  • CHECKSUM_PARTIAL:L4软件计算了伪报头的校验和,并且将值保存在了数据报的L4层首部的check字段中,网络设备硬件需要计算其余部分的校验和(报文首部+数据部分)。硬件需要计算的报文范围是从skb->csum_start到报文最后一个字节,计算结果需要填写到(skb->csum_start + skb->csum_offset)处。

net_device校验和字段

net_device的feature字段定义了如下和校验和相关的标记,这些标记表明了硬件计算校验和的能力。

feature 含义
NETIF_F_IP_CSUM 2 网络设备可以提供对基于IPv4的TCP和UDP数据包进行校验,其它协议报文不支持
NETIF_F_NO_CSUM 4 网络设备的传输非常可靠,无需L4执行任何校验,环回设备一般设置该标记
NETIF_F_HW_CSUM 8 网络设备可以对任何L4协议的数据包进行校验,基本很少有硬件能够实现
NETIF_F_IPV6_CSUM 16 网络设备可以对基于IPv6的TCP和UDP数据包进行校验,其它协议报文不支持

根据上述基础值重新定义了如下几个flag:

#define NETIF_F_GEN_CSUM (NETIF_F_NO_CSUM | NETIF_F_HW_CSUM)
#define NETIF_F_V4_CSUM     (NETIF_F_GEN_CSUM | NETIF_F_IP_CSUM)
#define NETIF_F_V6_CSUM     (NETIF_F_GEN_CSUM | NETIF_F_IPV6_CSUM)
#define NETIF_F_ALL_CSUM    (NETIF_F_V4_CSUM | NETIF_F_V6_CSUM)

注:这些概念和字段的含义同样适用于TCP校验和处理过程

接收报文的校验和计算

udp4_csum_init()

UDP接收到报文后,首先会调用该函数进行校验和检查。

/* Initialize UDP checksum. If exited with zero value (success),* CHECKSUM_UNNECESSARY means, that no more checks are required.* Otherwise, csum completion requires chacksumming packet body,* including udp header and folding it to skb->csum.*/
static inline int udp4_csum_init(struct sk_buff *skb, struct udphdr *uh, int proto)
{const struct iphdr *iph;int err;// 这两个字段用于指示对报文的哪些部分进行校验,cov指coverage,// 只有UDPLite使用,对于UDP,会对整个报文进行校验UDP_SKB_CB(skb)->partial_cov = 0;UDP_SKB_CB(skb)->cscov = skb->len;// UDPLITE,忽略if (proto == IPPROTO_UDPLITE) {err = udplite_checksum_init(skb, uh);if (err)return err;}iph = ip_hdr(skb);if (uh->check == 0) {// UDP首部校验和字段为0,这种情况说明已经处理过了,设置为CHECKSUM_UNNECESSARY,// 后续无需再进行处理skb->ip_summed = CHECKSUM_UNNECESSARY;} else if (skb->ip_summed == CHECKSUM_COMPLETE) {// 还有伪首部需要校验,所以添加伪首部校验,如果校验成功,设置为CHECKSUM_UNNECESSARY// csum_tcpudp_magic()计算伪首部校验和+skb->csum后返回新的校验和,返回0说明校验结果正确if (!csum_tcpudp_magic(iph->saddr, iph->daddr, skb->len, proto, skb->csum))skb->ip_summed = CHECKSUM_UNNECESSARY;}// 如果经过上面处理后发现仍然需要校验,则先只计算伪首部校验和,// 并将结果放入到skb->csum中if (!skb_csum_unnecessary(skb))skb->csum = csum_tcpudp_nofold(iph->saddr, iph->daddr,skb->len, proto, 0);return 0;
}// 在接收方向上,CHECKSUM_UNNECESSARY底层已经对数据包进行了校验,无需再进行校验和计算
static inline int skb_csum_unnecessary(const struct sk_buff *skb)
{return skb->ip_summed & CHECKSUM_UNNECESSARY;
}

如上,如果硬件没有参与校验,在udp4_csum_init()中是只做了伪首部校验的,完整的校验工作在udp_lib_checksum_complete()中完成。

udp_lib_checksum_complete()

// 返回0表示校验成功
static inline int udp_lib_checksum_complete(struct sk_buff *skb)
{// 如果需要校验则调用__udp_lib_checksum_complete()进行校验return !skb_csum_unnecessary(skb) &&__udp_lib_checksum_complete(skb);
}/**    Generic checksumming routines for UDP(-Lite) v4 and v6*/
static inline __sum16 __udp_lib_checksum_complete(struct sk_buff *skb)
{// 增加一个需要校验的长度字段,对于UDP,该字段就是整个报文长度return __skb_checksum_complete_head(skb, UDP_SKB_CB(skb)->cscov);
}__sum16 __skb_checksum_complete_head(struct sk_buff *skb, int len)
{__sum16 sum;// 计算校验和,如果成功,那么最终结果应该是0sum = csum_fold(skb_checksum(skb, 0, len, skb->csum));if (likely(!sum)) {// 为什么CHECKSUM_COMPLETE说明伪首部校验失败了,见udp4_csum_init()if (unlikely(skb->ip_summed == CHECKSUM_COMPLETE))netdev_rx_csum_fault(skb->dev);// 设置校验和状态为CHECKSUM_UNNECESSARYskb->ip_summed = CHECKSUM_UNNECESSARY;}return sum;
}

小结

接收过程中,重点在于检查数据报的校验结果是否为0,如果为0,则说明传输过程没有问题,否则为错误报文。

发送报文的校验和计算

首先,udp报文在通过ip_append_data()封装skb时,对skb中的校验和相关字段进行了初始化,相关代码如下:

int ip_append_data(xxx)
{...int csummode = CHECKSUM_NONE;
.../** transhdrlen > 0 means that this is the first fragment and we wish* it won't be fragmented in the future.*/// 1)第一个IP片段;2)本次封装数据长度小于MTU;3)硬件支持校验和能力;4)无扩展头;if (transhdrlen &&length + fragheaderlen <= mtu &&rt->u.dst.dev->features & NETIF_F_V4_CSUM &&!exthdrlen)csummode = CHECKSUM_PARTIAL;
...while (length > 0) {...if (copy <= 0) {// skb刚被分配后,设置初始值skb->ip_summed = csummode;skb->csum = 0;...// 数据被封装到skb后,重新对csummode赋值csummode = CHECKSUM_NONE;}}
...
}

上述逻辑的效果就是:只有第一个IP片段满足一定条件下时,其skb->ip_summed字段才有可能被设置为CHECKSUM_PARTIAL;其它IP片段的skb->ip_summed均为CHECKSUM_NONE。

skb的封装: ip_generic_getfrag()

在数据封装过程中,会根据skb->ip_summed的设置情况,计算计算校验和。

int ip_generic_getfrag(void *from, char *to, int offset, int len, int odd, struct sk_buff *skb)
{struct iovec *iov = from;if (skb->ip_summed == CHECKSUM_PARTIAL) {// CHECKSUM_PARTIAL情况硬件会计算数据包除伪首部外部分的校验和,所以拷贝过程中无需计算校验和if (memcpy_fromiovecend(to, iov, offset, len) < 0)return -EFAULT;} else {// 硬件无法帮忙,在拷贝过程中,将数据内容的校验和结果计算出来并保存在skb->csum中__wsum csum = 0;if (csum_partial_copy_fromiovecend(to, iov, offset, len, &csum) < 0)return -EFAULT;skb->csum = csum_block_add(skb->csum, csum, odd);}return 0;
}

udp_push_pending_frames()

随后,udp在构造首部时,会根据skb->ip_summed的赋值情况计算校验和。

#define CSUM_MANGLED_0 ((__force __sum16)0xffff)static int udp_push_pending_frames(struct sock *sk)
{...__wsum csum = 0;...if (is_udplite) /* UDP-Lite */csum  = udplite_csum_outgoing(sk, skb);else if (sk->sk_no_check == UDP_CSUM_NOXMIT) {   /* UDP csum disabled */// UDP发送校验和被关闭了,重新设置skb->ip_summed,告诉硬件无需计算校验和skb->ip_summed = CHECKSUM_NONE;goto send;} else if (skb->ip_summed == CHECKSUM_PARTIAL) { /* UDP hardware csum */// 硬件计算校验和,需要将skb->csum_start和skb->csum_offset设置正确udp4_hwcsum_outgoing(sk, skb, fl->fl4_src,fl->fl4_dst, up->len);goto send;} else /*   `normal' UDP    */// 正常情况下,UDP需要完成所有的校验和计算工作csum = udp_csum_outgoing(sk, skb);// 在csum的基础上累加伪首部校验和uh->check = csum_tcpudp_magic(fl->fl4_src, fl->fl4_dst, up->len,sk->sk_protocol, csum);// 最终的校验和结果为0时,会将校验和字段设置为全1if (uh->check == 0)uh->check = CSUM_MANGLED_0;
send:
...
}

udp4_hwcsum_outgoing()

当ip_append_data()在IP报文的第一个分段上设置skb->ip_summed==CHECKSUM_PARTIAL时,UDP会使用该函数为硬件执行校验和计算做准备。

/***     udp4_hwcsum_outgoing  -  handle outgoing HW checksumming*   @sk:   socket we are sending on*   @skb:  sk_buff containing the filled-in UDP header*            (checksum field must be zeroed out)*/
static void udp4_hwcsum_outgoing(struct sock *sk, struct sk_buff *skb,__be32 src, __be32 dst, int len      )
{unsigned int offset;struct udphdr *uh = udp_hdr(skb);__wsum csum = 0;if (skb_queue_len(&sk->sk_write_queue) == 1) {// 对于不需要分段的UDP报文,计算伪报文,并且设置skb->csum_start和skb->csum_offset,// 可以看到,skb->csum_start是相对于skb->head指针的偏移/** Only one fragment on the socket.*/skb->csum_start = skb_transport_header(skb) - skb->head;skb->csum_offset = offsetof(struct udphdr, check);uh->check = ~csum_tcpudp_magic(src, dst, len, IPPROTO_UDP, 0); // 为何要取反?} else {/** HW-checksum won't work as there are two or more* fragments on the socket so that all csums of sk_buffs* should be together*/// 如果UDP报文需要分段,那么即使硬件有计算校验和的能力,这里也会用软件校验。// 遍历所有的IP分段,重新计算校验和(如果应用程序使用MSG_MORE发送数据,并且第一次// 写操作的数据量小于一个IP片段,就会出现这种情况)。offset = skb_transport_offset(skb);skb->csum = skb_checksum(skb, offset, skb->len - offset, 0);// 告诉硬件无需再计算校验和skb->ip_summed = CHECKSUM_NONE;skb_queue_walk(&sk->sk_write_queue, skb) {csum = csum_add(csum, skb->csum);}uh->check = csum_tcpudp_magic(src, dst, len, IPPROTO_UDP, csum);if (uh->check == 0)uh->check = CSUM_MANGLED_0;}
}

udp_csum_outgoing()

当UDP报文的所有内容的校验和都需要有软件来计算时,会使用该函数将除伪首部外的UDP报文部分校验和计算出来。

/***     udp_csum_outgoing  -  compute UDPv4/v6 checksum over fragments*     @sk:   socket we are writing to*   @skb:  sk_buff containing the filled-in UDP header*            (checksum field must be zeroed out)*/
static inline __wsum udp_csum_outgoing(struct sock *sk, struct sk_buff *skb)
{// 计算UDP首部校验和__wsum csum = csum_partial(skb_transport_header(skb),sizeof(struct udphdr), 0);// 累加所有IP片段的数据部分校验和,这些片段的校验和在ip_append_data()封装skb过程中// 就已经被保存在了skb->csum中skb_queue_walk(&sk->sk_write_queue, skb) {csum = csum_add(csum, skb->csum);}return csum;
}

小结

从上面代码实现可以看出,发送流程中,UDP校验和的处理有如下几个关键点:

  1. 只有当UDP报文的数据可以用一个IP片段发送出去,而且硬件支持校验和计算时,UDP才会将校验和计算的任务交给硬件完成;
  2. 伪首部的校验和计算总是由软件自己完成的;

UDP之数据报校验和相关推荐

  1. linux 内核协议栈 UDP数据报校验和

    目录 1 校验和相关字段 1.1 struct sk_buff 1.2 struct net_device 2 收包流程数据报的校验和计算 __udp4_lib_rcv() 2.1 udp4_csum ...

  2. 计算机网络|UDP用户数据报服务

    用户数据报UDP UDP概述:用户是运输层的协议,UPD的主要特点是: 1.UPD是无连接的:即发送数据前不需要建立连接,因此减少了开销和发送数据之前的时延. 2.UDP是尽最大努力交付的,即不保证交 ...

  3. java实现ip首部校验和算法,理解传输层中UDP协议首部校验和以及校验和计算方法的Java实现...

    UDP,全称User Datagram Protocol,用户数据报协议,是TCP/IP四层参考模型中传输层的一种面向报文的.无连接的.不能保证可靠的.无拥塞控制的协议.UDP协议因为传输效率高,常用 ...

  4. 一个udp用户数据报的数据字段为8192_基于FPGA的千兆网UDP通信分析

    千兆网UDP通信 以太网帧格式 图8‑12 以太网帧格式 表8‑5 以太网帧格式说明 UDP协议分析 为什么UDP协议在FPGA实现时很受欢迎,最主要一个原因就是简单,简答到什么地步呢?UDP协议只是 ...

  5. 【计算机网络】——习题解析:UDP 用户数据报的首部十六进制表示是:06 32 00 45 00 1C E2 17,试求源端口、目的端口、 用户数据报的总长度、数据部分长度等

    UDP 用户数据报的首部十六进制表示是:06 32 00 45 00 1C E2 17,试求源端口.目的端口. 用户数据报的总长度.数据部分长度.这个用户数据报是从客户发送给服务器发送给客户?使用 U ...

  6. linux内核协议栈 UDP之数据报接收过程

    UDP报文接收概述 UDP数据报的接收要分两部分来看: 网络层接收完数据包后递交给UDP后,UDP的处理过程.该过程UDP需要做的工作就是接收数据包并对其进行校验,校验成功后将其放入接收队列 sk_r ...

  7. 互联网协议 — UDP 用户数据报协议

    目录 文章目录 目录 UDP 协议 UDP Header UDP 协议 UDP(User Datagram Protocol,用户数据报协议),是一种无连接的.非可靠的传输层协议.换句话说,当 UDP ...

  8. udp tcp ip 校验和对比

    首先TCP,UDP,IP三个协议中校验和都占16位,也就是两个字节. UDP的校验和结算相对简单.首先UDP报文长度不是确定的,所以计算校验和前要先将报文的末尾用'0'补齐.使报文为偶数个字节. 发送 ...

  9. TCP/IP UDP用户数据报协议 运输层

    快速导航 UDP在TCP/IP中的位置? 用户数据报的格式? UDP协议对数据进行封装和解封? 队列与端口实现? 复用与分用? UDP的一些特点? UDP输入队列? 输入模块伪代码欣赏? User D ...

最新文章

  1. C# 使用HttpWebRequest提交ASP.NET表单并保持Session和Cookie
  2. 创建自己的人脸识别系统
  3. 李宗纯:图机器学习在度小满风控中的应用
  4. Git - 使用指南
  5. ThinkPHP5下自己写日志
  6. 内核控制Meta标签:让360浏览器默认使用极速模式打开网页(转)
  7. 163相册密码破解 - 简单版
  8. 怎样下载python模块sublime text3中_python安装环境配置、python模块添加、sublime text编辑器配置...
  9. 学习可以借鉴的大牛们的网站
  10. 单线程与线程池的性能对比
  11. android 播放音乐媒体文件(二)
  12. java详细设计模式有代码
  13. c语言编程分数化简,C语言编程实例:将真分数分解为埃及分数
  14. 【Android 逆向】Android 逆向通用工具开发 ( PC 端工具 hacktool 启动 main 函数分析 | hacktool 工程中的核心类 HackCommand 分析 )
  15. Linux下格式化sd卡和重新分区
  16. 关于fai值导入程序的思考
  17. 可长时间佩戴的耳机真的存在吗?骨传导耳机是否对耳朵伤害更小?
  18. IOS下载资源zip到本地然后读取
  19. 2.11 求N分之一序列前N项和
  20. 从邮箱到云客服,SaaS行业发展到哪种程度了?

热门文章

  1. 使用AZ3166(MXChip IoT DevKit)开发translator
  2. SimpleDateFormat 的使用及其 注意事项
  3. bzoj3166: [Heoi2013]Alo
  4. 契约测试(中):利用PACT做契约测试
  5. AMD 双核驱动、补丁、优化工具下载地址和安装方法
  6. dreamweaver html语言,Dreamweaver网页设计与制作(HTML+CSS+JavaScript)
  7. 希捷SSHD通电不转的数据恢复方法
  8. 一些有用的Web或者手机UI设计工具
  9. 新基建下的城轨,城市群功能还能如何被提升?
  10. usb万能驱动win7_IT知识大全:驱动程序详解!