译|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 层的入口点。 让我们继续旅程。

目录
相关文章
|
7月前
|
监控 Linux
Linux的epoll用法与数据结构data、event
Linux的epoll用法与数据结构data、event
102 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
152 0
|
Linux
linux下的内存查看(virt,res,shr,data的意义)
linux下的内存查看(virt,res,shr,data的意义)
177 0
|
SQL 存储 缓存
译|Monitoring and Tuning the Linux Networking Stack: Sending Data(十)
译|Monitoring and Tuning the Linux Networking Stack: Sending Data(十)
368 1
|
SQL 缓存 监控
译|Monitoring and Tuning the Linux Networking Stack: Sending Data(十一)
译|Monitoring and Tuning the Linux Networking Stack: Sending Data(十一)
166 0
|
1月前
|
Linux 网络安全 数据安全/隐私保护
Linux 超级强大的十六进制 dump 工具:XXD 命令,我教你应该如何使用!
在 Linux 系统中,xxd 命令是一个强大的十六进制 dump 工具,可以将文件或数据以十六进制和 ASCII 字符形式显示,帮助用户深入了解和分析数据。本文详细介绍了 xxd 命令的基本用法、高级功能及实际应用案例,包括查看文件内容、指定输出格式、写入文件、数据比较、数据提取、数据转换和数据加密解密等。通过掌握这些技巧,用户可以更高效地处理各种数据问题。
104 8
|
1月前
|
监控 Linux
如何检查 Linux 内存使用量是否耗尽?这 5 个命令堪称绝了!
本文介绍了在Linux系统中检查内存使用情况的5个常用命令:`free`、`top`、`vmstat`、`pidstat` 和 `/proc/meminfo` 文件,帮助用户准确监控内存状态,确保系统稳定运行。
327 6
|
1月前
|
Linux
在 Linux 系统中,“cd”命令用于切换当前工作目录
在 Linux 系统中,“cd”命令用于切换当前工作目录。本文详细介绍了“cd”命令的基本用法和常见技巧,包括使用“.”、“..”、“~”、绝对路径和相对路径,以及快速切换到上一次工作目录等。此外,还探讨了高级技巧,如使用通配符、结合其他命令、在脚本中使用,以及实际应用案例,帮助读者提高工作效率。
82 3
|
1月前
|
监控 安全 Linux
在 Linux 系统中,网络管理是重要任务。本文介绍了常用的网络命令及其适用场景
在 Linux 系统中,网络管理是重要任务。本文介绍了常用的网络命令及其适用场景,包括 ping(测试连通性)、traceroute(跟踪路由路径)、netstat(显示网络连接信息)、nmap(网络扫描)、ifconfig 和 ip(网络接口配置)。掌握这些命令有助于高效诊断和解决网络问题,保障网络稳定运行。
75 2
|
18天前
|
Linux Shell
Linux 10 个“who”命令示例
Linux 10 个“who”命令示例
47 14
Linux 10 个“who”命令示例