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

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

IP 协议层

UDP 协议层简单地调用 ip_send_skb 传递 skbs 给 IP 协议,因此让我们从那开始,并掌握 IP 协议层!

ip_send_skb

ip_send_skb 函数位于 ./net/ipv4/ip_output.c 中,非常短。 它只是向下调用 ip_local_out,如果 ip_local_out 返回某种错误,它就会增加错误统计信息。 我们来看一下:

int ip_send_skb(struct net *net, struct sk_buff *skb)
{
        int err;
        err = ip_local_out(skb);
        if (err) {
                if (err > 0)
                        err = net_xmit_errno(err);
                if (err)
                        IP_INC_STATS(net, IPSTATS_MIB_OUTDISCARDS);
        }
        return err;
}

如上所述,调用 ip_local_out,然后处理返回值。 调用 net_xmit_errno “翻译” 来自底层的错误为 IP 和 UDP 协议层可以理解的错误。 如果发生错误,将增加 IP 协议统计信息 “OutDiscards” 。 稍后我们将看到获得此统计信息要读取哪些文件。 现在,让我们继续探索,看看 ip_local_out 会把我们带到哪里。

ip_local_out__ip_local_out

幸运的是,ip_local_out__ip_local_out 都很简单。ip_local_out 只是向下调用 __ip_local_out,并根据返回值调用路由层发送数据包:

int ip_local_out(struct sk_buff *skb)
{
        int err;
        err = __ip_local_out(skb);
        if (likely(err == 1))
                err = dst_output(skb);
        return err;
}

可以从 __ip_local_out 的源代码中看到,该函数首先做了两件重要的事情:

  1. 设置 IP 数据包的长度
  2. 调用 ip_send_check 计算要写入 IP 数据包报头的校验和。 ip_send_check 函数调用 ip_fast_csum 来计算校验和。 在 x86 和 x86_64 体系结构上,此功能以汇编实现。 你可以在这里阅读 64 位的实现,在这里阅读 32 位的实现

接下来,IP 协议层调用 nf_hook 向下调用 netfilter。传回 nf_hook 函数的返回值给 ip_local_out。 如果 nf_hook 返回 1,表明允许数据包通过,调用者应该自己传递它。 正如我们在上面看到的,实际正是如此:ip_local_out 检查返回值 1,并调用 dst_output 传递数据包。 让我们来看看 __ip_local_out 的代码:

int __ip_local_out(struct sk_buff *skb)
{
        struct iphdr *iph = ip_hdr(skb);
        iph->tot_len = htons(skb->len);
        ip_send_check(iph);
        return nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT, skb, NULL,
                       skb_dst(skb)->dev, dst_output);
}

netfilter 和 nf_hook

简洁起见,我决定跳过对 netfilter、iptables 和 conntrack 的深入研究。 你可以从 这里这里 开始深入了解 netfilter 的源代码。

简版:nf_hook 是一个包装器,它调用 nf_hook_thresh,首先检查指定的协议族和钩子类型(在本例中分别为 NFPROTO_IPV4NF_INET_LOCAL_OUT)是否安装了过滤器,并试图返回执行流程到 IP 协议层,以避免深入 netfilter 和在其下面的钩子,如 iptables 和 conntrack。

请记住:如果你有很多或非常复杂的 netfilter 或 iptables 规则,这些规则将在启动原始 sendmsg 调用的用户进程的 CPU 上下文中执行。 如果您设置了 CPU pinning 以限制此进程的执行到特定的 CPU(或一组 CPU),请注意 CPU 将花费系统时间处理出站 iptables 规则。 根据系统的工作负载,如果您在这里测量性能回归,您可能需要小心地固定进程到 CPU 或降低规则集的复杂性。

为了便于讨论,我们假设 nf_hook 返回 1 表示调用方(在本例中是 IP 协议层)应该自己传递数据包。

目标缓存

在 Linux 内核中,dst 代码实现了协议无关的目标缓存。 为了理解如何设置 dst 条目以继续发送 UDP 数据报,我们需要简要地探讨一下 dst 条目和路由是如何生成的。 目标缓存、路由和邻居子系统都可以单独进行极其详细的探讨。 出于我们的目的,我们可以快速查看一下这一切是如何结合在一起的。

