Linux kernel checksum calculation and skb->ip_summed

Background

最近在看内核相关的checksum代码,起因是往虚拟机virtio-net发包时,想通过VIRTIO_NET_HDR_F_DATA_VALID这个flag减少GuestOS的校验和计算开销。关于这个flag,以及virtio_net_hdr这个结构体相关的内容,值得单独说明,所以在这里只是简单提一下。

最初的疑问是VIRTIO_NET_HDR_F_DATA_VALID到底对应几层校验和?

从其定义(DPDK 19.11)可以看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* This is the first element of the scatter-gather list. If you don't
* specify GSO or CSUM features, you can simply ignore the header.
*/
struct virtio_net_hdr {
#define VIRTIO_NET_HDR_F_NEEDS_CSUM 1 /**< Use csum_start,csum_offset*/
#define VIRTIO_NET_HDR_F_DATA_VALID 2 /**< Checksum is valid */
uint8_t flags;
#define VIRTIO_NET_HDR_GSO_NONE 0 /**< Not a GSO frame */
#define VIRTIO_NET_HDR_GSO_TCPV4 1 /**< GSO frame, IPv4 TCP (TSO) */
#define VIRTIO_NET_HDR_GSO_UDP 3 /**< GSO frame, IPv4 UDP (UFO) */
#define VIRTIO_NET_HDR_GSO_TCPV6 4 /**< GSO frame, IPv6 TCP */
#define VIRTIO_NET_HDR_GSO_ECN 0x80 /**< TCP has ECN set */
uint8_t gso_type;
uint16_t hdr_len; /**< Ethernet + IP + tcp/udp hdrs */
uint16_t gso_size; /**< Bytes to append to hdr_len per frame */
uint16_t csum_start; /**< Position to start checksumming from */
uint16_t csum_offset; /**< Offset after that to place checksum */
};

virtio_net_hdr包含了报文的csum_startcsum_offset,且包含了gso相关字段,从这里可以猜测,它就是描述L4的卸载相关属性的。

为此再接着看VIRTIO_NET_HDR_F_DATA_VALID相关的处理逻辑。

首先看内核(4.18)相关实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static inline int virtio_net_hdr_from_skb(const struct sk_buff *skb,
struct virtio_net_hdr *hdr,
bool little_endian,
bool has_data_valid,
int vlan_hlen)
{
......
if (skb->ip_summed == CHECKSUM_PARTIAL) {
hdr->flags = VIRTIO_NET_HDR_F_NEEDS_CSUM;
hdr->csum_start = __cpu_to_virtio16(little_endian,
skb_checksum_start_offset(skb) + vlan_hlen);
hdr->csum_offset = __cpu_to_virtio16(little_endian,
skb->csum_offset);
} else if (has_data_valid &&
skb->ip_summed == CHECKSUM_UNNECESSARY) {
hdr->flags = VIRTIO_NET_HDR_F_DATA_VALID;
} /* else everything is zero */
......
}

static void receive_buf(struct virtnet_info *vi, struct receive_queue *rq,
void *buf, unsigned int len, void **ctx,
unsigned int *xdp_xmit, unsigned int *rbytes)
{
......
if (hdr->hdr.flags & VIRTIO_NET_HDR_F_DATA_VALID)
skb->ip_summed = CHECKSUM_UNNECESSARY;
......
}

再看DPDK pmd_virtio中的相关实现:

1
2
3
4
5
6
7
8
9
10
/* Optionally fill offload information in structure */
static inline int
virtio_rx_offload(struct rte_mbuf *m, struct virtio_net_hdr *hdr)
{
......
} else if (hdr->flags & VIRTIO_NET_HDR_F_DATA_VALID && l4_supported) {
m->ol_flags |= PKT_RX_L4_CKSUM_GOOD;
}
......
}

从DPDK的代码中可以直观地确认,当PMD收到有VIRTIO_NET_HDR_F_DATA_VALID置位的报文时,为mbuf置上PKT_RX_L4_CKSUM_GOOD以标识L4校验和正确;内核对应的情况是skb->ip_summed == CHECKSUM_UNNECESSARY

