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

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

__netif_receive_skb_core 传送数据到数据包抓取和协议层

__netif_receive_skb_core 执行传递数据到协议栈的繁重工作。 在此之前,它检查是否安装了捕获传入数据包的数据包抓取。 AF_PACKET 地址族就是一个这样的例子,它通常通过 libpcap库使用。

如果存在这样的抓取,则首先传送数据到那里,然后传送到下一个协议层。

数据包抓取传送

如果安装了一个数据包抓取(通常通过 libpcap),数据包将通过来自 net/core/dev.c 的代码发送到那里:

list_for_each_entry_rcu(ptype, &ptype_all, list) {
  if (!ptype->dev || ptype->dev == skb->dev) {
    if (pt_prev)
      ret = deliver_skb(skb, pt_prev, orig_dev);
    pt_prev = ptype;
  }
}

如果你对数据如何通过 pcap 的路径感到好奇,请阅读 net/packet/af_packet.c

协议层交付

一旦满足抓取,__netif_receive_skb_core 发送数据到协议层。它从数据中获取协议字段并遍历为该协议类型注册的传递函数列表来实现这一点。

这可以在  net/core/dev.c 中的 __netif_receive_skb_core 中看到:

type = skb->protocol;
list_for_each_entry_rcu(ptype,
                &ptype_base[ntohs(type) & PTYPE_HASH_MASK], list) {
        if (ptype->type == type &&
            (ptype->dev == null_or_dev || ptype->dev == skb->dev ||
             ptype->dev == orig_dev)) {
                if (pt_prev)
                        ret = deliver_skb(skb, pt_prev, orig_dev);
                pt_prev = ptype;
        }
}

上面的 ptype_base 标识符被定义为  net/core/dev.c 中链表组成的散列表:

struct list_head ptype_base[PTYPE_HASH_SIZE] __read_mostly;

每个协议层在哈希表中的给定槽处向链表添加过滤器,使用称为 ptype_head 的辅助函数计算:

static inline struct list_head *ptype_head(const struct packet_type *pt)
{
        if (pt->type == htons(ETH_P_ALL))
                return &ptype_all;
        else
                return &ptype_base[ntohs(pt->type) & PTYPE_HASH_MASK];
}

调用 dev_add_pack 向链表中添加筛选器。 这就是协议层如何为它们的协议类型的网络数据传送,注册它们自己的。

现在您知道了网络数据是如何从 NIC 传输到协议层的。

协议层注册

现在我们知道了数据是如何从网络设备子系统传递到协议栈的,让我们看看协议层是如何注册自己的。

本文将探讨 IP 协议栈,因为它是一种常用的协议,并且与大多数读者相关。

IP 协议层

IP 协议层将自身插入 ptype_base 哈希表,以便从前面部分描述的网络设备层传递数据到它。

这发生在 net/ipv4/af_inet.cinet_init 函数中:

dev_add_pack(&ip_packet_type);

这将注册在 net/ipv4/af_inet.c 中定义的 IP 数据包类型结构:

static struct packet_type ip_packet_type __read_mostly = {
        .type = cpu_to_be16(ETH_P_IP),
        .func = ip_rcv,
};

__netif_receive_skb_core 调用 deliver_skb(如上一节所示),deliver_skb 调用func(在本例中为 ip_rcv)。

ip_rcv

从高层次来看,ip_rcv 函数非常简单。有几个完整性检查以确保数据有效。统计计数器也会增加。

ip_rcv 通过 netfilter 传递数据包给 ip_rcv_finish 结束。这样做是为了让任何应该在 IP 协议层匹配的 iptables 规则在数据包继续之前查看数据包。

我们可以在 net/ipv4/ip_input.c 中的 ip_rcv 结尾处看到将数据交给 netfilter 的代码:

return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL, ip_rcv_finish);
netfilter 和 iptables

为了简洁(和我的 RSI),我决定跳过对 netfilter、iptables 和 conntrack 的深入研究。

简而言之,NF_HOOK_THRESH 会检查是否安装了过滤器,并尝试返回执行到 IP 协议层,以避免深入到 netfilter 和 iptables 和 conntrack 等下面的任何钩子。

请记住:如果您有许多或非常复杂的 netfilter 或 iptables 规则,那么这些规则将在 softirq 上下文中执行,并可能产生网络堆栈中的延迟。不过,如果您需要安装特定的规则集,这可能是不可避免的。

ip_rcv_finish

