译|Monitoring and Tuning the Linux Networking Stack: Receiving Data(九)

简介: 译|Monitoring and Tuning the Linux Networking Stack: Receiving Data(九)

UDP协议层

UDP 协议层的代码可以在以下文件中找到:net/ipv4/udp. c.

udp_rcv

udp_rcv 函数的代码只有一行,它直接调用 __udp4_lib_rcv 来接收数据报。

__udp4_lib_rcv

__udp4_lib_rcv 函数检查以确保数据包有效,并获取 UDP 报头、UDP 数据报长度、源地址和目标地址。 接下来,是一些附加的完整性检查和校验和验证。

回想一下,在前面的 IP 协议层,我们看到在将数据包交到上层协议(在我们的情况下是 UDP)之前执行了一个优化,以附加 dst_entry 到数据包。

如果找到一个套接字和相应的 dst_entry__udp4_lib_rcv 将把数据包排队到套接字:

sk = skb_steal_sock(skb);
if (sk) {
  struct dst_entry *dst = skb_dst(skb);
  int ret;
  if (unlikely(sk->sk_rx_dst != dst))
    udp_sk_rx_dst_set(sk, dst);
  ret = udp_queue_rcv_skb(sk, skb);
  sock_put(sk);
  /* a return value > 0 means to resubmit the input, but
   * it wants the return to be -protocol, or 0
   */
  if (ret > 0)
    return -ret;
  return 0;
} else {

如果 early_demux 操作没有附加套接字,则现在将调用 __udp4_lib_lookup_skb 来查找接收套接字 。

在上述两种情况下,数据报将排队到套接字:

ret = udp_queue_rcv_skb(sk, skb);
sock_put(sk);

如果没有找到套接字,则丢弃数据报:

/* No socket. Drop packet silently, if checksum is wrong */
if (udp_lib_checksum_complete(skb))
        goto csum_error;
UDP_INC_STATS_BH(net, UDP_MIB_NOPORTS, proto == IPPROTO_UDPLITE);
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0);
/*
 * Hmm.  We got an UDP packet to a port to which we
 * don't wanna listen.  Ignore it.
 */
kfree_skb(skb);
return 0;
udp_queue_rcv_skb

此函数的初始部分如下所示:

  1. 确定与数据报关联的套接字是否是封装套接字。 如果是,在继续之前传递数据包到该层的处理函数。
  2. 确定数据报是否为 UDP-Lite 数据报,并执行一些完整性检查。
  3. 验证数据报的 UDP 校验和,如果校验和失败,则丢弃数据报。

最后,我们到达接收队列逻辑,它首先检查套接字的接收队列是否已满。 来自 net/ipv4/udp.c

if (sk_rcvqueues_full(sk, skb, sk->sk_rcvbuf))
  goto drop;
sk_rcvqueues_full

sk_rcvqueues_full 函数检查套接字的 backlog 长度和套接字的 sk_rmem_alloc,以确定总和是否大于套接字的 sk_rcvbufsk->sk_rcvbuf):

/*
 * Take into account size of receive queue and backlog queue
 * Do not take into account this skb truesize,
 * to allow even a single big packet to come.
 */
static inline bool sk_rcvqueues_full(const struct sock *sk, const struct sk_buff *skb,
                                     unsigned int limit)
{
        unsigned int qsize = sk->sk_backlog.len + atomic_read(&sk->sk_rmem_alloc);
        return qsize > limit;
}

调优这些值有点棘手,因为有很多东西可以调整。

调优:套接字接收队列内存

sksk->sk_rcvbuf(在上面的sk_rcvqueues_full中称为limit)值可以增加到 sysctlnet.core.rmem_max 设置的值。

设置 sysctl 增加最大接收缓冲区大小。

$ sudo sysctl -w net.core.rmem_max=8388608

sk->sk_rcvbufnet.core.rmem_default 值开始,也可以设置 sysctl 来调整,如下所示:

设置 sysctl 来调整默认的初始接收缓冲区大小。

$ sudo sysctl -w net.core.rmem_default=8388608