我们上面看到的代码调用了 dst_output(skb)。 这个函数只是查找 skb 附加的 dst 条目 skb 并调用 output 函数。 我们来看一下:

/* Output packet to network from transport.  */
static inline int dst_output(struct sk_buff *skb)
{
        return skb_dst(skb)->output(skb);
}

看起来很简单,但 output 函数起初是如何被关联到 dst 条目的呢?

重要的是要了解,有许多不同的方式添加目标缓存条目。 到目前为止,我们在代码路径中看到的一种方式是从 udp_sendmsg 调用 ip_route_output_flowip_route_output_flow 函数调用 __ip_route_output_key,后者调用 __mkroute_output__mkroute_output 函数创建路由和目标缓存条目。 当它执行时,它会确定适合于此目标的输出函数。 大多数时候,这个函数是 ip_output

ip_output

因此,dst_output 执行 output 函数,在 UDP IPv4 情况下为 ip_outputip_output 函数很简单:

int ip_output(struct sk_buff *skb)
{
        struct net_device *dev = skb_dst(skb)->dev;
        IP_UPD_PO_STATS(dev_net(dev), IPSTATS_MIB_OUT, skb->len);
        skb->dev = dev;
        skb->protocol = htons(ETH_P_IP);
        return NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING, skb, NULL, dev,
                            ip_finish_output,
                            !(IPCB(skb)->flags & IPSKB_REROUTED));
}

首先,更新统计计数器 IPSTATS_MIB_OUTIP_UPD_PO_STATS 宏增加字节数和数据包数。 我们将在后面的部分中看到如何获得 IP 协议层统计信息以及它们各自的含义。 接下来,设置传输此 skb 的设备、协议。

最后,调用 NF_HOOK_COND 传递控制权给 netfilter。 查看 NF_HOOK_COND 的函数原型有助于更清楚地解释它的工作原理。 来源为 ./include/linux/netfilter.h

static inline int
NF_HOOK_COND(uint8_t pf, unsigned int hook, struct sk_buff *skb,
             struct net_device *in, struct net_device *out,
             int (*okfn)(struct sk_buff *), bool cond)