这里就是比较疑惑的地方了:既然从结构体本身成员,以及DPDK代码中大致确认,VIRTIO_NET_HDR_F_DATA_VALID是描述的L4校验和状态,为何内核中的代码对应的是skb->ip_summed这个成员呢?从其命名上看应该描述的是L3/IP的校验和才对呀?

skb->ip_summed

首先关注这个令人疑惑的成员,在skbuff.h中,有大段的注释对其进行了详细的描述。

下面对接收方向进行说明,skb->ip_summed可能的取值有CHECKSUM_NONECHECKSUM_UNNECESSARYCHECKSUM_COMPLETECHECKSUM_PARTIAL

CHECKSUM_NONE

这种情况很好理解,注释上解释的也比较清楚,当硬件没有对报文进行校验(如硬件不支持校验特性)时,会将skb->ip_summed置为CHECKSUM_NONE,并且此时的skb->csum也是无意义的。软件针对此种场景需要完整计算校验和。

CHECKSUM_COMPLETE

这是最通用的一种场景,硬件计算了报文从L4头部开始的数据的1的补码和并且保存到了skb->csum

需要注意的是,硬件不需要对L3/L4进行协议解析即可做到这一点。因为整个IP协议族(IPv4、TCP、UDP等)的校验和计算方式都是一样的,即1的反码和的16bit 1的补码,所以这种情况下硬件只需从L4头部开始计算剩余的1的补码和即可。

在软件计算特定的L4校验和时,可以通过硬件计算的完整结果skb->csum,进行进一步计算获得最终结果。如:TCP通过skb->csum加上伪校验和来计算TCP校验和。

CHECKSUM_UNNECESSARY

这种场景下,硬件已经确认了特定类型(TCP、UDP、GRE、SCP和FCoE)的L4协议的校验和正确,软件无需再进行校验和的验证。

需要注意的是,尽管已经确定了特定类型的L4校验和正确,skb->csum还是未定义的,且后续流程也不应该修改skb->csum

CHECKSUM_PARTIAL

这种场景硬件计算从skb->csum_start开始的数据的校验和,并保存到skb->csum_start + skb->csum_offset的位置上。

Why is it named as ip_summed?

从上面的取值场景可以发现,skb->ip_summed描述的是L4的校验和状态,这和对DPDK代码,以及virtio_net_hdr的分析的结论是一致的,但是为什么用了一个这么令人费解的命名呢?

为此我向Linux netdev的mailing list发送了一封邮件询问这件事,有幸得到了大佬的解答,大意是:虽然命名确实很令人费解,但skb->ip_summed描述的是L4的校验和状态。主要原因是整个IP协议族的校验和计算方式都是一样的,所以命名里的ip不是专指IP协议,而是指IP协议族使用的校验和计算方式。其实不仅仅是ip_summed,内核里应该也有一些地方的ip并不是特指IPv4协议,如ip_compute_csum

同时关于L3/IP校验和,由于L3/IP校验和只需要计算首部,计算量不大,并且在报文接收,解析报文首部时,首部的内容已经被缓存到cache中去了,所以软件计算的开销会很小,由内核进行L3/IP的校验和计算。

完整邮件见:Confused about ip_summed member in sk_buff

How does kernel calculate the checksum?

IPv4

1
2
3
4
5
6
7
8
9
10
11
12
Header Checksum:  16 bits

A checksum on the header only. Since some header fields change
(e.g., time to live), this is recomputed and verified at each point
that the internet header is processed.

The checksum algorithm is:

The checksum field is the 16 bit one's complement of the one's
complement sum of all 16 bit words in the header. For purposes of
computing the checksum, the value of the checksum field is zero.
——RFC791

从IPv4校验和的定义可以知道,它的值就是首部所有16bit字的1的补码和的1的补码,计算时,校验和字段以0计算。

