UDP的checksum计算与硬件Offload

发布于:2023-01-20 ⋅ 阅读:(2) ⋅ 点赞:(0) ⋅ 评论:(0)

Linux内核中UDP数据包的计算由函数udp_send_skb完成。根据代码可见,存在4种不同的计算方式。对于IPv4协议来说,UDP的校验和是可选的,用户可通过setsockopt(SO_NO_CHECK)系统调用关闭校验和计算。

    if (is_udplite)                  /*     UDP-Lite      */
        csum = udplite_csum(skb);
    else if (sk->sk_no_check_tx) {           /* UDP csum off */
        skb->ip_summed = CHECKSUM_NONE;
        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);
 

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

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

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

    struct sk_buff *frags;
    
    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);
 

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

此处看到每个单独skb的校验和已经计算完成,要归功于函数ip_make_skb,内核在生成skb缓存,将用户空间数据拷贝到内核空间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);
    }
 

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

    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);
 

四、通用校验和计算
内核函数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)部分的计算为递归调用计算校验和。