NF_HOOK_COND 检查传入的条件。 在此情况下,条件是 !(IPCB(skb)->flags & IPSKB_REROUTED。 如果条件为真,那么传递 skb 给 netfilter。 如果 netfilter 允许数据包通过,则调用 okfn。 此情况下,okfnip_finish_output

ip_finish_output

ip_finish_output函数也很简洁明了。 我们来看一下:

static int ip_finish_output(struct sk_buff *skb)
{
#if defined(CONFIG_NETFILTER) && defined(CONFIG_XFRM)
        /* Policy lookup after SNAT yielded a new policy */
        if (skb_dst(skb)->xfrm != NULL) {
                IPCB(skb)->flags |= IPSKB_REROUTED;
                return dst_output(skb);
        }
#endif
        if (skb->len > ip_skb_dst_mtu(skb) && !skb_is_gso(skb))
                return ip_fragment(skb, ip_finish_output2);
        else
                return ip_finish_output2(skb);
}

如果在此内核中启用了 netfilter 和数据包转换,会更新 skb 的标志,并通过 dst_output 将其发送回。 两种比较常见的情况是:

  1. 如果数据包的长度大于 MTU,并且数据包的分段不会卸载到设备,则调用 ip_fragment 以在传输之前对数据包进行分段。
  2. 否则,直接传递数据包到 ip_finish_output2

在继续内核学习之前,让我们稍微绕个圈子来讨论一下路径 MTU 发现。

路径 MTU 发现

Linux 提供了一个我前面避免提到的特性:路径 MTU 发现。 此功能允许内核自动确定特定路由的最大 MTU。 确定此值并发送小于或等于路由 MTU 的数据包意味着可以避免 IP 分段。 这是首选设置,因为数据包分段会消耗系统资源,而且似乎很容易避免:简单地发送足够小的数据包,就不需要分段。

调用 setsockopt,您可以在应用程序中使用 SOL_IP 级别和 IP_MTU_DISCOVER optname 调整每个套接字的路径 MTU 发现设置。optval 可以是 IP 协议手册页中描述的几个值之一。 您可能希望设置的值为:IP_PMTUDISC_DO 表示“始终执行路径 MTU 发现”。 更高级的网络应用程序或诊断工具可以选择自己实现 RFC 4821 ,以在应用程序启动时确定特定路由的 PMTU。 在这种情况下,您可以使用 IP_PMTUDISC_PROBE 选项,该选项告诉内核设置“Don’t Fragment”位,允许您发送大于 PMTU 的数据。

调用 getsockopt,您的应用程序可以使用 SOL_IPIP_MTU optname 来检索 PMTU。 您可以使用它来帮助指导应用程序尝试在传输之前构造 UDP 数据报的大小。

如果已启用 PTMU 发现,则任何发送大于 PMTU 的 UDP 数据的尝试都将导致应用程序收到错误码 EMSGSIZE。 然后,应用程序可以使用更少的数据重试。

强烈建议启用 PTMU 发现,因此我将避免详细描述 IP 分段代码路径。 当查看 IP 协议层统计信息时,我将解释所有统计信息,包括与分段相关的统计信息。 其中许多在 ip_fragment。 无论是否分段,都调用了 ip_finish_output2,所以让我们继续。

ip_finish_output2

ip_finish_output2 在 IP 分段之后被调用,并且也直接从 ip_finish_output 调用。 在向下传递数据包到邻居缓存之前,此函数增加各种统计计数器。 让我们看看它是如何工作的:

static inline int ip_finish_output2(struct sk_buff *skb)
{
        /* variable declarations */
        if (rt->rt_type == RTN_MULTICAST) {
                IP_UPD_PO_STATS(dev_net(dev), IPSTATS_MIB_OUTMCAST, skb->len);
        } else if (rt->rt_type == RTN_BROADCAST)
                IP_UPD_PO_STATS(dev_net(dev), IPSTATS_MIB_OUTBCAST, skb->len);
        /* Be paranoid, rather than too clever. */
        if (unlikely(skb_headroom(skb) < hh_len && dev->header_ops)) {
                struct sk_buff *skb2;
                skb2 = skb_realloc_headroom(skb, LL_RESERVED_SPACE(dev));
                if (skb2 == NULL) {
                        kfree_skb(skb);
                        return -ENOMEM;
                }
                if (skb->sk)
                        skb_set_owner_w(skb2, skb->sk);
                consume_skb(skb);
                skb = skb2;
        }

如果与此数据包相关联的路由结构是组播类型,使用IP_UPD_PO_STATS 宏来增加 OutMcastPktsOutMcastOctets 计数器。 否则,如果路由类型为广播,则增加 OutBcastPktsOutBcastOctets 计数器。

接下来,执行检查以确保 skb 结构具有足够的空间添加任何需要的链路层报头。 如果没有,则调用 skb_realloc_headroom 来分配额外的空间,并且新 skb 的成本将计入相关套接字。

rcu_read_lock_bh();
nexthop = (__force u32) rt_nexthop(rt, ip_hdr(skb)->daddr);
neigh = __ipv4_neigh_lookup_noref(dev, nexthop);
if (unlikely(!neigh))
        neigh = __neigh_create(&arp_tbl, &nexthop, dev, false);

继续,我们可以看到,下一跳是查询路由层,然后查找邻居缓存得到的。 如果找不到邻居,则调用 __neigh_create 创建一个。 例如,数据第一次发送到另一台主机时可能出现此情况。 请注意,此函数是调用 arp_tbl(在 ./net/ipv4/arp.c 中定义),在 ARP 表中创建邻居条目。 其他系统(如 IPv6 或 DECnet)维护自己的 ARP 表,并传递不同的结构给 __neigh_create。 本文并不旨在全面介绍邻居缓存,但如果必须创建邻居缓存,那么创建可能会导致缓存增长。 这篇文章将在下面的章节中介绍更多关于邻居缓存的细节。 无论如何,邻居缓存导出自己的统计信息,以便可以测量缓存增长。 有关详细信息,请参阅下面的监控部分。

if (!IS_ERR(neigh)) {
                int res = dst_neigh_output(dst, neigh, skb);
                rcu_read_unlock_bh();
                return res;
        }
        rcu_read_unlock_bh();
        net_dbg_ratelimited("%s: No header cache and no neighbour!\n",
                            __func__);
        kfree_skb(skb);
        return -EINVAL;
}

最后,如果没有返回错误,则调用 dst_neigh_output 沿着输出的旅程传递 skb。 否则,释放 skb 并返回 EINVAL。 此处的错误将产生连锁反应,并增加 ip_send_skb 中的 OutDiscards。 让我们继续探索 dst_neigh_output,并继续接近 Linux 内核的网络设备子系统。

dst_neigh_output

dst_neigh_output 函数为我们做了两件重要的事情。 首先,回想一下在这篇博客文章的前面,我们看到如果用户通过辅助消息指定 MSG_CONFIRMsendmsg 函数,则会翻转一个标志,指示远程主机的目标缓存条目仍然有效,不应被垃圾回收。 该检查在这里发生,设置邻居的 confirmed 字段为当前的 jiffies 计数。

static inline int dst_neigh_output(struct dst_entry *dst, struct neighbour *n,
                                   struct sk_buff *skb)
{
        const struct hh_cache *hh;
        if (dst->pending_confirm) {
                unsigned long now = jiffies;
                dst->pending_confirm = 0;
                /* avoid dirtying neighbour */
                if (n->confirmed != now)
                        n->confirmed = now;
        }

其次,检查邻居的状态,并调用适当的输出函数。 让我们来看看以下条件句,试着理解是怎么回事:

hh = &n->hh;
        if ((n->nud_state & NUD_CONNECTED) && hh->hh_len)
                return neigh_hh_output(hh, skb);
        else
                return n->output(n, skb);
}

如果邻居被认为是 NUD_CONNECTED,则意味着它是以下情况的一种或多种:

  • NUD_PERMANENT:静态路由。
  • NUD_NOARP:不需要 ARP 请求(例如,目的地是组播或广播地址,或环回设备)。
  • NUD_REACHABLE:邻居是“可达的”。只要 ARP 请求 成功处理,目的地就会被标记为可达。
目录
相关文章
|
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(十)
363 1
|
SQL 缓存 监控
译|Monitoring and Tuning the Linux Networking Stack: Sending Data(十一)
译|Monitoring and Tuning the Linux Networking Stack: Sending Data(十一)
156 0
|
缓存 监控 Linux
译|Monitoring and Tuning the Linux Networking Stack: Sending Data(九)
译|Monitoring and Tuning the Linux Networking Stack: Sending Data(九)
241 0
|
监控 Linux 调度
译|Monitoring and Tuning the Linux Networking Stack: Sending Data(八)
译|Monitoring and Tuning the Linux Networking Stack: Sending Data(八)
99 0
|
存储 Ubuntu Linux
译|Monitoring and Tuning the Linux Networking Stack: Sending Data(七)
译|Monitoring and Tuning the Linux Networking Stack: Sending Data(七)
147 0
|
15天前
|
监控 Linux
如何检查 Linux 内存使用量是否耗尽?这 5 个命令堪称绝了!
本文介绍了在Linux系统中检查内存使用情况的5个常用命令:`free`、`top`、`vmstat`、`pidstat` 和 `/proc/meminfo` 文件,帮助用户准确监控内存状态,确保系统稳定运行。
104 6
|
16天前
|
Linux
在 Linux 系统中,“cd”命令用于切换当前工作目录
在 Linux 系统中,“cd”命令用于切换当前工作目录。本文详细介绍了“cd”命令的基本用法和常见技巧,包括使用“.”、“..”、“~”、绝对路径和相对路径,以及快速切换到上一次工作目录等。此外,还探讨了高级技巧,如使用通配符、结合其他命令、在脚本中使用,以及实际应用案例,帮助读者提高工作效率。
57 3