您可以在应用程序中调用 setsockopt 并传递 SO_RCVBUF 来设置 sk->sk_rcvbuf 的大小。您可以使用 setsockopt 设置的最大值为 net.core.rmem_max

但是,您可以调用 setsockopt 并传递 SO_RCVBUFFORCE 来覆盖 net.core.rmem_max 的限制,但运行应用程序的用户需要具有 CAP_NET_ADMIN 权限。

当调用 skb_set_owner_r 设置数据报的所有者套接字时,会增加 sk->sk_rmem_alloc 的值。我们稍后将在 UDP 层中看到这个调用。

当调用 sk_add_backlog 时,会增加 sk->sk_backlog.len 的值,我们接下来将看到这个调用。

udp_queue_rcv_skb

一旦验证队列未满,则可以继续对数据报进行排队。 来自 net/ipv4/udp.c

bh_lock_sock(sk);
if (!sock_owned_by_user(sk))
  rc = __udp_queue_rcv_skb(sk, skb);
else if (sk_add_backlog(sk, skb, sk->sk_rcvbuf)) {
  bh_unlock_sock(sk);
  goto drop;
}
bh_unlock_sock(sk);
return rc;

第一步是确定套接字当前是否有任何来自用户空间程序的系统调用。 如果没有,则可以调用 __udp_queue_rcv_skb 添加数据报到接收队列。 如果是,则调用 sk_add_backlog 排队数据报到 backlog。

当套接字系统调用调用内核中的 release_sock 释放套接字时,backlog 上的数据报被添加到接收队列。

__udp_queue_rcv_skb

__udp_queue_rcv_skb 函数调用 sock_queue_rcv_skb 添加数据报到接收队列,如果数据报无法添加到套接字的接收队列,则会增加统计计数器。

来自 net/ipv4/udp.c

rc = sock_queue_rcv_skb(sk, skb);
if (rc < 0) {
  int is_udplite = IS_UDPLITE(sk);
  /* Note that an ENOMEM error is charged twice */
  if (rc == -ENOMEM)
    UDP_INC_STATS_BH(sock_net(sk), UDP_MIB_RCVBUFERRORS,is_udplite);
  UDP_INC_STATS_BH(sock_net(sk), UDP_MIB_INERRORS, is_udplite);
  kfree_skb(skb);
  trace_udp_fail_queue_rcv_skb(rc, sk);
  return -1;
}
监控: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

与此文件中 I P协议的详细统计信息非常相似,您需要阅读协议层源文件,以准确确定这些值在何时何地递增。

  • 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 应该得出接收端内存相关错误的计数。
/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 套接字状态。在上面的示例中,7TCP_CLOSE
  • tx_queue:内核为传出 UDP 数据报分配的内存量。
  • rx_queue:内核为传入 UDP 数据报分配的内存量。
  • trtm->whenretrnsmt:这些字段未被 UDP 协议层使用。
  • uid:创建此套接字的用户的有效用户 ID。
  • timeout:未被 UDP 协议层使用。
  • inode:与此套接字对应的 inode 编号。您可以使用它来帮助您确定哪个用户进程打开了此套接字。检查 /proc/[pid]/fd,其中包含指向 socket[:inode] 的符号链接。
  • ref:套接字的当前引用计数。
  • pointer:内核中 struct sock 的内存地址。
  • drops:与此套接字关联的数据报丢弃数。 请注意,这不包括任何与发送数据报有关的丢弃(在 corked 的 UDP 套接字上,或其他);在本博客考察的内核版本中,只在接收路径中增加。

可以在 net/ipv4/udp.c 中找到输出此内容的代码。

排队数据到套接字

