UDP之数据报校验和


协议设计上,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 3

struct 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_CSUM2网络设备可以提供对基于IPv4的TCP和UDP数据包进行校验,其它协议报文不支持
NETIF_F_NO_CSUM4网络设备的传输非常可靠,无需L4执行任何校验,环回设备一般设置该标记
NETIF_F_HW_CSUM8网络设备可以对任何L4协议的数据包进行校验,基本很少有硬件能够实现
NETIF_F_IPV6_CSUM16网络设备可以对基于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;

	// 计算校验和,如果成功,那么最终结果应该是0
	sum = 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_UNNECESSARY
		skb->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时,会将校验和字段设置为全1
	if (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. 伪首部的校验和计算总是由软件自己完成的;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值