以如下报文为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
16:31:34.953256 00:50:56:97:d1:f3 > a8:0c:ca:5f:8c:5c, ethertype IPv4 (0x0800), length 32174: (tos 0x0, ttl 64, id 5604, offset 0, flags [DF], proto TCP (6), length 32160)
10.103.240.223.22 > 172.23.9.13.12036: Flags [.], cksum 0x2dfe (incorrect -> 0x18ca), seq 74390940:74423060, ack 4837, win 64512, length 32120
0x0000: a80c ca5f 8c5c 0050 5697 d1f3 0800 4500 ..._.\.PV.....E.
0x0010: 7da0 15e4 4000 4006 f708 0a67 f0df ac17 }...@.@....g....
0x0020: 090d 0016 2f04 fb22 4704 23c3 2b20 5010 ..../.."G.#.+.P.
0x0030: fc00 2dfe 0000 33cd 52ed 85f9 888f b0aa ..-...3.R.......
0x0040: 7d4e 3cd0 db58 27eb bb87 a09e ea81 c61a }N<..X'.........
0x0050: 9119 23bd 0fa0 dd44 cae2 e84f 4c31 0241 ..#....D...OL1.A
0x0060: 54ee 860e 3feb 6d4e 1bd1 2ef5 faeb e2e4 T...?.mN........
0x0070: 6421 7c4d 3ec6 1e5f 0bd9 88ea 4776 3a5d d!|M>.._....Gv:]
0x0080: 0a05 abf7 fbd4 69f6 c1d9 74c9 fefc 2fc7 ......i...t.../.
0x0090: c8b0 b447 ccb9 6148 9480 d78a c3e9 8e95 ...G..aH........
0x00a0: 1d04 c8fd 66ab 993e ecf3 7ceb 5443 adfb ....f..>..|.TC..
0x00b0: a6e9 1e55 49d2 b86e 238d bac6 24aa 1b27 ...UI..n#...$..'

IPv4首部为:

1
4500 7da0 15e4 4000 4006 f708 0a67 f0df ac17 090d

其中校验和为:f708

下面来实际算一遍。根据校验和定义,先将校验和字段置零,所以IPv4首部在未计算校验和时为:

1
4500 7da0 15e4 4000 4006 0000 0a67 f0df ac17 090d

先将首部所有16bit字相加:

1
2
sum(32bit integer) = 0x4500 + 0x7da0 + 0x15e4 + 0x4000 + 0x4006 + 0x0000 + 0x0a67 + 0xf0df + 0xac17 + 0x090d
sum(32bit integer) = 0x308f4

再根据1的补码的加法原则,将进位加回结果:

1
2
sum(one's complement) = 0x08f4 + 0x3 (sum + carry)
sum(one's complement) = 0x08f7

再对和求16bit 1的补码:

1
csum = 16bit one’s complement(0x08f7) = 0xf708

可以看到,计算出的结果正好就和报文内原始的值相同,证明报文内的校验和字段是正确的。

当然还有另一种验证方式,在计算16bit 1的补码和时,将校验和的值也加入计算,然后判断最终值是否为0xffff(16bit 1的补码中的-0)。

再看看内核中通用的、非内联汇编版本的IPv4校验和实现ip_fast_csum,调用路径为ip_rcv->ip_fast_csum

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static inline __sum16 ip_fast_csum(const void *iph, unsigned int ihl)
{
const unsigned int *word = iph;
const unsigned int *stop = word + ihl;
unsigned int csum;
int carry;

csum = word[0];
csum += word[1];
carry = (csum < word[1]);
csum += carry;

csum += word[2];
carry = (csum < word[2]);
csum += carry;

csum += word[3];
carry = (csum < word[3]);
csum += carry;

word += 4;
do {
csum += *word;
carry = (csum < *word);
csum += carry;
word++;
} while (word != stop);

return csum_fold(csum);
}

需要注意的是,这个函数内,单次计算数据单位为32bit,并没有完全按照RFC的定义进行16bit累加,原因是32bit累加性能更好。

可以很容易想到,16bit累加的结果在32bit累加中是等效的,当低16bit累加发生进位后,会加到高16bit上去,高16bit再发生进位的话,再加回低16bit。这里可以看到代码里通过累加和与加数之间的大小比较,来确定是否发生了进位,如果进位加回累加和。

还有一点需要注意的是,IPv4协议首部是可变长度,最短20字节,为什么这个版本的实现中,先计算了前16字节,而不是20字节呢?这一点我个人的理解是16字节是2的幂,考虑指令、cache等因素下,应该性能会更好。

累加和计算完成后,调用csum_fold对结果进行折叠取反:

1
2
3
4
5
6
7
static inline __sum16 csum_fold(__wsum csum)
{
u32 sum = (__force u32)csum;
sum = (sum & 0xffff) + (sum >> 16);
sum = (sum & 0xffff) + (sum >> 16);
return (__force __sum16)~sum;
}

因为前面计算的累加和是32bit的,根据1的补码加法定义,高低16bit需要再加在一起,才是最后的结果。从形式上看,就像对累加和进行了折叠(fold)一样。折叠完了以后,就是完整了1的补码和了,再对结果求16bit 1的补码(取反),就得到了最终的校验和。这也是为什么很多地方把校验和计算的过程描述成“累加取反”,但光从这个描述很难和原始定义对应上。

TCP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Checksum:  16 bits

The checksum field is the 16 bit one's complement of the one's
complement sum of all 16 bit words in the header and text. If a
segment contains an odd number of header and text octets to be
checksummed, the last octet is padded on the right with zeros to
form a 16 bit word for checksum purposes. The pad is not
transmitted as part of the segment. While computing the checksum,
the checksum field itself is replaced with zeros.

The checksum also covers a 96 bit pseudo header conceptually
prefixed to the TCP header. This pseudo header contains the Source
Address, the Destination Address, the Protocol, and TCP length.
This gives the TCP protection against misrouted segments. This
information is carried in the Internet Protocol and is transferred
across the TCP/Network interface in the arguments or results of
calls by the TCP on the IP.

+--------+--------+--------+--------+
| Source Address |
+--------+--------+--------+--------+
| Destination Address |
+--------+--------+--------+--------+
| zero | PTCL | TCP Length |
+--------+--------+--------+--------+

The TCP Length is the TCP header length plus the data length in
octets (this is not an explicitly transmitted quantity, but is
computed), and it does not count the 12 octets of the pseudo
header.
--RFC793

和IPv4相比,TCP校验和除了还要计算payload(text)以外,还加上了伪头部(pseudo header),用于验证IPv4的信息是否匹配。伪头部是一个抽象的头部,只是为了在四层头部额外保存三层信息,一定程度上用于确认三层正确性,并不存在于实际报文中。同时,计算校验和时如果总数据不能整除16bit,则在末尾补0计算,这部分0同样只用来计算,也不存在于实际报文中。

以如下报文为例:

1
2
3
4
5
6
7
8
9
14:12:51.140861 a8:0c:ca:5f:8c:5c > 00:50:56:97:d1:f3, ethertype IPv4 (0x0800), length 106: (tos 0x0, ttl 125, id 4094, offset 0, flags [DF], proto TCP (6), length 92)
172.23.9.13.2774 > 10.103.240.223.22: Flags [P.], cksum 0xccb7 (correct), seq 0:52, ack 46045, win 32544, length 52
0x0000: 0050 5697 d1f3 a80c ca5f 8c5c 0800 4500 .PV......_.\..E.
0x0010: 005c 0ffe 4000 7d06 3d33 ac17 090d 0a67 .\..@.}.=3.....g
0x0020: f0df 0ad6 0016 6749 d568 1ba4 a329 5018 ......gI.h...)P.
0x0030: 7f20 ccb7 0000 0000 0010 f43e bdd1 e11e ...........>....
0x0040: e612 f35a d406 61c9 6ed5 5f47 a7e5 5d56 ...Z..a.n._G..]V
0x0050: 9906 ce6e 15ac af67 e2a6 ffc8 8724 cdff ...n...g.....$..
0x0060: 3d3f b304 5838 1883 71f6 =?..X8..q.

伪头部(src addr + dst addr + zero + protocol + l4 length)为:

1
ac17 090d 0a67 f0df 0000 0006 0000 0048

TCP首部为:

1
0ad6 0016 6749 d568 1ba4 a329 5018 7f20 ccb7 0000

其中校验和为:ccb7,计算校验和时将其置0:

1
0ad6 0016 6749 d568 1ba4 a329 5018 7f20 0000 0000

payload为:

1
0000 0010 f43e bdd1 e11e e612 f35a d406 61c9 6ed5 5f47 a7e5 5d56 9906 ce6e 15ac af67 e2a6 ffc8 8724 cdff 3d3f b304 5838 1883 71f6

将所有16bit字相加:

1
2
sum(32bit integer) = 0xac17 + 0x090d + 0x0a67 + 0xf0df + 0x0000 + 0x0006 + 0x0000 + 0x0048 + 0x0ad6 + 0x0016 + 0x6749 + 0xd568 + 0x1ba4 + 0xa329 + 0x5018 + 0x7f20 + 0x0000 + 0x0000 + 0x0000 + 0x0010 + 0xf43e + 0xbdd1 + 0xe11e + 0xe612 + 0xf35a + 0xd406 + 0x61c9 + 0x6ed5 + 0x5f47 + 0xa7e5 + 0x5d56 + 0x9906 + 0xce6e + 0x15ac + 0xaf67 + 0xe2a6 + 0xffc8 + 0x8724 + 0xcdff + 0x3d3f + 0xb304 + 0x5838 + 0x1883 + 0x71f6
sum(32bit integer) = 0x133335

再根据1的补码的加法原则,将进位加回结果:

1
2
sum(one's complement) = 0x3335 + 0x13 (sum + carry)
sum(one's complement) = 0x3348

再对和求16bit 1的补码:

1
csum = 16bit one’s complement(0x3348) = 0xccb7

可以看到,计算出的结果正好就和TCP首部的值相同。

内核中使用csum_tcpudp_nofold计算TCP的伪头部和,调用路径:tcp_v4_rcv->skb_checksum_init->__skb_checksum_validate->inet_compute_pseudo->csum_tcpudp_nofold

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static inline u32 from64to32(u64 x)
{
/* add up 32-bit and 32-bit for 32+c bit */
x = (x & 0xffffffff) + (x >> 32);
/* add up carry.. */
x = (x & 0xffffffff) + (x >> 32);
return (u32)x;
}

__wsum csum_tcpudp_nofold(__be32 saddr, __be32 daddr,
__u32 len, __u8 proto, __wsum sum)
{
unsigned long long s = (__force u32)sum;

s += (__force u32)saddr;
s += (__force u32)daddr;
#ifdef __BIG_ENDIAN
s += proto + len;
#else
s += (proto + len) << 8;
#endif
return (__force __wsum)from64to32(s);
}
EXPORT_SYMBOL(csum_tcpudp_nofold);

可以看到除了也是用32bit相加的方式以外,整体流程符合伪头部和计算的定义。还需要注意的一点是,上述调用链中,csum_tcpudp_nofold的参数中len的值为skb->lenskb->len代表当前协议层次的数据长度,当前为TCP协议处理流程,所以对应了TCP首部加TCP payload的总长;sum的值为0。

再看skb_checksum_init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* Perform checksum validate (init). Note that this is a macro since we only
* want to calculate the pseudo header which is an input function if necessary.
* First we try to validate without any computation (checksum unnecessary) and
* then calculate based on checksum complete calling the function to compute
* pseudo header.
*
* Return values:
* 0: checksum is validated or try to in skb_checksum_complete
* non-zero: value of invalid checksum
*/
#define __skb_checksum_validate(skb, proto, complete, \
zero_okay, check, compute_pseudo) \
({ \
__sum16 __ret = 0; \
skb->csum_valid = 0; \
if (__skb_checksum_validate_needed(skb, zero_okay, check)) \
__ret = __skb_checksum_validate_complete(skb, \
complete, compute_pseudo(skb, proto)); \
__ret; \
})

#define skb_checksum_init(skb, proto, compute_pseudo) \
__skb_checksum_validate(skb, proto, false, false, 0, compute_pseudo)

skb_checksum_init直接调用了__skb_checksum_validate,可以看到里面逻辑先将skb->csum_valid初始化,再调用__skb_checksum_validate_needed判断是否需要计算校验和,如果需要的话,再调用__skb_checksum_validate_complete进行具体计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static inline int skb_csum_unnecessary(const struct sk_buff *skb)
{
return ((skb->ip_summed == CHECKSUM_UNNECESSARY) ||
skb->csum_valid ||
(skb->ip_summed == CHECKSUM_PARTIAL &&
skb_checksum_start_offset(skb) >= 0));
}

/* Check if we need to perform checksum complete validation.
*
* Returns true if checksum complete is needed, false otherwise
* (either checksum is unnecessary or zero checksum is allowed).
*/
static inline bool __skb_checksum_validate_needed(struct sk_buff *skb,
bool zero_okay,
__sum16 check)
{
if (skb_csum_unnecessary(skb) || (zero_okay && !check)) {
skb->csum_valid = 1;
__skb_decr_checksum_unnecessary(skb);
return false;
}

return true;
}

__skb_checksum_validate_needed中逻辑较为简单,当skb_csum_unnecessary返回true或当前校验和check0并且为0的校验和是合法值时,将skb->csum_valid置1,并且调整skb中CHECKSUM_UNNECESSARY相关成员的值,返回false,否则返回true

skb_csum_unnecessary中则是根据skb->ip_summed以及其他skb的信息判断是否有必要计算校验和:

  • skb->ip_summed == CHECKSUM_UNNECESSARY:无需计算,和上面对CHECKSUM_UNNECESSARY的分析一致,这种情况下硬件已经确认校验和正确,协议栈不需要任何计算。
  • skb->csum_valid == 1:无需计算,当前协议层次的校验和已被确认为合法值。
  • skb->ip_summed == CHECKSUM_PARTIAL && skb_checksum_start_offset(skb) >= 0:无需计算,CHECKSUM_PARTIAL代表部分校验和已被硬件计算,skb_checksum_start_offset(skb)代表skb->csum_start合法,即已被计算的那部分校验和是合法的。
  • 其他情况:需要计算。

需要计算校验和的场景,调用__skb_checksum_validate_complete进行计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/* Validate (init) checksum based on checksum complete.
*
* Return values:
* 0: checksum is validated or try to in skb_checksum_complete. In the latter
* case the ip_summed will not be CHECKSUM_UNNECESSARY and the pseudo
* checksum is stored in skb->csum for use in __skb_checksum_complete
* non-zero: value of invalid checksum
*
*/
static inline __sum16 __skb_checksum_validate_complete(struct sk_buff *skb,
bool complete,
__wsum psum)
{
if (skb->ip_summed == CHECKSUM_COMPLETE) {
if (!csum_fold(csum_add(psum, skb->csum))) {
skb->csum_valid = 1;
return 0;
}
}

skb->csum = psum;

if (complete || skb->len <= CHECKSUM_BREAK) {
__sum16 csum;

csum = __skb_checksum_complete(skb);
skb->csum_valid = !csum;
return csum;
}

return 0;
}

准确来说,__skb_checksum_validate_complete只处理CHECKSUM_COMPLETE相关的场景。

特别地,当skb->ip_summed == CHECKSUM_COMPLETE时,根据前面对CHECKSUM_COMPLETE场景的分析,此时skb->csum保存的是L4开始的1的补码和,直接将skb->csumpsum累加取反即可得到TCP校验和。

对于特殊场景,如下面的分支中,会直接调用__skb_checksum_complete进行完整计算,否则只保存伪头部和,在后续流程中(如tcp_v4_rcv->tcp_v4_do_rcv->tcp_checksum_complete)再进行完整计算。

tcp_checksum_complete最终也是调用__skb_checksum_complete进行计算,其中逻辑大概也是求补码和,然后取反。求补码和的地方相对比较复杂,涉及skb链的场景,就不细说了。

UDP

1
2
3
4
5
Checksum is the 16-bit one's complement of the one's complement sum of a
pseudo header of information from the IP header, the UDP header, and the
data, padded with zero octets at the end (if necessary) to make a
multiple of two octets.
--RFC 768

UDP首部校验和与TCP校验和的计算方式基本一致,也需要计算伪头部,但不需要计算payload,具体流程就不展开细说了。

一个典型的调用链:udp_rcv->__udp4_lib_rcv->udp4_csum_init->skb_checksum_init_zero_check

  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • Copyrights © 2021-2023 Martzki
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信