一旦 net filter 有机会查看数据并决定如何处理它,就会调用 ip_rcv_finish。 当然,只有当数据没有被 netfilter 丢弃时才会发生这种情况。

ip_rcv_finish 以一个优化开始。为了传递数据包到适当的位置,来自路由系统的dst_entry 需要到位。 为了获得一个 dst_entry,代码最初尝试从该数据的目的地的更高级别协议调用 early_demux 函数。

early_demux 流程是一种优化,它试图检查 dst_entry 是否缓存在套接字结构上,来找到传递数据包所需的 dst_entry

下面是 net/ipv4/ip_input.c 中的内容:

if (sysctl_ip_early_demux && !skb_dst(skb) && skb->sk == NULL) {
  const struct net_protocol *ipprot;
  int protocol = iph->protocol;
  ipprot = rcu_dereference(inet_protos[protocol]);
  if (ipprot && ipprot->early_demux) {
    ipprot->early_demux(skb);
    /* must reload iph, skb->head might have changed */
    iph = ip_hdr(skb);
  }
}

如您所见,上述代码受到 sysctl_ip_early_demux 的保护。默认情况下,early_demux 是启用的。下一节将介绍如何禁用它以及为什么要禁用它。

如果启用了优化并且没有缓存条目(因为这是第一个到达的数据包),则移交该数据包给内核中的路由系统,在那里将计算并分配 dst_entry

路由层完成后,更新统计计数器,并调用 dst_input(skb) 结束函数,该函数又调用了由路由系统关联的数据包的 dst_entry 结构上的输入函数指针。

如果数据包的最终目的地是本地系统,则路由系统将在数据包的 dst_entry 结构上的输入函数指针中关联 ip_local_deliver 函数。

调优:调整 IP 协议 early demux

设置 sysctl 禁用 early_demux 优化。

$ sudo sysctl -w net.ipv4.ip_early_demux=0

默认值为1;启用 early_demux

添加此 sysctl 是因为一些用户发现在某些情况下使用 early_demux 优化会使吞吐量降低约 5%

ip_local_deliver

回想一下在 IP 协议层中看到的以下模式:

  1. 调用 ip_rcv 做一些初始簿记。
  2. 移交数据包给 netfilter 进行处理,并带有一个指针,指向处理完成时要执行的回调。
  3. ip_rcv_finish 是该回调函数,它完成了数据包的处理,并继续推送数据包到网络栈。

ip_local_deliver 具有相同的模式。 来自 net/ipv4/ip_input.c

/*
 *      Deliver IP Packets to the higher protocol layers.
 */
int ip_local_deliver(struct sk_buff *skb)
{
        /*
         *      Reassemble IP fragments.
         */
        if (ip_is_fragment(ip_hdr(skb))) {
                if (ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER))
                        return 0;
        }
        return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN, skb, skb->dev, NULL,
                       ip_local_deliver_finish);
}

假设数据没有首先被 netfilter 丢弃,一旦 netfilter 有机会查看数据,将调用 ip_local_deliver_finish

ip_local_deliver_finish

ip_local_deliver_finish 从数据包中获取协议,查找为该协议注册的 net_protocol 结构,并调用 net_protocol 结构中 handler 指向的函数。

这向上传递数据包到更高级别的协议层。

监控:IP 协议层统计信息

读取 /proc/net/snmp 监控详细的 IP 协议统计信息。

$ cat /proc/net/snmp
Ip: Forwarding DefaultTTL InReceives InHdrErrors InAddrErrors ForwDatagrams InUnknownProtos InDiscards InDelivers OutRequests OutDiscards OutNoRoutes ReasmTimeout ReasmReqds ReasmOKs ReasmFails FragOKs FragFails FragCreates
Ip:                   1           64 25922988125                0                    0             15771700                            0           0 25898327616 22789396404 12987882                    51          
                       1       10129840     2196520                  1              0              0                    0
...

此文件包含多个协议层的统计信息。 首先显示 IP 协议层。第一行包含空格分隔的名称,每个名称对应下一行中的相应值。

在 IP 协议层中,您会发现统计计数器正在增加。计数器引用 C 枚举类型。 /proc/net/snmp 所有有效的枚举值和它们对应的字段名称可以在 include/uapi/linux/snmp.h 中找到:

