译|Monitoring and Tuning the Linux Networking Stack: Sending Data(二)

简介: 译|Monitoring and Tuning the Linux Networking Stack: Sending Data(二)

UDP corking

在变量声明和一些基本的错误检查之后,udp_sendmsg 要做的第一件事就是检查套接字是否“corked”。 UDP corking 是一项特性,允许用户程序请求内核累积多次 send 调用的数据到单个数据报中发送。 在用户程序中有两种方法可启用此选项:

  1. 使用 setsockopt 系统调用,传递 UDP_CORK 套接字选项。
  2. 调用 sendsendtosendmsg 时,传递带有 MSG_MOREflags

以上选项分别记录在 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 目标地址和端口

接下来,从两个可能的来源之一确定目标地址和端口:

  1. 套接字本身存储的目标地址,因为套接字在某个时间点已连接。
  2. 辅助结构传入的地址,正如在 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 结构。 上面的代码显示了内核解析该数据设置 daddrdport

当内核函数访问 udp_sendmsg 函数时,内核函数没有构造 struct msghdr 结构,则从套接字本身获取目标地址和端口,并标记套接字为“已连接”。

两种情况下,都设置 daddrdport 为目标地址和端口。

套接字传输簿记和时间戳

接下来,获取并存储套接字上设置的源地址、设备索引和时间戳选项(如SOCK_TIMESTAMPING_TX_HARDWARESOCK_TIMESTAMPING_TX_SOFTWARESOCK_WIFI_STATUS):

ipc.addr = inet->inet_saddr;
ipc.oif = sk->sk_bound_dev_if;
sock_tx_timestamp(sk, &ipc.tx_flags);
sendmsg 发送辅助消息

除了发送或接收数据包之外,sendmsgrecvmsg 系统调用还允许用户设置或请求辅助数据。 用户程序可以创建一个嵌入了请求的 struct msghdr,来使用这些辅助数据。许多辅助数据类型都记录在 IP 手册页 中。

辅助数据的一个常见例子是 IP_PKTINFO。 在 sendmsg 的情况下,此数据类型允许程序设置 struct in_pktinfo,以便发送数据时使用。 通过在结构 struct in_pktinfo 中填充字段,程序可以指定要在数据包上使用的源地址。 如果程序是侦听多个 IP 地址的服务器程序,这是一个有用的选项。 在这种情况下,服务器程序可能希望使用与客户端连接服务器的 IP 地址来回复客户端。IP_PKTINFO 恰好适合这种情况。

类似地,当用户程序向 sendmsg 传递数据时, IP_TTLIP_TOS 辅助消息允许用户在每个数据包的级别设置 IP 数据包的 TTLTOS 值。如果需要,也可以通过使用 setsockopt 设置 IP_TTLIP_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 标志。 随后进行检查以确定:

  • 套接字是否已设置(使用 setsockoptSO_DONTROUTE ,或
  • 调用 sendtosendmsg 时,是否已指定 MSG_DONTROUTE 标志,或
  • 是否已设置 is_strictroute ,代表需要严格源记录路由

然后,置位 tos0x1RTO_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 消息来指定一个源地址或设备索引来发送数据包。

如果目标地址是组播地址:

  1. 设置组播设备索引为数据包发送的设备索引,并且
  2. 设置组播源地址为数据包的源地址。

除非用户发送 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);

一旦该流结构构造完成,套接字及其流结构就被传递到安全子系统,使得诸如 SELinuxSMACK 之类的系统可以在流结构上设置安全 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 缓存失效

在调用 sendsendtosendmsg 时,如果用户指定了 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,其考虑了各种因素,例如:

  • MTU
  • UDP corking(如果启用)。
  • UDP Fragmentation Offloading(UFO)。
  • Fragmentation,如果不支持 UFO ,并且传输数据大于 MTU。

大多数网络设备驱动程序不支持 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,
                       &current->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);
}


目录
相关文章
|
7月前
|
监控 Linux
Linux的epoll用法与数据结构data、event
Linux的epoll用法与数据结构data、event
92 0
|
运维 监控 网络协议
译|llustrated Guide to Monitoring and Tuning the Linux Networking Stack: Receiving Data
译|llustrated Guide to Monitoring and Tuning the Linux Networking Stack: Receiving Data
143 0
|
Linux
linux下的内存查看(virt,res,shr,data的意义)
linux下的内存查看(virt,res,shr,data的意义)
172 0
|
SQL 缓存 监控
译|Monitoring and Tuning the Linux Networking Stack: Sending Data(十一)
译|Monitoring and Tuning the Linux Networking Stack: Sending Data(十一)
156 0
|
15天前
|
监控 Linux
如何检查 Linux 内存使用量是否耗尽?这 5 个命令堪称绝了!
本文介绍了在Linux系统中检查内存使用情况的5个常用命令:`free`、`top`、`vmstat`、`pidstat` 和 `/proc/meminfo` 文件,帮助用户准确监控内存状态,确保系统稳定运行。
104 6
|
16天前
|
Linux
在 Linux 系统中,“cd”命令用于切换当前工作目录
在 Linux 系统中,“cd”命令用于切换当前工作目录。本文详细介绍了“cd”命令的基本用法和常见技巧,包括使用“.”、“..”、“~”、绝对路径和相对路径,以及快速切换到上一次工作目录等。此外,还探讨了高级技巧,如使用通配符、结合其他命令、在脚本中使用,以及实际应用案例,帮助读者提高工作效率。
57 3
|
16天前
|
监控 安全 Linux
在 Linux 系统中,网络管理是重要任务。本文介绍了常用的网络命令及其适用场景
在 Linux 系统中,网络管理是重要任务。本文介绍了常用的网络命令及其适用场景,包括 ping(测试连通性)、traceroute(跟踪路由路径)、netstat(显示网络连接信息)、nmap(网络扫描)、ifconfig 和 ip(网络接口配置)。掌握这些命令有助于高效诊断和解决网络问题,保障网络稳定运行。
48 2
|
24天前
|
缓存 监控 Linux
|
27天前
|
Linux Shell 数据安全/隐私保护
|
11天前
|
运维 监控 网络协议
运维工程师日常工作中最常用的20个Linux命令,涵盖文件操作、目录管理、权限设置、系统监控等方面
本文介绍了运维工程师日常工作中最常用的20个Linux命令,涵盖文件操作、目录管理、权限设置、系统监控等方面,旨在帮助读者提高工作效率。从基本的文件查看与编辑,到高级的网络配置与安全管理,这些命令是运维工作中的必备工具。
44 3