网络数据调用 sock_queue_rcv 排队到套接字。在添加数据报到队列之前,此函数会执行一些操作:

  1. 检查套接字的分配内存,以确定它是否超过了接收缓冲区大小。如果是,则增加套接字的丢弃计数。
  2. 接下来使用 sk_filter 处理已应用于套接字的 Berkeley Packet Filter 过滤器。
  3. 运行 sk_rmem_schedule,以确保有足够的接收缓冲区空间来接受此数据报。
  4. 接下来调用 skb_set_owner_r 将数据报的大小计入套接字。这会增加 sk->sk_rmem_alloc
  5. 调用 __skb_queue_tail 添加数据到队列中。
  6. 最后,调用 sk_data_ready 通知处理程序函数通知任何等待套接字中数据到达的进程。

这就是数据如何到达系统并遍历网络堆栈,直到它到达套接字并准备好被用户程序读取。

其他

有一些额外的事情值得一提,值得一提的是,似乎不太正确的其他任何地方。

时间戳

正如上面的博客文章中提到的,网络栈可以收集传出数据的时间戳。 请参阅上面的网络栈演练,了解软件中的传输时间戳发生的位置。 一些 NIC 甚至还支持硬件中的时间戳。

如果您想尝试确定内核网络栈在发送数据包时增加了多少延迟,这是一个有用的特性。

关于时间戳的内核文档非常好,甚至还有一个包含的示例程序和 Makefile,你可以查看

使用 ethtool -T 确定您的驱动程序和设备支持的时间戳模式。

$ sudo ethtool -T eth0
Time stamping parameters for eth0:
Capabilities:
  software-transmit     (SOF_TIMESTAMPING_TX_SOFTWARE)
  software-receive      (SOF_TIMESTAMPING_RX_SOFTWARE)
  software-system-clock (SOF_TIMESTAMPING_SOFTWARE)
PTP Hardware Clock: none
Hardware Transmit Timestamp Modes: none
Hardware Receive Filter Modes: none

不幸的是,这个网卡不支持硬件接收时间戳,但是软件时间戳仍然可以在这个系统上使用,以帮助我确定内核给我的数据包接收路径增加了多少延迟。

低延迟套接字的忙轮询

可以使用名为 SO_BUSY_POLL 的套接字选项,当执行阻塞接收且没有数据时,它会导致内核忙碌轮询新数据。

重要提示:要使此选项正常工作,您的设备驱动程序必须支持它。Linux 内核 3.13.0 的 igb 驱动程序不支持此选项。然而,ixgbe 驱动程序支持。如果您的驱动程序在其 struct net_device_ops 结构(在上面的博客文章中提到)的 ndo_busy_poll 字段中设置了一个函数,则它支持 SO_BUSY_POLL

Intel 提供了一篇很棒的论文,解释了这是如何工作的以及如何使用它。

当为单个套接字使用此套接字选项时,您应该传递一个以微秒为单位的时间值,作为在设备驱动程序的接收队列中忙碌轮询新数据的时间。在设置此值后,当您对此套接字发出阻塞读取时,内核将忙碌轮询新数据。

您还可以设置 sysctl 值 net.core.busy_poll 为以微秒为单位的时间值,表示使用 pollselect 的调用应忙碌轮询等待新数据到达的时间。

此选项可以减少延迟,但会增加 CPU 使用率和功耗。

Netpoll:支持关键环境中的联网

Linux 内核提供了一种方法,可以在内核崩溃时使用设备驱动程序在 NIC 上发送和接收数据。这个 API 被称为 Netpoll,它被一些东西使用,但最值得注意的是:kgdbnetconsole

大多数驱动程序都支持 Netpoll;您的驱动程序需要实现 ndo_poll_controller 函数,并将其关联到探测期间注册的 struct net_device_ops(如上所示)。

当网络设备子系统对传入或传出数据执行操作时,首先检查 netpoll 系统以确定数据包是否目标为 netpoll。

例如,我们可以在 __netif_receive_skb_core 中看到以下代码,来自 net/dev/core.c

static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc)
{
  /* ... */
  /* if we've gotten here through NAPI, check netpoll */
  if (netpoll_receive_skb(skb))
    goto out;
  /* ... */
}

Netpoll 检查发生在大多数处理传输或接收网络数据的 Linux 网络设备子系统代码之前。

