文章目录
协议设计上,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_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;
// 计算校验和,如果成功,那么最终结果应该是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校验和的处理有如下几个关键点:
- 只有当UDP报文的数据可以用一个IP片段发送出去,而且硬件支持校验和计算时,UDP才会将校验和计算的任务交给硬件完成;
- 伪首部的校验和计算总是由软件自己完成的;