TCP实现之:IP分片内核实现
TCP实现之:IP分片内核实现
一、前言
先来回顾一下基本概念吧。啥是分片?啥是分段?
报文在网络设备间传输的时候,一次能够传输单个报文的尺寸是有限制的,这个限制被称为MTU
:最大传输单元(Maximum Transmission Unit
)。不同类型的网络的MTU
大小可能有差异,如以太网的MTU
为1500。这个MTU
指的是报文所能携带的有效数据,因此并不是以太网数据帧的真实大小,其真实大小为:
1500
+
14
+
4
=
1518
1500 + 14 + 4 = 1518
1500+14+4=1518
其中14为以太网报头,4位为尾部校验和FCS
。
当IP
报文的长度大于MTU
时,需要将报文拆分为多个长度不大于MTU
的数据片,这个过程称为IP
层的分片。由于单个数据片的丢失会导致整个IP
报文的失效与重传,因此IP
的分片要尽量避免。因此,对于TCP
报文,在将报文交给IP
协议之前,会将报文拆分成长度不大于MTU
的数据段,这个过程称为TCP
层的分段。
二、IP
分片的实现
下面我们主要以原始套接字在进行报文发送时的IP分片为例来进行讲解。
2.1 原始套接字
针对于IP
分片,原始套接字有两种情况:是否设置了IP_HDRINCL
套接字选项。该选项用来控制内核是否生成IP
报头:当设置该选项时,意味着IP
头部已经包含在了用户数据里,不需要内核添加IP
头部;否则,内核将根据套接字中的信息来为报文添加IP
头部。
当设置了该选项时,内核将不会为该报文分片,报文长度如果大于MTU
将直接返回错误。官方可能认为,在这种模式下,用户态将负责报文的全权处理,包括分片等操作。
当未设置该选项的时候,内核会调用ip_append_data
来将用户数据添加到套接字的待发送队列中。下面我们来详细看一下这个函数的实现。
ip_append_data
static int __ip_append_data(struct sock *sk,
struct flowi4 *fl4,
struct sk_buff_head *queue,
struct inet_cork *cork,
struct page_frag *pfrag,
int getfrag(void *from, char *to, int offset,
int len, int odd, struct sk_buff *skb),
void *from, int length, int transhdrlen,
unsigned int flags)
{
struct inet_sock *inet = inet_sk(sk);
struct sk_buff *skb;
struct ip_options *opt = cork->opt;
int hh_len;
int exthdrlen;
int mtu;
int copy;
int err;
int offset = 0;
unsigned int maxfraglen, fragheaderlen, maxnonfragsize;
int csummode = CHECKSUM_NONE;
struct rtable *rt = (struct rtable *)cork->dst;
u32 tskey = 0;
skb = skb_peek_tail(queue); //取出发送队列最后一个skb
exthdrlen = !skb ? rt->dst.header_len : 0;
mtu = cork->fragsize; //cork里面保存了IP分片过程中需要用到的很多参数,fragsize为MTU
if (cork->tx_flags & SKBTX_ANY_SW_TSTAMP &&
sk->sk_tsflags & SOF_TIMESTAMPING_OPT_ID)
tskey = sk->sk_tskey++;
hh_len = LL_RESERVED_SPACE(rt->dst.dev); //以太网头部长度
fragheaderlen = sizeof(struct iphdr) + (opt ? opt->optlen : 0); //IP头部长度
maxfraglen = ((mtu - fragheaderlen) & ~7) + fragheaderlen; //最大片段长度,指的是IP报文总长度。
maxnonfragsize = ip_sk_ignore_df(sk) ? 0xFFFF : mtu; //最大允许的未分片的数据的尺寸。也就是说,数据的总长度不能超过这个值。如果sk设置的ignore_df(忽略禁止分片),那么其大小为0xFFFF;否则为MTU。
/* cork->length为已经添加的数据长度。若数据长度超过允许的长度,返回EMSGSIZE。 */
if (cork->length + length > maxnonfragsize - fragheaderlen) {
ip_local_error(sk, EMSGSIZE, fl4->daddr, inet->inet_dport,
mtu - (opt ? opt->optlen : 0));
return -EMSGSIZE;
}
/*
* transhdrlen > 0 means that this is the first fragment and we wish
* it won't be fragmented in the future.
*/
if (transhdrlen &&
length + fragheaderlen <= mtu &&
rt->dst.dev->features & (NETIF_F_HW_CSUM | NETIF_F_IP_CSUM) &&
!(flags & MSG_MORE) &&
!exthdrlen)
csummode = CHECKSUM_PARTIAL;
cork->length += length;
/* 对于GSO标志的报文,或者是支持UFO的网卡驱动(网卡驱动进行的UDP分段),使用ip_ufo_append_data方法来处理。该方法不会对报文进行分片。 */
if ((skb && skb_is_gso(skb)) ||
((length > mtu) &&
(skb_queue_len(queue) <= 1) &&
(sk->sk_protocol == IPPROTO_UDP) &&
(rt->dst.dev->features & NETIF_F_UFO) && !rt->dst.header_len &&
(sk->sk_type == SOCK_DGRAM) && !sk->sk_no_check_tx)) {
err = ip_ufo_append_data(sk, queue, getfrag, from, length,
hh_len, fragheaderlen, transhdrlen,
maxfraglen, flags);
if (err)
goto error;
return 0;
}
/* So, what's going on in the loop below?
*
* We use calculated fragment length to generate chained skb,
* each of segments is IP fragment ready for sending to network after
* adding appropriate IP header.
*/
if (!skb)
goto alloc_new_skb;
/* 开始循环将报文数据添加到套接口的发送队列中。 */
while (length > 0) {
/* 检查当前的skb可以容纳的报文数据长度。 */
copy = mtu - skb->len;
if (copy < length)
copy = maxfraglen - skb->len;
/* 当前skb满了,重新分配一个新的skb。 */
if (copy <= 0) {
char *data;
unsigned int datalen;
unsigned int fraglen;
unsigned int fraggap;
unsigned int alloclen;
struct sk_buff *skb_prev;
alloc_new_skb:
skb_prev = skb;
if (skb_prev)
fraggap = skb_prev->len - maxfraglen;
else
fraggap = 0;
/* datalen为要拷贝的数据的长度(不包括报文头部) */
datalen = length + fraggap;
if (datalen > mtu - fragheaderlen)
datalen = maxfraglen - fragheaderlen;
fraglen = datalen + fragheaderlen;
if ((flags & MSG_MORE) &&
!(rt->dst.dev->features&NETIF_F_SG))
alloclen = mtu;
else
alloclen = fraglen;
/* 计算需要分配的skb线性缓存区的大小。该大小由报文长度、headroom和tailroom组成。 */
alloclen += exthdrlen;
/* The last fragment gets additional space at tail.
* Note, with MSG_MORE we overallocate on fragments,
* because we have no idea what fragment will be
* the last.
*/
if (datalen == length + fraggap)
alloclen += rt->dst.trailer_len;
/* 进行skb的创建 */
if (transhdrlen) {
skb = sock_alloc_send_skb(sk,
alloclen + hh_len + 15,
(flags & MSG_DONTWAIT), &err);
} else {
skb = NULL;
if (atomic_read(&sk->sk_wmem_alloc) <=
2 * sk->sk_sndbuf)
skb = sock_wmalloc(sk,
alloclen + hh_len + 15, 1,
sk->sk_allocation);
if (unlikely(!skb))
err = -ENOBUFS;
}
if (!skb)
goto error;
/*
* Fill in the control structures
*/
skb->ip_summed = csummode;
skb->csum = 0;
/* 开辟headroom。在此之前,head、data、tail指向同一位置:线性缓存区的头部。 */
skb_reserve(skb, hh_len);
/* only the initial fragment is time stamped */
skb_shinfo(skb)->tx_flags = cork->tx_flags;
cork->tx_flags = 0;
skb_shinfo(skb)->tskey = tskey;
tskey = 0;
/* 开辟data空间,将tail往下移动,并增加len计数。 */
data = skb_put(skb, fraglen + exthdrlen);
/* 设置三层协议报头偏移。 */
skb_set_network_header(skb, exthdrlen);
/* 设置四层协议报头偏移。 */
skb->transport_header = (skb->network_header +
fragheaderlen);
/* 此时data指向了IP数据区 */
data += fragheaderlen + exthdrlen;
/* fraggap代表上一个skb多余的数据长度,要转移到当前的skb。不太懂这种操作。 */
if (fraggap) {
skb->csum = skb_copy_and_csum_bits(
skb_prev, maxfraglen,
data + transhdrlen, fraggap, 0);
skb_prev->csum = csum_sub(skb_prev->csum,
skb->csum);
data += fraggap;
pskb_trim_unique(skb_prev, maxfraglen);
}
/* 计算要拷贝的数据长度,去除fraggap和transhdrlen。 */
copy = datalen - transhdrlen - fraggap;
/*
* getfrag是用于对from进行数据分片的函数,其可以是raw_getfrag、udp_getfrag等。
* 对于raw_getfrag,它所做的事情是把msg迭代器中的copy长度的报文数据拷贝到skb的报文区。
* 其实这里的offset没有多大的意义,因为msg迭代器拷贝时会从上一次结束的地方重新开始。
*/
if (copy > 0 && getfrag(from, data + transhdrlen, offset, copy, fraggap, skb) < 0) {
err = -EFAULT;
kfree_skb(skb);
goto error;
}
offset += copy;
length -= datalen - fraggap;
transhdrlen = 0;
exthdrlen = 0;
csummode = CHECKSUM_NONE;
/*
* 将新创建的skb添加到发送队列。从这里我们可以看出,其并没有对IP头部进行初始化,只是保留了IP头部空间。
*/
__skb_queue_tail(queue, skb);
continue;
}
/* 之前的skb有足够的空间,不需要重新分配skb。 */
if (copy > length)
copy = length;
/* 设备不支持分散聚合IO,并且skb的tailroom有足够的空间。 */
if (!(rt->dst.dev->features&NETIF_F_SG) &&
skb_tailroom(skb) >= copy) {
unsigned int off;
off = skb->len;
if (getfrag(from, skb_put(skb, copy),
offset, copy, off, skb) < 0) {
__skb_trim(skb, off);
err = -EFAULT;
goto error;
}
} else {
/* 使用frags。此时,skb里存的数据量还没有达到MTU,但是线性缓存区已经满了 */
int i = skb_shinfo(skb)->nr_frags;
err = -ENOMEM;
if (!sk_page_frag_refill(sk, pfrag))
goto error;
if (!skb_can_coalesce(skb, i, pfrag->page,
pfrag->offset)) {
err = -EMSGSIZE;
if (i == MAX_SKB_FRAGS)
goto error;
__skb_fill_page_desc(skb, i, pfrag->page,
pfrag->offset, 0);
skb_shinfo(skb)->nr_frags = ++i;
get_page(pfrag->page);
}
copy = min_t(int, copy, pfrag->size - pfrag->offset);
if (getfrag(from,
page_address(pfrag->page) + pfrag->offset,
offset, copy, skb->len, skb) < 0)
goto error_efault;
pfrag->offset += copy;
skb_frag_size_add(&skb_shinfo(skb)->frags[i - 1], copy);
skb->len += copy;
skb->data_len += copy;
skb->truesize += copy;
atomic_add(copy, &sk->sk_wmem_alloc);
}
offset += copy;
length -= copy;
}
return 0;
error_efault:
err = -EFAULT;g
error:
cork->length -= length;
IP_INC_STATS(sock_net(sk), IPSTATS_MIB_OUTDISCARDS);
return err;
总结一下,ip_append_data
是一个相对比较通用的函数。它的作用是把数据添加到套接字发送队列中的skb
中去。如果数据量很小,它不会创建新的skb
,而是将数据添加到最后一个skb
的frag
中(skb
的数据量没有超过MTU
)。如果数据量很大,那么它会将其进行拆分。需要注意的是,这里的拆分并不是IP
的分片,具体的拆分过程会根据传递进去的getfrag
函数而不同。这里创建的skb
单个数据量都不会超过MTU
,而且skb
中没有构建IP
报头。
ip_push_pending_frames
这个函数一般都是与上面那个ip_append_data
配合使用的,即前者想将用户态的报文数据整理好存储到套接字发送队列中,此函数再将队列中的数据一次性发送出去。值得一提的是,当前队列中的所有数据都是一块的,也就是说他们都属于同一个上层(用户态)传递下来的数据,比如说一个大的UDP
数据块。
int ip_push_pending_frames(struct sock *sk, struct flowi4 *fl4)
{
struct sk_buff *skb;
skb = ip_finish_skb(sk, fl4);
if (!skb)
return 0;
/* Netfilter gets whole the not fragmented skb. */
return ip_send_skb(sock_net(sk), skb);
}
该函数首先调用了ip_finish_skb
,这个函数是__ip_make_skb
的封装。而__ip_make_skb
这个函数的作用我们之前也已经讲过了,它是用来将套接字发送队列中的skb
组装成一个skb
的。具体是怎么个逻辑呢?它取出发送队列queue
中的第一个skb
,并将其余的skb
一个接一个的连接到第一个skb
的frag_list
中,为后面的IP
分片做准备。第一个skb
的长度为所有skb
的数据部分总长度,且其余的skb
的data
均指向IP
数据部分(不包含IP
头部),只有第一个skb
拥有IP
头部。
struct sk_buff *__ip_make_skb(struct sock *sk,
struct flowi4 *fl4,
struct sk_buff_head *queue,
struct inet_cork *cork)
{
struct sk_buff *skb, *tmp_skb;
struct sk_buff **tail_skb;
struct inet_sock *inet = inet_sk(sk);
struct net *net = sock_net(sk);
struct ip_options *opt = NULL;
struct rtable *rt = (struct rtable *)cork->dst;
struct iphdr *iph;
__be16 df = 0;
__u8 ttl;
/* 取出队列中的第一个skb元素 */
skb = __skb_dequeue(queue);
if (!skb)
goto out;
/* 取出第一个元素的frag_list */
tail_skb = &(skb_shinfo(skb)->frag_list);
/* 将skb的data设置到IP头部 */
if (skb->data < skb_network_header(skb))
__skb_pull(skb, skb_network_offset(skb));
/* 循环取出queue中的所有skb,并逐个添加到第一个skb的frag_list中。 */
while ((tmp_skb = __skb_dequeue(queue)) != NULL) {
/* 将tmp_skb的data移动到IP报文的数据区域(排除IP头部) */
__skb_pull(tmp_skb, skb_network_header_len(skb));
*tail_skb = tmp_skb;
tail_skb = &(tmp_skb->next);
skb->len += tmp_skb->len;
skb->data_len += tmp_skb->len;
skb->truesize += tmp_skb->truesize;
tmp_skb->destructor = NULL;
tmp_skb->sk = NULL;
}
/* Unless user demanded real pmtu discovery (IP_PMTUDISC_DO), we allow
* to fragment the frame generated here. No matter, what transforms
* how transforms change size of the packet, it will come out.
*/
skb->ignore_df = ip_sk_ignore_df(sk);
/* DF bit is set when we want to see DF on outgoing frames.
* If ignore_df is set too, we still allow to fragment this frame
* locally. */
if (inet->pmtudisc == IP_PMTUDISC_DO ||
inet->pmtudisc == IP_PMTUDISC_PROBE ||
(skb->len <= dst_mtu(&rt->dst) &&
ip_dont_fragment(sk, &rt->dst)))
df = htons(IP_DF);
if (cork->flags & IPCORK_OPT)
opt = cork->opt;
if (cork->ttl != 0)
ttl = cork->ttl;
else if (rt->rt_type == RTN_MULTICAST)
ttl = inet->mc_ttl;
else
ttl = ip_select_ttl(inet, &rt->dst);
/* 构造IP头部 */
iph = ip_hdr(skb);
iph->version = 4;
iph->ihl = 5;
iph->tos = (cork->tos != -1) ? cork->tos : inet->tos;
iph->frag_off = df;
iph->ttl = ttl;
iph->protocol = sk->sk_protocol;
ip_copy_addrs(iph, fl4);
ip_select_ident(net, skb, sk);
if (opt) {
iph->ihl += opt->optlen>>2;
ip_options_build(skb, opt, cork->addr, rt, 0);
}
skb->priority = (cork->tos != -1) ? cork->priority: sk->sk_priority;
skb->mark = cork->mark;
skb->tstamp = cork->transmit_time;
/*
* Steal rt from cork.dst to avoid a pair of atomic_inc/atomic_dec
* on dst refcount
*/
cork->dst = NULL;
skb_dst_set(skb, &rt->dst);
if (iph->protocol == IPPROTO_ICMP)
icmp_out_count(net, ((struct icmphdr *)
skb_transport_header(skb))->type);
ip_cork_release(cork);
out:
return skb;
}
构造出来一个完整的skb
后,ip_send_skb
负责将这个报文发送出去。后面的发送流程与我们之前的关于IP
协议实现部分就是一致的了,流程为:
ip_send_skb -> ip_local_out -> __ip_local_out -> dst_output -> ip_output -> ip_finish_output
值得注意的是,在ip_finish_output
函数中会根据skb
的长度来选择是否需要对skb
进行分片,即真正的IP
分片是在这里进行的,具体的分片函数为:ip_fragment
。
ip_fragment
分片函数会先进行简单的检查,主要为DF
(Don't Fragment
)标志位的检查。
static int ip_fragment(struct net *net, struct sock *sk, struct sk_buff *skb,
unsigned int mtu,
int (*output)(struct net *, struct sock *, struct sk_buff *))
{
struct iphdr *iph = ip_hdr(skb);
/* 检查IP头部是否设置了DF,没有的话直接进行分片。 */
if ((iph->frag_off & htons(IP_DF)) == 0)
return ip_do_fragment(net, sk, skb, output);
/* IP头部设置了DF,且skb没有设置ignore_df,则返回错误。 */
if (unlikely(!skb->ignore_df ||
(IPCB(skb)->frag_max_size &&
IPCB(skb)->frag_max_size > mtu))) {
IP_INC_STATS(net, IPSTATS_MIB_FRAGFAILS);
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED,
htonl(mtu));
kfree_skb(skb);
return -EMSGSIZE;
}
return ip_do_fragment(net, sk, skb, output);
}
检查都通过后,调用ip_do_fragment
来对报文进行真正的分片。其实分片要做的工作不多,因为skb
的构造在之前就已经完成了,主要包括以下几点:
- 对于具有片段的
skb
,将第一个skb
的IP
头部拷贝到frag_list
中所有的skb
里; - 计算
IP
报头中分片的偏移量; - 调用
output
将报文发送出去。
以下代码为针对frag_list
的IP
分片。如果存在frag_list
,就意味着队列中的每个skb
的长度都不会大于MTU
,只需要进行IP
头部的封装就行了。
int ip_do_fragment(struct net *net, struct sock *sk, struct sk_buff *skb,
int (*output)(struct net *, struct sock *, struct sk_buff *))
{
struct iphdr *iph;
struct sk_buff *skb2;
struct rtable *rt = skb_rtable(skb);
unsigned int mtu, hlen, ll_rs;
struct ip_fraglist_iter iter;
ktime_t tstamp = skb->tstamp;
struct ip_frag_state state;
int err = 0;
/* for offloaded checksums cleanup checksum before fragmentation */
if (skb->ip_summed == CHECKSUM_PARTIAL &&
(err = skb_checksum_help(skb)))
goto fail;
/*
* 提取出第一个skb的IP头部地址,作为所有片段的IP头部.
*/
iph = ip_hdr(skb);
/* 计算分片的MTU。 */
mtu = ip_skb_dst_mtu(sk, skb);
if (IPCB(skb)->frag_max_size && IPCB(skb)->frag_max_size < mtu)
mtu = IPCB(skb)->frag_max_size;
/*
* Setup starting values.
*/
hlen = iph->ihl * 4;
mtu = mtu - hlen; /* Size of data space */
IPCB(skb)->flags |= IPSKB_FRAG_COMPLETE;
ll_rs = LL_RESERVED_SPACE(rt->dst.dev);
/*
* 检查到skb存在frag_list,进入frag_list分片流程。
*/
if (skb_has_frag_list(skb)) {
struct sk_buff *frag, *frag2;
/* 第一个skb线性和frags数据长度的总和,不包括分片队列的长度。 */
unsigned int first_len = skb_pagelen(skb);
/* 如果去除了分片队列的长度后,skb的长度还是大于MTU,则进入到slow_path流程,即线性数据IP分片流程 */
if (first_len - hlen > mtu ||
((first_len - hlen) & 7) ||
ip_is_fragment(iph) ||
skb_cloned(skb) ||
skb_headroom(skb) < ll_rs)
goto slow_path;
/* 遍历skb的分片队列,检查每个片段是否有效。 */
skb_walk_frags(skb, frag) {
/* 对于长度超标或者headroom不足的报文,进入到slow_path_clean流程。该流程会恢复skb片段的初始状态,并进入slow_path处理流程。 */
if (frag->len > mtu ||
((frag->len & 7) && frag->next) ||
skb_headroom(frag) < hlen + ll_rs)
goto slow_path_clean;
/* Partially cloned skb? */
if (skb_shared(frag))
goto slow_path_clean;
BUG_ON(frag->sk);
if (skb->sk) {
frag->sk = skb->sk;
frag->destructor = sock_wfree;
}
skb->truesize -= frag->truesize;
}
/* 生成IP头部校验码、设置MF标志、设置IP头部正确的数据长度等。 */
ip_fraglist_init(skb, iph, hlen, &iter);
/* 使用迭代器进入片段发送流程。 */
for (;;) {
/* Prepare header of the next frame,
* before previous one went down. */
if (iter.frag) {
/* 拷贝上一个skb的cb部分内容到当前片段。 */
ip_fraglist_ipcb_prepare(skb, &iter);
/* 拷贝上一个skb的IP头部部分内容到当前片段,设置分片偏移量,计算校验码等。 */
ip_fraglist_prepare(skb, &iter);
}
/* 设置时间戳,并调用output进行报文的发送。 */
skb->tstamp = tstamp;
err = output(net, sk, skb);
if (!err)
IP_INC_STATS(net, IPSTATS_MIB_FRAGCREATES);
if (err || !iter.frag)
break;
skb = ip_fraglist_next(&iter);
}
if (err == 0) {
IP_INC_STATS(net, IPSTATS_MIB_FRAGOKS);
return 0;
}
kfree_skb_list(iter.frag);
IP_INC_STATS(net, IPSTATS_MIB_FRAGFAILS);
return err;
下面部分的代码是进入到线性数据分片的流程:
/* 清空之前设置的属性。 */
slow_path_clean:
skb_walk_frags(skb, frag2) {
if (frag2 == frag)
break;
frag2->sk = NULL;
frag2->destructor = NULL;
skb->truesize += frag2->truesize;
}
}
slow_path:
/* 初始化state对象,该对象用于存储分片过程中用到的一些信息,包括MTU、剩余数据长度、分片偏移等。 */
ip_frag_init(skb, hlen, ll_rs, mtu, IPCB(skb)->flags & IPSKB_FRAG_PMTU,
&state);
/* 循环拷贝skb中的数据 */
while (state.left > 0) {
/* 根据偏移量判断当前是否是第一个IP分片。 */
bool first_frag = (state.offset == 0);
/* 分配一个新的skb2,并将skb中的元数据、IP报头、以及剩余的数据(长度不超过MTU)拷贝进去,设置分片偏移量、计算校验码等。注意,在进行数据拷贝时,会依次从线性区域、frags和frag_list中取数据。 */
skb2 = ip_frag_next(skb, &state);
if (IS_ERR(skb2)) {
err = PTR_ERR(skb2);
goto fail;
}
/* 拷贝IP标志和选项。 */
ip_frag_ipcb(skb, skb2, first_frag, &state);
/*
* Put this fragment into the sending queue.
*/
skb2->tstamp = tstamp;
/* 发送出去。 */
err = output(net, sk, skb2);
if (err)
goto fail;
IP_INC_STATS(net, IPSTATS_MIB_FRAGCREATES);
}
consume_skb(skb);
IP_INC_STATS(net, IPSTATS_MIB_FRAGOKS);
return err;
fail:
kfree_skb(skb);
IP_INC_STATS(net, IPSTATS_MIB_FRAGFAILS);
return err;
}
至此,整个IP
的分片过程就完成了。
从以上的内容中我们可以看出,虽然IP_HDRINCL
类型的套接字不支持IP
分片,那也只是在一开始长度检查的时候进行了限制,如果把raw_send_hdrinc
里长度检查部分去掉,后面的流程里是可以进行正常的IP
分片的。
2.2 UDP
报文分片
UDP
报文发送的流程为:
udp_sendmsg -> __ip_append_data -> __ip_make_skb -> ip_send_skb -> ip_local_out -> __ip_local_out -> dst_output -> ip_output -> ip_finish_output
。
可以看出来,UDP
的发送与原始套接字的流程基本上相似,只不过在udp_sendmsg
函数中,内核会对用户态传递进来的UDP
数据封装UDP
头部。