Netpoll API 的使用者可以调用 netpoll_setup 来注册 struct netpoll 结构。struct netpoll 结构具有关联接收钩子的函数指针,API 导出了一个发送数据的函数。

如果您对使用 Netpoll API 感兴趣,您应该查看 netconsole 驱动程序、Netpoll API 头文件 include/linux/netpoll.h这个优秀的演讲

SO_INCOMING_CPU

SO_INCOMING_CPU 标志直到 Linux 3.19 才被添加,但它非常有用,应该包含在此博客文章中。

您可以使用 getsockoptSO_INCOMING_CPU 选项来确定哪个 CPU 处理特定套接字的网络数据包。然后,您的应用程序可以使用此信息将套接字交给在所需 CPU 上运行的线程,以帮助增加数据局部性和 CPU 缓存命中。

引入 SO_INCOMING_CPU邮件列表消息提供了一个简短的示例架构,其中此选项很有用。

DMA引擎

DMA 引擎是一种硬件,它允许 CPU 卸载大型复制操作。这使得 CPU 使用硬件完成内存复制时可以执行其他任务。启用 DMA 引擎并运行利用它的代码,应该会降低 CPU 使用率。

Linux 内核具有通用的 DMA 引擎接口,DMA 引擎驱动程序作者可以插入。您可以在 内核源代码文档 中了解更多关于 Linux DMA 引擎接口的信息。

尽管内核支持一些 DMA 引擎,但我们将讨论一种非常常见的特定 DMA 引擎:Intel IOAT DMA 引擎

英特尔的 I/O 加速技术(IOAT)

许多服务器都包含 Intel I/O AT 组件包,它由一系列性能更改组成。

其中一个更改是包含硬件 DMA 引擎。您可以检查 dmesg 输出中的 ioatdma,以确定模块是否正在加载并且是否找到了支持的硬件。

DMA 卸载引擎在几个地方使用,最值得注意的是在 TCP 栈中。

对 Intel IOAT DMA 引擎的支持包含在 Linux 2.6.18 中,但由于一些不幸的 数据损坏错误,它在 3.13.11.10 中被禁用。

在 3.13.11.10 之前的内核上的用户可能默认在其服务器上使用 ioatdma 模块。也许这将在未来的内核版本中得到修复。

直接缓存访问

Intel I/O AT 组件包 一起包含的另一个有趣功能是直接缓存访问 (DCA)。

此功能允许网络设备(通过其驱动程序)直接放置网络数据到 CPU 缓存中。具体如何实现这一点是特定于驱动程序的。对于 igb 驱动程序,您可以检查 函数 igb_update_dca 的代码,以及 igb_update_rx_dca 的代码igb 驱动程序向 NIC 写入寄存器值来使用 DCA。

要使用 DCA,您需要确保在 BIOS 中启用了 DCA,加载了 dca 模块,并且您的网络卡和驱动程序都支持 DCA。

监控 IOAT DMA 引擎

如果您正在使用 ioatdma 模块,尽管有上面提到的数据损坏的风险,您可以检查 sysfs 中的一些条目监控它。

监控 DMA 通道的卸载 memcpy 操作总数。

$ cat /sys/class/dma/dma0chan0/memcpy_count
123205655

类似地,要获取此 DMA 通道卸载的字节数,可以运行以下命令:

监控 DMA 通道传输的总字节数。

$ cat /sys/class/dma/dma0chan0/bytes_transferred
131791916307
调优 IOAT DMA 引擎

IOAT DMA 引擎仅在数据包大小高于某个阈值时使用。 这个阈值被称为 copybreak。 之所以进行此检查,是因为对于小型副本,设置和使用 DMA 引擎的开销不值得加速传输。

使用 sysctl 调整 DMA 引擎 copybreak

$ sudo sysctl -w net.ipv4.tcp_dma_copybreak=2048

默认值为 4096。

结论

Linux 网络堆栈非常复杂。

