最后,如果没有错误发生,__ip_make_skb
出队队列中的 skb,添加 IP 选项,并返回一个 skb,该 skb 已准备好传递给底层发送:
return __ip_make_skb(sk, fl4, &queue, &cork);
传输数据!
如果没有发生错误,则 skb 会交给 udp_send_skb
,它传递 skb 到网络栈的下一层,即 IP 协议栈:
err = PTR_ERR(skb); if (!IS_ERR_OR_NULL(skb)) err = udp_send_skb(skb, fl4); goto out;
如果出现错误,将在稍后计数。 有关详细信息,请参阅 UDP corking 的“错误统计”部分。
corked UDP 套接字的慢速路径:没有预先存在的 corked 数据
如果正在使用 UDP corking,但没有预先存在的 corked 数据,则慢速路径开始:
- 锁定套接字。
- 检查应用程序缺陷:corked 套接字被 “re-corked”。
- 准备此 UDP 流的流结构,以进行 corking。
- 追加要发送的数据到现有数据。
你可以在下一段代码中看到这一点,udp_sendmsg
继续向下:
lock_sock(sk); if (unlikely(up->pending)) { /* The socket is already corked while preparing it. */ /* ... which is an evident application bug. --ANK */ release_sock(sk); LIMIT_NETDEBUG(KERN_DEBUG pr_fmt("cork app bug 2\n")); err = -EINVAL; goto out; } /* * Now cork the socket to pend data. */ fl4 = &inet->cork.fl.u.ip4; fl4->daddr = daddr; fl4->saddr = saddr; fl4->fl4_dport = dport; fl4->fl4_sport = inet->inet_sport; up->pending = AF_INET; do_append_data: up->len += ulen; err = ip_append_data(sk, fl4, getfrag, msg->msg_iov, ulen, sizeof(struct udphdr), &ipc, &rt, corkreq ? msg->msg_flags|MSG_MORE : msg->msg_flags);
ip_append_data
ip_append_data
是一个小的包装函数,它在调用 __ip__append_data
之前做两件主要事情:
- 检查用户是否传入了
MSG_PROBE
标志。 此标志表示用户不想真正发送数据。 应探测路径(例如,以确定 PMTU)。 - 检查套接字的发送队列是否为空。 如果是,意味着没有待处理的 corking 数据,因此调用
ip_setup_cork
来设置 corking。
处理完上述条件后,就会调用 __ip_append_data
函数,该函数包含大量逻辑以处理数据为数据包。
__ip_append_data
如果套接字被 corked,则从 ip_append_data
调用该函数;如果套接字未被 corked ,则从 ip_make_skb
调用该函数。 在这两种情况下,该函数要么分配一个新的缓冲区来存储传入的数据,要么追加数据到现有数据中。
这种工作方式以套接字的发送队列为中心。 等待发送的现有数据(例如,如果套接字被 corked)在队列中有一个条目,可以在其中追加其他数据。
这个函数很复杂;它执行多轮计算,以确定如何构建传递给底层网络层的 skb,并且详细探讨缓冲器分配过程对于理解如何传输网络数据并非绝对必要。
该函数的重点包括:
- 处理 UDP fragmentation offloading(UFO)(如果硬件支持)。 绝大多数网络硬件不支持 UFO。 如果您的网卡驱动程序支持,它将设置功能标志
NETIF_F_UFO
。 - 处理支持 分散/聚集 IO 的网卡。 许多卡都支持此功能,并使用
NETIF_F_SG
功能标志进行通告。 该功能的可用性表明,网络卡能够处理数据分散在一组缓冲区中的数据包;内核不需要花费时间合并多个缓冲区为单个缓冲区。期望的是结果避免额外的复制,大多数网卡都支持该功能。 - 调用
sock_wmalloc
跟踪发送队列的大小。 当分配一个新的 skb 时,skb 的大小会被计入拥有它的套接字,并且套接字的发送队列的分配字节会增加。 如果发送队列中没有足够的空间,则不分配 skb,并返回并跟踪错误。 我们将在下面的调优部分看到如何设置套接字发送队列大小。 - 增加错误统计信息。 此函数中的任何错误都将增加 “discard”。 我们将在下面的监控部分看到如何读取这个值。
此函数执行成功后,将返回 0
。此时传输的数据已组装成适合网络设备的 skb,等待在发送队列上。
在 uncorked 的情况下,持有 skb 的队列传递给上述的 __ip_make_skb
,在那里它出队并准备经由 udp_send_skb
发送到更低层。
在 corked 的情况下,向上传递 __ip_append_data
的返回值。 数据停留在发送队列中,直到udp_sendmsg
确定是时候调用 udp_push_pending_frames
确认 skb 并调用 udp_send_skb
。
刷新 corked 套接字
现在,udp_sendmsg
继续检查 ___ip_append_skb
的返回值 (下面的 err
):
if (err) udp_flush_pending_frames(sk); else if (!corkreq) err = udp_push_pending_frames(sk); else if (unlikely(skb_queue_empty(&sk->sk_write_queue))) up->pending = 0; release_sock(sk);
让我们来看看每个分支:
- 如果出现错误(
err
非零),则调用udp_flush_pending_frames
,从而取消阻塞并从套接字的发送队列中删除所有数据。 - 如果发送此数据时未指定
MSG_MORE
,则称为udp_push_pending_frames
,它尝试传递数据到较低的网络层。 - 如果发送队列为空,则标记套接字为不再阻塞。
如果 append 操作成功完成,并且还有更多的数据要 cork,则代码继续清理并返回所追加的数据的长度:
ip_rt_put(rt); if (free) kfree(ipc.opt); if (!err) return len;
这就是内核处理 corked 的 UDP 套接字的方式。
错误统计
如果:
- non-corking 快速路径无法创建 skb 或
udp_send_skb
报告错误,或 ip_append_data
无法追加数据到 corked 的 UDP 套接字,或- 在尝试传输 corked skb 时,
udp_push_pending_frames
返回从udp_send_skb
收到的错误
只有当收到的错误是 ENOBUFS
(没有可用的内核内存)或套接字设置了 SOCK_NOSPACE
(发送队列已满)时,SNDBUFERRORS
统计信息才会增加:
/* * ENOBUFS = no kernel mem, SOCK_NOSPACE = no sndbuf space. Reporting * ENOBUFS might not be good (it's not tunable per se), but otherwise * we don't have a good statistic (IpOutDiscards but it can be too many * things). We could add another new stat but at least for now that * seems like overkill. */ if (err == -ENOBUFS || test_bit(SOCK_NOSPACE, &sk->sk_socket->flags)) { UDP_INC_STATS_USER(sock_net(sk), UDP_MIB_SNDBUFERRORS, is_udplite); } return err;
我们将在下面的监控部分看到如何读取这些计数。
udp_send_skb
udp_sendmsg
调用 udp_send_skb
函数 最终下推 skb 到网络栈的下一层,在本例中是 IP 协议层。 该函数做了几件重要的事情:
- 添加 UDP 报头到 skb。
- 处理校验和:软件校验和、硬件校验和或无校验和(如果禁用)。
- 尝试调用
ip_send_skb
发送 skb 到 IP 协议层。 - 增加传输成功或失败的统计计数器。
我们来看看。 首先,创建 UDP 报头:
static int udp_send_skb(struct sk_buff *skb, struct flowi4 *fl4) { /* useful variables ... */ /* * 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;
接下来,处理校验和。 有几种情况:
- 首先处理 UDP-Lite 校验和。
- 接下来,如果套接字被设置为不生成校验和(通过
setsockopt
设置SO_NO_CHECK
),将如此标记 skb。 - 接下来,如果硬件支持 UDP 校验和,调用
udp4_hwcsum
来设置。 请注意,如果数据包被分段,内核将在软件中生成校验和。 您可以在udp4_hwcsum
的源代码中看到这一点。 - 最后,调用
udp_csum
生成软件校验和。
if (is_udplite) /* UDP-Lite */ csum = udplite_csum(skb); else if (sk->sk_no_check == UDP_CSUM_NOXMIT) { /* UDP csum disabled */ 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);
接下来,添加 psuedo 报头:
uh->check = csum_tcpudp_magic(fl4->saddr, fl4->daddr, len, sk->sk_protocol, csum); if (uh->check == 0) uh->check = CSUM_MANGLED_0;
如果校验和为 0,则根据 RFC 768 设置其等效的补码值为校验和。最终,skb 被传递到 IP 协议栈,增加统计信息:
send: err = ip_send_skb(sock_net(sk), skb); if (err) { if (err == -ENOBUFS && !inet->recverr) { UDP_INC_STATS_USER(sock_net(sk), UDP_MIB_SNDBUFERRORS, is_udplite); err = 0; } } else UDP_INC_STATS_USER(sock_net(sk), UDP_MIB_OUTDATAGRAMS, is_udplite); return err;
如果 ip_send_skb
执行成功,则增加 OUTDATAGRAMS
统计信息。 如果 IP 协议层报告错误,则增加 SNDBUFERRORS
,但仅当错误为 ENOBUFS
(内核内存不足)且未启用错误队列时,才增加。
在讨论 IP 协议层之前,让我们先看看如何在 Linux 内核中监控和调优 UDP 协议层。
监控:UDP 协议层统计信息
获取 UDP 协议统计信息的两个非常有用的文件是:
/proc/net/snmp
/proc/net/udp
/proc/net/snmp
读取 /proc/net/snmp
监控详细的 UDP 协议统计信息。
$ cat /proc/net/snmp | grep Udp\: Udp: InDatagrams NoPorts InErrors OutDatagrams RcvbufErrors SndbufErrors Udp: 16314 0 0 17161 0 0
为了准确地理解这些统计信息在哪里增加,您需要仔细阅读内核源代码。 在一些情况下,一些错误会计入多个统计量中。
InDatagrams
:当用户程序使用recvmsg
读取数据报时增加。 当 UDP 数据包被封装并发回处理时,也会增加。NoPorts
:当 UDP 数据包到达目的地为没有程序侦听的端口时增加。InErrors
:在以下几种情况下增加:接收队列中没有内存,当看到错误的校验和时,sk_add_backlog
无法添加数据报。OutDatagrams
:当 UDP 数据包无错误地传递到要发送的 IP 协议层时增加。RcvbufErrors
:当sock_queue_rcv_skb
报告没有可用内存时增加;如果sk->sk_rmem_alloc
大于等于sk->sk_rcvbuf
就会发生这种情况。SndbufErrors
:如果 IP 协议层在尝试发送数据包时报告错误,并且没有设置错误队列,则会增加。 如果没有可用的发送队列空间或内核内存,也会增加。InCsumErrors
:检测到 UDP 校验和失败时增加。 请注意,在我能找到的所有情况下,InCsumErrors
与InErrors
会同时增加。 因此,InErrors
-InCsumErros
应当得出接收端的内存相关错误的计数。
请注意,UDP 协议层发现的一些错误会报告到其他协议层的统计信息文件。 举个例子:路由错误。 udp_sendmsg
发现的路由错误将增加 IP 协议层的 OutNoRoutes
统计信息。
/proc/net/udp
读取 /proc/net/udp
监控 UDP 套接字统计信息
$ cat /proc/net/udp sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode ref pointer drops 515: 00000000:B346 00000000:0000 07 00000000:00000000 00:00000000 00000000 104 0 7518 2 0000000000000000 0 558: 00000000:0371 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 7408 2 0000000000000000 0 588: 0100007F:038F 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 7511 2 0000000000000000 0 769: 00000000:0044 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 7673 2 0000000000000000 0 812: 00000000:006F 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 7407 2 0000000000000000 0
第一行描述后续行中的每个字段:
sl
:套接字的内核哈希槽local_address
:套接字的十六进制本地地址和端口号,以:
分隔。rem_address
:套接字的十六进制远程地址和端口号,以:
分隔。st
:套接字的状态。 奇怪的是,UDP 协议层似乎使用了一些 TCP 套接字状态。 在上面的例子中,7
是TCP_CLOSE
。tx_queue
:内核中为传出 UDP 数据报分配的内存量。rx_queue
:内核中为传入 UDP 数据报分配的内存量。tr
,tm->when
,retrnsmt
:UDP 协议层未使用这些字段。uid
:创建此套接字的用户的有效用户 ID。timeout
:UDP 协议层未使用。inode
:与此套接字对应的 inode 编号。 您可以使用它来帮助您确定哪个用户进程打开了此套接字。 检查/proc/[pid]/fd
,它将包含到socket:[inode]
的符号链接。ref
:套接字的当前引用计数。pointer
:内核中struct sock
的内存地址。drops
:与此套接字关联的数据报丢弃数。 请注意,这不包括任何与发送数据报有关的丢弃(在 corked 的 UDP 套接字上,或其他);在本博客考察的内核版本中,只在接收路径中增加。
可以在 net/ipv4/udp.c
中找到输出此内容的代码。
调优:套接字发送队列内存
发送队列(也称为写入队列)的最大大小可以设置 net.core.wmem_max
sysctl 来调整
设置 sysctl
增加最大发送缓冲区大小。
$ sudo sysctl -w net.core.wmem_max=8388608
sk->sk_write_queue
从 net.core.wmem_default
值开始,也可以设置 sysctl 来调整,如下所示:
设置 sysctl
来调整默认的初始发送缓冲区大小 。
$ sudo sysctl -w net.core.wmem_default=8388608
您还可以从应用程序调用 setsockopt
并传递 SO_SNDBUF
来设置 sk->sk_write_queue
大小 。 您可以使用 setsockopt
设置的最大值是 net.core.wmem_max
。
但是,当运行应用程序的用户具有 CAP_NET_ADMIN
权限时,可以调用 setsockopt
并传递 SO_SNDBUFFORCE
来覆盖 net.core.wmem_max
限制。
每次调用 ip_append_data
分配 skb 时,sk->sk_wmem_alloc
都会增加。 正如我们将看到的,UDP 数据报传输很快,通常不会在发送队列中花费太多时间。