enum
{
  IPSTATS_MIB_NUM = 0,
/* frequently written fields in fast path, kept in same cache line */
  IPSTATS_MIB_INPKTS,     /* InReceives */
  IPSTATS_MIB_INOCTETS,     /* InOctets */
  IPSTATS_MIB_INDELIVERS,     /* InDelivers */
  IPSTATS_MIB_OUTFORWDATAGRAMS,   /* OutForwDatagrams */
  IPSTATS_MIB_OUTPKTS,      /* OutRequests */
  IPSTATS_MIB_OUTOCTETS,      /* OutOctets */
  /* ... */

读取 /proc/net/netstat 监控扩展 IP 协议统计信息。

$ cat /proc/net/netstat | grep IpExt
IpExt: InNoRoutes InTruncatedPkts InMcastPkts OutMcastPkts InBcastPkts OutBcastPkts InOctets OutOctets InMcastOctets OutMcastOctets InBcastOctets OutBcastOctets InCsumErrors InNoECTPkts InECT0Pktsu InCEPkts
IpExt: 0 0 0 0 277959 0 14568040307695 32991309088496 0 0 58649349 0 0 0 0 0

格式类似于 /proc/net/snmp,不同之处在于行的前缀是 IpExt

一些有趣的统计数据:

  • InReceives:到达 ip_rcv 的 IP 数据包总数,未进行任何数据完整性检查。
  • InHdrErrors:头部损坏的 IP 数据包总数。头部过短、过长、不存在、IP 协议版本号错误等。
  • InAddrErrors:主机不可达的 IP 数据包总数。
  • ForwDatagrams:已转发的 IP 数据包总数。
  • InUnknownProtos:头部中指定了未知或不支持协议的 IP 数据包总数。
  • InDiscards:由于内存分配失败而丢弃的 IP 数据包或校验和失败修剪的数据包总数。
  • InDelivers:成功传递到更高协议层的 IP 数据包总数。请注意,即使 IP 层没有丢弃数据,更高协议层也可能丢弃数据。
  • InCsumErrors:校验和错误的 IP 数据包总数。

请注意,这些值都是在 IP 层的特定位置增加的。代码有时会移动,可能会出现双重计数错误或其他统计错误。如果这些统计数据对您很重要,强烈建议您阅读 IP 协议层源代码,了解您重要的指标何时增加(或不增加)。

更高级别协议注册

本文将研究 UDP,但 TCP 协议处理程序的注册方式和时间与 UDP 协议处理程序相同。

net/ipv4/af_inet.c 中,可以找到包含将 UDP、TCP 和 ICMP 协议连接到 IP 协议层的处理程序函数的结构定义。来自 net/ipv4/af_inet.c

static const struct net_protocol tcp_protocol = {
        .early_demux    =       tcp_v4_early_demux,
        .handler        =       tcp_v4_rcv,
        .err_handler    =       tcp_v4_err,
        .no_policy      =       1,
        .netns_ok       =       1,
};
static const struct net_protocol udp_protocol = {
        .early_demux =  udp_v4_early_demux,
        .handler =      udp_rcv,
        .err_handler =  udp_err,
        .no_policy =    1,
        .netns_ok =     1,
};
static const struct net_protocol icmp_protocol = {
        .handler =      icmp_rcv,
        .err_handler =  icmp_err,
        .no_policy =    1,
        .netns_ok =     1,
};

这些结构在 inet 地址族的初始化代码中注册。 来自 net/ipv4/af_inet.c

/*
 *      Add all the base protocols.
 */
if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0)
        pr_crit("%s: Cannot add ICMP protocol\n", __func__);
if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)
        pr_crit("%s: Cannot add UDP protocol\n", __func__);
if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)
        pr_crit("%s: Cannot add TCP protocol\n", __func__);

我们将研究 UDP 协议层。 如上所述,UDP 的 handler 函数称为 udp_rcv

IP 层在此处理数据,这是进入 UDP 层的入口点。 让我们继续旅程。

目录
相关文章
|
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`等,建议运维人员掌握以应对各种运维场景。
|
19小时前
|
存储 Linux Shell
linux课程第二课------命令的简单的介绍2
linux课程第二课------命令的简单的介绍2
|
20小时前
|
安全 Linux C语言
linux课程第一课------命令的简单的介绍
linux课程第一课------命令的简单的介绍
|
1天前
|
Linux Shell 开发工具
【Linux】:文本编辑与输出命令 轻松上手nano、echo和cat
【Linux】:文本编辑与输出命令 轻松上手nano、echo和cat
8 0