如果不深入了解究竟发生了什么,就不可能监控或调优它(或任何其他复杂的软件)。通常,在互联网的荒野中,您可能会偶然发现一个包含一组 sysctl 值的示例 sysctl.conf,复制并粘贴到您的计算机上。这可能不是优化您的网络堆栈的最佳方法。

监控网络堆栈需要在每一层仔细核算网络数据。从驱动程序开始,然后向上进行。这样您就可以确定丢弃和错误发生在哪里,然后调整设置以确定如何减少您看到的错误。

不幸的是,没有简单的出路。

原文:Monitoring and Tuning the Linux Networking Stack: Receiving Data

本文作者 : cyningsun

本文地址https://www.cyningsun.com/04-24-2023/monitoring-and-tuning-the-linux-networking-stack-recv-cn.html

版权声明 :本博客所有文章除特别声明外,均采用 CC BY-NC-ND 3.0 CN 许可协议。转载请注明出处!

# Network

  1. 译|A scalable, commodity data center network architecture
  2. 译|llustrated Guide to Monitoring and Tuning the Linux Networking Stack: Receiving Data
  3. 译|Monitoring and Tuning the Linux Networking Stack: Sending Data
  4. TCP/IP 网络传输
  5. TCP/IP 网络设备与基础概念
目录
相关文章
|
10月前
|
运维 监控 网络协议
译|llustrated Guide to Monitoring and Tuning the Linux Networking Stack: Receiving Data
译|llustrated Guide to Monitoring and Tuning the Linux Networking Stack: Receiving Data
73 0
|
6月前
|
Linux
linux下的内存查看(virt,res,shr,data的意义)
linux下的内存查看(virt,res,shr,data的意义)
109 0
|
10月前
|
SQL 缓存 监控
译|Monitoring and Tuning the Linux Networking Stack: Sending Data(十一)
译|Monitoring and Tuning the Linux Networking Stack: Sending Data(十一)
84 0
|
10月前
|
SQL 存储 缓存
译|Monitoring and Tuning the Linux Networking Stack: Sending Data(十)
译|Monitoring and Tuning the Linux Networking Stack: Sending Data(十)
207 1
|
10月前
|
缓存 监控 Linux
译|Monitoring and Tuning the Linux Networking Stack: Sending Data(九)
译|Monitoring and Tuning the Linux Networking Stack: Sending Data(九)
138 0
|
2天前
|
监控 Linux Perl
【专栏】Linux 命令小技巧:显示文件指定行的内容
【4月更文挑战第28天】本文介绍了Linux中显示文件指定行内容的方法,包括使用`head`和`tail`命令显示文件头尾部分,利用`sed`的行号指定功能以及`awk`处理文本数据。文章还列举了在代码审查、日志分析和文本处理中的应用场景,并提醒注意文件编码、行号准确性及命令组合使用。通过练习和实践,可以提升Linux文本文件处理的效率。
|
2天前
|
运维 网络协议 Linux
【专栏】运维工程师工作时最常用的 20 个 Linux 命令有哪些?建议收藏
【4月更文挑战第28天】本文介绍了运维工程师常用的20个Linux命令,包括`ls`、`cd`、`pwd`、`mkdir`、`rm`、`cp`、`mv`、`cat`、`more`、`less`、`head`、`tail`、`grep`、`find`、`chmod`、`chown`、`chgrp`、`ps`、`top`和`ifconfig`,帮助提升工作效率。此外,还提到了其他常用的命令如`df`、`free`、`tar`、`ssh`、`scp`、`ping`、`netstat`、`iptables`、`systemctl`、`hostname`等,建议运维人员掌握以应对各种运维场景。
|
17小时前
|
存储 Linux Shell
linux课程第二课------命令的简单的介绍2
linux课程第二课------命令的简单的介绍2
|
18小时前
|
安全 Linux C语言
linux课程第一课------命令的简单的介绍
linux课程第一课------命令的简单的介绍
|
1天前
|
Linux Shell 开发工具
【Linux】:文本编辑与输出命令 轻松上手nano、echo和cat
【Linux】:文本编辑与输出命令 轻松上手nano、echo和cat
8 0