UDP corking
在变量声明和一些基本的错误检查之后,udp_sendmsg
要做的第一件事就是检查套接字是否“corked”。 UDP corking 是一项特性,允许用户程序请求内核累积多次 send
调用的数据到单个数据报中发送。 在用户程序中有两种方法可启用此选项:
- 使用
setsockopt
系统调用,传递UDP_CORK
套接字选项。 - 调用
send
、sendto
或sendmsg
时,传递带有MSG_MORE
的flags
。
以上选项分别记录在 UDP 手册页 和 send / sendto / sendmsg 手册页 。
udp_sendmsg
检查 up->pending
以确定套接字当前是否被 corked。如果是,则直接追加数据。 稍后将看到如何追加数据。
int udp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, size_t len) { /* variables and error checking ... */ fl4 = &inet->cork.fl.u.ip4; if (up->pending) { /* * There are pending frames. * The socket lock must be held while it's corked. */ lock_sock(sk); if (likely(up->pending)) { if (unlikely(up->pending != AF_INET)) { release_sock(sk); return -EINVAL; } goto do_append_data; } release_sock(sk); }
获取 UDP 目标地址和端口
接下来,从两个可能的来源之一确定目标地址和端口:
- 套接字本身存储的目标地址,因为套接字在某个时间点已连接。
- 辅助结构传入的地址,正如在
sendto
的内核代码中看到的那样。
内核处理逻辑如下:
/* * Get and verify the address. */ if (msg->msg_name) { struct sockaddr_in *usin = (struct sockaddr_in *)msg->msg_name; if (msg->msg_namelen < sizeof(*usin)) return -EINVAL; if (usin->sin_family != AF_INET) { if (usin->sin_family != AF_UNSPEC) return -EAFNOSUPPORT; } daddr = usin->sin_addr.s_addr; dport = usin->sin_port; if (dport == 0) return -EINVAL; } else { if (sk->sk_state != TCP_ESTABLISHED) return -EDESTADDRREQ; daddr = inet->inet_daddr; dport = inet->inet_dport; /* Open fast path for connected socket. Route will not be used, if at least one option is set. */ connected = 1; }
是的,UDP 协议层使用 TCP_ESTABLISHED
! 不管怎样,套接字状态都使用 TCP 状态描述。
回想一下前面看到的,当用户程序调用 sendto
时,内核是如何代表用户组装一个 struct msghdr
结构。 上面的代码显示了内核解析该数据设置 daddr
和 dport
。
当内核函数访问 udp_sendmsg
函数时,内核函数没有构造 struct msghdr
结构,则从套接字本身获取目标地址和端口,并标记套接字为“已连接”。
两种情况下,都设置 daddr
和 dport
为目标地址和端口。
套接字传输簿记和时间戳
接下来,获取并存储套接字上设置的源地址、设备索引和时间戳选项(如SOCK_TIMESTAMPING_TX_HARDWARE
、SOCK_TIMESTAMPING_TX_SOFTWARE
、SOCK_WIFI_STATUS
):
ipc.addr = inet->inet_saddr; ipc.oif = sk->sk_bound_dev_if; sock_tx_timestamp(sk, &ipc.tx_flags);
sendmsg
发送辅助消息
除了发送或接收数据包之外,sendmsg
和 recvmsg
系统调用还允许用户设置或请求辅助数据。 用户程序可以创建一个嵌入了请求的 struct msghdr
,来使用这些辅助数据。许多辅助数据类型都记录在 IP 手册页 中。
辅助数据的一个常见例子是 IP_PKTINFO
。 在 sendmsg
的情况下,此数据类型允许程序设置 struct in_pktinfo
,以便发送数据时使用。 通过在结构 struct in_pktinfo
中填充字段,程序可以指定要在数据包上使用的源地址。 如果程序是侦听多个 IP 地址的服务器程序,这是一个有用的选项。 在这种情况下,服务器程序可能希望使用与客户端连接服务器的 IP 地址来回复客户端。IP_PKTINFO
恰好适合这种情况。
类似地,当用户程序向 sendmsg
传递数据时, IP_TTL
和 IP_TOS
辅助消息允许用户在每个数据包的级别设置 IP 数据包的 TTL 和 TOS 值。如果需要,也可以通过使用 setsockopt
设置 IP_TTL
和 IP_TOS
在套接字级别,生效套接字的所有传出数据包。 Linux 内核使用数组转换指定的 TOS 值为优先级。 优先级影响数据包从排队规则传输的方式和时间。 稍后会详细了解这意味着什么。
内核如何处理 sendmsg
在 UDP 套接字上的辅助消息:
if (msg->msg_controllen) { err = ip_cmsg_send(sock_net(sk), msg, &ipc, sk->sk_family == AF_INET6); if (err) return err; if (ipc.opt) free = 1; connected = 0; }
./net/ipv4/ip_sockglue. c 中的 ip_cmsg_send
负责辅助消息的内部解析。 请注意,只要提供任何辅助数据,都会标记该套接字为未连接。
设置自定义 IP 选项
接下来,sendmsg
检查用户是否指定了任何带有自定义 IP 选项的辅助消息。 如果设置了选项,则使用这些选项。 如果没有,则使用此套接字已在使用的选项:
if (!ipc.opt) { struct ip_options_rcu *inet_opt; rcu_read_lock(); inet_opt = rcu_dereference(inet->inet_opt); if (inet_opt) { memcpy(&opt_copy, inet_opt, sizeof(*inet_opt) + inet_opt->opt.optlen); ipc.opt = &opt_copy.opt; } rcu_read_unlock(); }
接下来,该函数检查是否设置了源记录路由(SRR)IP 选项。 源记录路由有两种类型:宽松源记录路由和严格源记录路由。 如果设置了此选项,记录并存储第一跳地址为 faddr
,标记套接字为“未连接”。 faddr
将在后面用到:
ipc.addr = faddr = daddr; if (ipc.opt && ipc.opt->opt.srr) { if (!daddr) return -EINVAL; faddr = ipc.opt->opt.faddr; connected = 0; }
在处理 SRR 选项后,从用户辅助消息设置的值,或套接字当前使用的值中,获取 TOS IP 标志。 随后进行检查以确定:
- 套接字是否已设置(使用
setsockopt
)SO_DONTROUTE
,或 - 调用
sendto
或sendmsg
时,是否已指定MSG_DONTROUTE
标志,或 - 是否已设置
is_strictroute
,代表需要严格源记录路由
然后,置位 tos
的 0x1
(RTO_ONLINK
)位,且标记套接字为“未连接”:
tos = get_rttos(&ipc, inet); if (sock_flag(sk, SOCK_LOCALROUTE) || (msg->msg_flags & MSG_DONTROUTE) || (ipc.opt && ipc.opt->opt.is_strictroute)) { tos |= RTO_ONLINK; connected = 0; }
组播还是单播?
接下来,代码尝试处理组播。 这有点棘手,因为如前所述,用户可以发送辅助 IP_PKTINFO
消息来指定一个源地址或设备索引来发送数据包。
如果目标地址是组播地址:
- 设置组播设备索引为数据包发送的设备索引,并且
- 设置组播源地址为数据包的源地址。
除非用户发送 IP_PKTINFO
辅助消息覆盖设备索引。 我们来看一下:
if (ipv4_is_multicast(daddr)) { if (!ipc.oif) ipc.oif = inet->mc_index; if (!saddr) saddr = inet->mc_addr; connected = 0; } else if (!ipc.oif) ipc.oif = inet->uc_index;
如果目标地址不是组播地址,则会设置设备索引,除非用户使用 IP_PKTINFO
覆盖了该索引。
路由
是时候探讨路由了!
UDP 层负责路由的代码从一个快速路径开始。如果套接字已连接,请尝试获取路由结构:
if (connected) rt = (struct rtable *)sk_dst_check(sk, 0);
如果套接字没有连接,或者虽然连接了,但路由助手 sk_dst_check
判定路由已淘汰,则代码进入慢速路径以生成路由结构。 首先调用 flowi4_init_output
来构造一个描述此 UDP 流的结构:
if (rt == NULL) { struct net *net = sock_net(sk); fl4 = &fl4_stack; flowi4_init_output(fl4, ipc.oif, sk->sk_mark, tos, RT_SCOPE_UNIVERSE, sk->sk_protocol, inet_sk_flowi_flags(sk)|FLOWI_FLAG_CAN_SLEEP, faddr, saddr, dport, inet->inet_sport);
一旦该流结构构造完成,套接字及其流结构就被传递到安全子系统,使得诸如 SELinux 或 SMACK 之类的系统可以在流结构上设置安全 id 值。 接下来,ip_route_output_flow
调用 IP 路由代码来生成此流的路由结构:
security_sk_classify_flow(sk, flowi4_to_flowi(fl4)); rt = ip_route_output_flow(net, fl4, sk);
如果无法生成路由结构,并且错误为 ENETUNREACH
,则 OUTNOROUTES
统计计数器增加。
if (IS_ERR(rt)) { err = PTR_ERR(rt); rt = NULL; if (err == -ENETUNREACH) IP_INC_STATS(net, IPSTATS_MIB_OUTNOROUTES); goto out; }
保存上述统计计数器的文件的位置、其他计数器及其含义,将在下面的 UDP 监控章节中讨论。
接下来,如果路由用于广播,但是在套接字上没有设置 SOCK_BROADCAST
套接字选项,则代码终止。 如果套接字“已连接”(如本函数所述),则缓存路由结构到套接字:
err = -EACCES; if ((rt->rt_flags & RTCF_BROADCAST) && !sock_flag(sk, SOCK_BROADCAST)) goto out; if (connected) sk_dst_set(sk, dst_clone(&rt->dst));
使用 MSG_CONFIRM
阻止 ARP 缓存失效
在调用 send
、sendto
或 sendmsg
时,如果用户指定了 MSG_CONFIRM
标志,UDP 协议层将处理该标志:
if (msg->msg_flags&MSG_CONFIRM) goto do_confirm; back_from_confirm:
此标志指示系统确认 ARP 缓存条目仍然有效,并阻止其被垃圾回收。 dst_confirm
函数只是在目标缓存条目上设置一个标志,在查询邻居缓存并找到条目时再次检查该标志。我们稍后再看。 UDP 网络应用程序常使用此功能 ,以减少不必要的 ARP 流量。 do_confirm
标签位于此函数的末尾附近,但它很简单:
do_confirm: dst_confirm(&rt->dst); if (!(msg->msg_flags&MSG_PROBE) || len) goto back_from_confirm; err = 0; goto out;
这段代码确认缓存条目,如果不是探测消息,则跳回到 back_from_confirm
。
一旦 do_confirm
代码跳回到 back_from_confirm
(或者没有跳转 do_confirm
),代码会尝试处理 UDP cork 和 uncorked 的情况。
uncorked UDP 套接字的快速路径:准备传输数据
如果未请求 UDP corking,调用 ip_make_skb
,数据可以打包到 struct sk_buff
,并传递给 udp_send_skb
,以向下移动栈并更接近 IP 协议层。 请注意,前面调用 ip_route_output_flow
生成的路由结构也会传入。 它将被关联到 skb,并稍后在 IP 协议层中使用。
/* Lockless fast path for the non-corking case. */ if (!corkreq) { skb = ip_make_skb(sk, fl4, getfrag, msg->msg_iov, ulen, sizeof(struct udphdr), &ipc, &rt, msg->msg_flags); err = PTR_ERR(skb); if (!IS_ERR_OR_NULL(skb)) err = udp_send_skb(skb, fl4); goto out; }
ip_make_skb
函数尝试构建一个 skb,其考虑了各种因素,例如:
大多数网络设备驱动程序不支持 UFO,因为网络硬件本身不支持此功能。 让我们看一下这段代码,记住 corking 是禁用的。 接下来我们查看启用 corking 的路径。
ip_make_skb
ip_make_skb
函数可以在 ./net/ipv4/ip_output.c 中找到。 这个函数有点棘手。 ip_make_skb
依赖底层代码(译者释:__ip_make_skb
)构建 skb,它需要传入一个 corking 结构和 skb 排队的队列。 在套接字没有 corked 的情况下,传入一个伪 corking 结构和空队列。
让我们来看看伪 corking 结构和队列是如何构造的:
struct sk_buff *ip_make_skb(struct sock *sk, /* more args */) { struct inet_cork cork; struct sk_buff_head queue; int err; if (flags & MSG_PROBE) return NULL; __skb_queue_head_init(&queue); cork.flags = 0; cork.addr = 0; cork.opt = NULL; err = ip_setup_cork(sk, &cork, /* more args */); if (err) return ERR_PTR(err);
如上所述,corking 结构(cork
)和队列(queue
)都在栈上分配的;当 ip_make_skb
完成时,两者都不再需要。 调用 ip_setup_cork
来构建伪 corking 结构,它分配内存、并初始化结构。 接下来,调用 __ip_append_data
,传入队列和 corking 结构:
err = __ip_append_data(sk, fl4, &queue, &cork, ¤t->task_frag, getfrag, from, length, transhdrlen, flags);
稍后我们将看到这个函数是如何工作的,因为它在套接字是否被 corked 的情况下都会使用。 现在,我们只需要知道 __ip_append_data
会创建一个 skb,向其追加数据,并添加该 skb 到传入的队列中。 如果追加数据失败,则调用 __ip_flush_pending_frame
静默丢弃数据,并向上返回错误码:
if (err) { __ip_flush_pending_frames(sk, &queue, &cork); return ERR_PTR(err); }