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

目录

1 校验和相关字段

1.1 struct sk_buff

1.2 struct net_device

2 收包流程数据报的校验和计算 __udp4_lib_rcv()

2.1 udp4_csum_init()

2.2 udp_lib_checksum_complete()

3 发包流程数据报的校验和计算 udp_send_skb()

3.1 硬件校验和 udp4_hwcsum()

3.2 UDP软件校验和 udp_csum()

3.3 UDPLITE校验和 udplite_csum()

3.4 通用校验和计算 skb_checksum()


1 校验和相关字段

由于目前很多网卡设备是支持对L4层数据包进行校验和的计算和验证的,所以在L4协议软件的实现中,会根据网卡的支持情况作不同的处理,为此内核在 struct sk_buff 结构和 struct net_device中增加了校验和相关的参数,如下:

1.1 struct sk_buff

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收到数据包后如果检查ip_summed是这种情况,就可以跳过校验过程;
  • CHECKSUM_COMPLETE:硬件已经校验了L4报头和其payload部分,并且校验和保存在了csum中,L4软件只需要再计算伪报头然后检查校验结果即可。

在发包流程中,ip_summed字段包含了L4软件告诉设备驱动程序当前校验和的状态,各取值含义如下:

  • CHECKSUM_NONE:L4软件已经进行了校验,硬件无需做任何事情;
  • CHECKSUM_PARTIAL:L4软件计算了伪报头,并且将值保存在了首部的check字段中,硬件需要计算其余部分的校验和。

1.2 struct net_device

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

feature含义
NETIF_F_NO_CSUM    该设备非常可靠,无需L4执行任何校验,环回设备一般设置该标记
NETIF_F_IP_CSUM    设备可以对基于IPv4的TCP和UDP数据包进行校验
NETIF_F_IPV6_CSUM   设备可以对基于IPv6的TCP和UDP数据包进行校验
NETIF_F_HW_CSUM    设备可以对任何L4协议的数据包进行校验

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

2 收包流程数据报的校验和计算 __udp4_lib_rcv()

2.1 udp4_csum_init()

@skb: 待校验的数据报
@uh:该数据报的UDP首部
@proto:L4协议号,为IPPROTO_UDP或者IPPROTO_UDPLITE
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);
	//UDP首部校验和字段为0,这种情况说明已经处理过了,设置为CHECKSUM_UNNECESSARY,后续无需再进行处理
	if (uh->check == 0) {
		skb->ip_summed = CHECKSUM_UNNECESSARY;
	} else if (skb->ip_summed == CHECKSUM_COMPLETE) {
		//还有伪首部需要校验,所以添加伪首部校验,如果校验成功,设置为CHECKSUM_UNNECESSARY
		//csum_tcpudp_magic()计算伪首部校验和后进行验证,如果验证ok,返回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表示校验ok,无需再进行校验和计算
static inline int skb_csum_unnecessary(const struct sk_buff *skb)
{
	return skb->ip_summed & CHECKSUM_UNNECESSARY;
}

2.2 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时是校验失败???
		if (unlikely(skb->ip_summed == CHECKSUM_COMPLETE))
			netdev_rx_csum_fault(skb->dev);
		//设置校验和状态为CHECKSUM_UNNECESSARY
		skb->ip_summed = CHECKSUM_UNNECESSARY;
	}
	return sum;
}

3 发包流程数据报的校验和计算 udp_send_skb()

static int udp_send_skb(struct sk_buff *skb, struct flowi4 *fl4)
{
	struct sock *sk = skb->sk;
	struct inet_sock *inet = inet_sk(sk);
	struct udphdr *uh;
	int err = 0;
	int is_udplite = IS_UDPLITE(sk);
	int offset = skb_transport_offset(skb);
	int len = skb->len - offset;
	__wsum csum = 0;

	/*
	 * Create a UDP header
	 */
	uh = udp_hdr(skb);
	uh->source = inet->inet_sport;
	uh->dest = fl4->fl4_dport;
	uh->len = htons(len);
	uh->check = 0;

	if (is_udplite)  				 /*     UDP-Lite      */
		csum = udplite_csum(skb);

	else if (sk->sk_no_check_tx) {			 /* UDP csum off */
		skb->ip_summed = CHECKSUM_NONE; //用户可通过setsockopt(SO_NO_CHECK)系统调用关闭校验和计算
		goto send;

	} else if (skb->ip_summed == CHECKSUM_PARTIAL) { /* UDP hardware csum */

		udp4_hwcsum(skb, fl4->saddr, fl4->daddr);
		goto send;

	} else
		csum = udp_csum(skb);

	/* add protocol-dependent pseudo-header */
	uh->check = csum_tcpudp_magic(fl4->saddr, fl4->daddr, len,
				      sk->sk_protocol, csum);
	
	if (uh->check == 0)
		uh->check = CSUM_MANGLED_0;

send:
	err = ip_send_skb(sock_net(sk), skb);
	if (err) {
		if (err == -ENOBUFS && !inet->recverr) {
			UDP_INC_STATS(sock_net(sk),
				      UDP_MIB_SNDBUFERRORS, is_udplite);
			err = 0;
		}
	} else
		UDP_INC_STATS(sock_net(sk),
			      UDP_MIB_OUTDATAGRAMS, is_udplite);
	return err;
}

3.1 硬件校验和 udp4_hwcsum()

如果 ip_summed 的值为CHECKSUM_PARTIAL,说明采用网卡硬件计算UDP数据包的校验和。这里之所以称为 partial,是因为网卡硬件计算的校验和不包括IP伪头部的数据,即不包括IP头部中的源地址、目的地址,长度和协议号字段。所以需要内核程序首先计算出IP伪头部的校验和放置在UDP头部的校验和字段中。网卡硬件校验和计算函数 udp4_hwcsum,并不一定能够利用硬件的校验和计算功能,对于存在skb分片的数据包就需要在程序中软件进行计算。

如果skb没有分片数据,就可利用网卡硬件计算校验和。需要设置 csum_start 告知网卡硬件需要计算校验和的数据起始地址,以及计算完成之后校验和结果的放置偏移地址,csum_offset 即是 UDP头部中 check 字段的偏移地址。另外,利用函数 csum_tcpudp_magic 计算IP伪头部的校验和。

对于skb具有分片的数据包,软件遍历所有的分片计算整个数据包的校验和,注意此处并没有使用skb_checksum计算整个数据包的校验和,因为其中每个片段的校验和已经计算完成,所以提前累加了所有sk_buff片段数据的校验和,之后使用skb_checksum计算其余数据部分的校验和,减少了重复计算。最后累加上IP伪头部的数据。将变量ip_summed设置为CHECKSUM_NONE表明校验和已经计算完成。

/**
 * 	udp4_hwcsum  -  handle outgoing HW checksumming
 * 	@skb: 	sk_buff containing the filled-in UDP header
 * 	        (checksum field must be zeroed out)
 *	@src:	source IP address
 *	@dst:	destination IP address
 */
void udp4_hwcsum(struct sk_buff *skb, __be32 src, __be32 dst)
{
	struct udphdr *uh = udp_hdr(skb);
	int offset = skb_transport_offset(skb);
	int len = skb->len - offset;
	int hlen = len;
	__wsum csum = 0;

	if (!skb_has_frag_list(skb)) { //没有分片
		/*
		 * 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 {
		struct sk_buff *frags;

		/*
		 * 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
		 */
		skb_walk_frags(skb, frags) {
			csum = csum_add(csum, frags->csum);
			hlen -= frags->len;
		}

		csum = skb_checksum(skb, offset, hlen, csum);
		skb->ip_summed = CHECKSUM_NONE;

		uh->check = csum_tcpudp_magic(src, dst, len, IPPROTO_UDP, csum);
		if (uh->check == 0)
			uh->check = CSUM_MANGLED_0;
	}
}
EXPORT_SYMBOL_GPL(udp4_hwcsum);

3.2 UDP软件校验和 udp_csum()

由函数 udp_csum 进行计算。此函数分成两个部分,首先计数 UDP 头部数据的校验和,在与 skb中已经计算的校验和累加。其次在与 skb 分片列表中的校验和累加,得到所有数据的校验和。最后,还要累加上IP伪头部的校验和。

此处看到每个单独skb的校验和已经计算完成,要归功于函数ip_make_skb,内核在生成skb缓存,将用户空间数据拷贝到内核空间skb时,提前进行了部分校验和的计算工作。

static inline __wsum udp_csum(struct sk_buff *skb)
{
	__wsum csum = csum_partial(skb_transport_header(skb),
				   sizeof(struct udphdr), skb->csum);

	for (skb = skb_shinfo(skb)->frag_list; skb; skb = skb->next) {
		csum = csum_add(csum, skb->csum);
	}
	return csum;
}

3.3 UDPLITE校验和 udplite_csum()

如果是 udplite 协议(IPPROTO_UDPLITE),校验和计算与UDP基本相同,唯一区别是用户层可指定校验和所覆盖的数据长度。使用 udplite 特定的校验和计算函数 udplite_csum。其最终通过调用 skb_checksum 函数实现。

/* Fast-path computation of checksum. Socket may not be locked. */
static inline __wsum udplite_csum(struct sk_buff *skb)
{
	const struct udp_sock *up = udp_sk(skb->sk);
	const int off = skb_transport_offset(skb);
	int len = skb->len - off;

	if ((up->pcflag & UDPLITE_SEND_CC) && up->pcslen < len) {
		if (0 < up->pcslen)
			len = up->pcslen;
		udp_hdr(skb)->len = htons(up->pcslen);
	}
	skb->ip_summed = CHECKSUM_NONE;     /* no HW support for checksumming */

	return skb_checksum(skb, off, len, 0);
}

3.4 通用校验和计算 skb_checksum()

内核函数skb_checksum实现通用的完整校验和计算功能。其三个重要的参数为需要计算校验和的skb,起始偏移offset和长度len。首先我们知道skb缓存中存放的数据可能分布在3个区域:

  1. skb线性区域,由skb成员data开始到tail所指向的空间;
  2. 页面片段区域,由保存在skb_shared_info中的成员(skb_frag_t结构体)frags所指向的页面空间;
  3. skb分片区域,由保存在skb_shared_info中的成员(struct sk_buff)*frag_list所指向。

具体 skb_checksum 函数的实现分为了三个部分,如果校验和长度len为总长度,其将跨越了三个部分计算校验和。另外需要注意,起始偏移offset有可能起始于三个部分中的任何一个,如果offset偏移到3)skb分片区域,前两个区域中的数据将不会被计算到。

对于3)skb分片区域,有可能分片skb又存在提到的三个数据区域,所以3)部分的计算为递归调用计算校验和。

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux 内核协议栈是指在Linux操作系统中实现网络通信的核心组件。它是一个自上而下的分层体系结构,用于处理从应用层到物理层的网络数据传输。 在协议栈的顶层是应用层协议,例如HTTP、FTP和DNS等。应用层协议使用套接字与传输层进行通信。传输层包括TCP和UDP协议,负责在主机之间提供可靠的数据传输和错误恢复。TCP提供面向连接的服务,确保数据的可靠交付,而UDP提供无连接的服务,并且不进行错误恢复。 传输层下面是网络层,它负责将数据包从源主机传输到目标主机。网络层使用IP协议进行数据包的路由和转发。IP协议定义了IP地址和数据包封装格式等规范。 在网络层下面是数据链路层,它包括以太网、Wi-Fi和PPP等协议。数据链路层负责将数据包从网络层传输到物理层,并提供错误检测和传输控制。 最底层是物理层,它包括电缆、光纤和无线电等物理介质,负责实际的数据传输。 Linux内核协议栈的实现在内核空间中。它使用网络协议栈的核心模块来处理网络数据的收发、封装和解封装等操作。每个层级的协议会根据需要访问下一层的服务,并将处理后的数据传递给上一层或下一层协议。 协议栈的设计目标是高性能和灵活性。Linux内核协议栈使用分层设计,使得各个层级的功能独立,并且可以根据具体需求进行扩展和定制。内核代码的高度优化使得协议栈能够高效地处理大量的网络数据,并提供可靠、快速的网络通信服务。 总之,Linux内核协议栈Linux操作系统中负责实现网络通信的关键组件。通过合理的分层设计和优化的内核代码,协议栈能够提供高性能和灵活性的网络通信服务。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值