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

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

“硬件头”(hh)已缓存(因为之前发送过数据并已生成它),则调用 neigh_hh_output。否则,调用 output 函数。两条代码路径都以 dev_queue_xmit 结束,它传递 skb 到 Linux 网络设备子系统,在到达设备驱动程序层之前会进行更多处理。让我们跟随 neigh_hh_outputn->output 代码路径,直至 dev_queue_xmit

neigh_hh_output

如果目标是 NUD_CONNECTED,并且硬件头已缓存,则调用 neigh_hh_output ,它在移交skb 给 dev_queue_xmit 之前执行一小段处理逻辑。 让我们从 ./include/net/neighbor.h 来看看:

static inline int neigh_hh_output(const struct hh_cache *hh, struct sk_buff *skb)
{
        unsigned int seq;
        int hh_len;
        do {
                seq = read_seqbegin(&hh->hh_lock);
                hh_len = hh->hh_len;
                if (likely(hh_len <= HH_DATA_MOD)) {
                        /* this is inlined by gcc */
                        memcpy(skb->data - HH_DATA_MOD, hh->hh_data, HH_DATA_MOD);
                 } else {
                         int hh_alen = HH_DATA_ALIGN(hh_len);
                         memcpy(skb->data - hh_alen, hh->hh_data, hh_alen);
                 }
         } while (read_seqretry(&hh->hh_lock, seq));
         skb_push(skb, hh_len);
         return dev_queue_xmit(skb);
}

这个函数有点难以理解,部分原因是同步读/写已缓存硬件头的锁定原语。 这段代码使用了一种叫做 seqlock 的东西。 你可以把上面的 do { } while() 循环想象成一种简单的重试机制,它将尝试执行循环中的操作,直到成功执行为止。

循环本身试图确定在复制之前是否需要对齐硬件头部的长度。 这是必需的,因为某些硬件报头(如 IEEE 802.11 报头)大于 HH_DATA_MOD(16 字节)。

一旦数据被复制到 skb,并且 skb_push 更新了 skb 的内部指针跟踪数据,skb 就会传递给 dev_queue_xmit 进入 Linux 网络设备子系统。

n->output

如果目标不是 NUD_CONNECTED 或硬件头尚未缓存,则代码沿着 n->output 路径继续。 邻居结构的输出函数指针关联了什么 output? 嗯,那要看情况了。 为了理解这是如何设置的,我们需要了解更多关于邻居缓存的工作原理。

一个 struct neighbour 包含几个重要的字段。 上面看到的 nud_state 字段,output 函数和 ops 结构。 回想一下之前看到的,如果在缓存中没有找到现有的条目,则从 ip_finish_output2 调用 __neigh_create。 当调用 __neigh_creaet 时,邻居被分配,其 output函数初始设置neigh_blackhole。 随着 __neigh_create 代码执行,它根据邻居的状态调整 output 的值以指向适当的 output 函数。

例如,当代码确定要连接的邻居时,neigh_connect 设置 output 指针为 neigh->ops->connected_output。 或者,在代码怀疑邻居可能关闭时(例如,如果自发送探测以来已经超过/proc/sys/net/ipv4/neigh/default/delay_first_probe_time 秒),neigh_suspect 设置 output 指针为 neigh->ops->output

换句话说:neigh->output 设置为 neigh->ops_connected_output 还是 neigh->ops->output, 取决于邻居的状态。 neigh->ops 从何而来?

在分配邻居之后,arp_constructor(来自 ./net/ipv4/arp.c)被调用来设置 struct neighbour 的一些字段。 特别地,此函数检查与邻居相关联的设备,并且如果该设备暴露包含cache以太网设备这样做)函数的 header_ops 结构 ,则 neigh->ops 被设置为 ./net/ipv4/arp. c 中定义的以下结构:

static const struct neigh_ops arp_hh_ops = {
        .family =               AF_INET,
        .solicit =              arp_solicit,
        .error_report =         arp_error_report,
        .output =               neigh_resolve_output,
        .connected_output =     neigh_resolve_output,
};

因此,无论邻居缓存代码是否视邻居为 “已连接”或“可疑”,都将关联 neigh_resolve_output 函数到 neigh->output,并且在调用 n->output 时被调用。

neigh_resolve_output

此函数的目的是尝试解析未连接的邻居,或已连接但没有缓存硬件头的邻居。 让我们来看看这个函数是如何工作的:

/* Slow and careful. */
int neigh_resolve_output(struct neighbour *neigh, struct sk_buff *skb)
{
        struct dst_entry *dst = skb_dst(skb);
        int rc = 0;
        if (!dst)
                goto discard;
        if (!neigh_event_send(neigh, skb)) {
                int err;
                struct net_device *dev = neigh->dev;
                unsigned int seq;

代码首先执行一些基本检查,然后继续调用 neigh_event_sendneigh_event_send 函数是__neigh_event_send 的简单包装。__neigh_event_send 实际完成解析邻居的繁重工作。 您可以在 ./net/core/neighbor.c 中阅读 __neigh_event_send 的源代码,但从代码中可以看出,用户最感兴趣的有三点:

  1. 假设/proc/sys/net/ipv4/neigh/default/app_solicit/proc/sys/net/ipv4/neigh/default/mcast_solicit 中设置的值允许发送探测,则 NUD_NONE 状态(分配时的默认状态)的邻居将立即发送 ARP 请求(如果不允许,则标记状态为 NUD_FAILED)。 邻居状态被更新并设置为 NUD_INCOMPLETE
  2. 更新状态为 NUD_STALE 的邻居为 NUD_DELAYED,并设置一个计时器以稍后探测它们(稍后:当前时间 +/proc/sys/net/ipv4/neigh/default/delay_first_probe_time 秒)。
  3. 检查 NUD_INCOMPLETE 的任何邻居 (包括上面第一点),以确保未解析邻居的排队数据包数量小于等于 /proc/sys/net/ipv4/neigh/default/unres_qlen。 如果有更多的数据包,则将数据包出队并丢弃,直到长度低于等于 proc 中的值。针对此类情况,邻居缓存统计中的统计计数器都将增加。

如果需要立刻发送 ARP 探测,它就会发送。__neigh_event_send 将返回 0,指示邻居被视为“已连接”或“已延迟”的,否则返回 1。 返回值 0 允许 neigh_resolve_output 函数继续执行:

if (dev->header_ops->cache && !neigh->hh.hh_len)
        neigh_hh_init(neigh, dst);

如果邻居关联的设备的协议实现(在此例子中是以太网)支持缓存硬件报头,并且它当前没有被缓存,则调用 neigh_hh_init 缓存它。

do {
        __skb_pull(skb, skb_network_offset(skb));
        seq = read_seqbegin(&neigh->ha_lock);
        err = dev_hard_header(skb, dev, ntohs(skb->protocol),
                              neigh->ha, NULL, skb->len);
} while (read_seqretry(&neigh->ha_lock, seq));

接下来,使用 seqlock 同步访问邻居结构的硬件地址,当尝试为 skb 创建以太网报头时,dev_hard_header 将读取该地址。 一旦 seqlock 允许继续执行,就会进行错误检查:

if (err >= 0)
                rc = dev_queue_xmit(skb);
        else
                goto out_kfree_skb;
}

如果以太网头被写入而没有返回错误,则 skb 被传递到 dev_queue_xmit,以通过 Linux 网络设备子系统进行传输。 如果有错误,goto 将丢弃 skb,设置返回代码并返回错误:

out:
        return rc;
discard:
        neigh_dbg(1, "%s: dst=%p neigh=%p\n", __func__, dst, neigh);
out_kfree_skb:
        rc = -EINVAL;
        kfree_skb(skb);
        goto out;
}
EXPORT_SYMBOL(neigh_resolve_output);

在进入 Linux 网络设备子系统前,让我们看一下一些监控和调优 IP 协议层的文件。

监控:IP 协议层

/proc/net/snmp

读取 /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 */
  /* ... */

一些有趣的统计数据:

  • OutRequests:每次尝试发送 IP 数据包时增加。 看起来,每次是否成功,都会增加此值。
  • OutDiscards:每次丢弃 IP 数据包时增加。 如果数据追加到 skb(对于 corked 的套接字)失败,或者 IP 下面的层返回错误,就会发生这种情况。
  • OutNoRoute:在多个位置增加,例如在 UDP 协议层(udp_sendmsg),如果无法为给定目标生成路由。 当应用程序在 UDP 套接字上调用 “connect” 但找不到路由时也会增加。
  • FragOKs:每个被分段的数据包增加一次。 例如,被分割成 3 个片段的数据包增加该计数器一次。
  • FragCreates:每个创建的片段增加一次。 例如,被分割成 3 个片段的数据包增加该计数器三次。
  • FragFails:如果尝试分段,但不允许分段,则增加(因为设置了 “Don’t Fragment” 位)。 如果输出片段失败,也会增加。

其他统计数据记录在接收端博客文章中

/proc/net/netstat

读取 /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

一些有趣的统计数据:

  • OutMcastPkts:每次发送目的地为组播地址的数据包时增加。
  • OutBcastPkts:每次发送目的地为广播地址的数据包时增加。
  • OutOctects:输出的数据包字节数。
  • OutMcastOctets:输出的组播数据包字节数。
  • OutBcastOctets:输出的广播数据包字节数。

其他统计数据记录在接收端博客文章中

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

Linux 网络设备子系统

在我们继续讨论 dev_queue_xmit 的数据包传输路径之前,让我们花一点时间来谈谈一些重要的概念,这些概念将出现在接下来的部分。

Linux 流量控制

Linux 支持一种叫做流量控制的特性。 此功能允许系统管理员控制如何从计算机传输数据包。 本文不会深入讨论 Linux 流量控制的各方面的细节。这篇文档提供了对系统、其控制和特性的深入研究。 有几个概念值得一提,以使下面看到的代码更容易理解。

流量控制系统包含几种不同的排队系统,它们为控制流量提供不同的功能。单个排队系统通常称为 qdisc,也称为排队规则。您可以将 qdisc 视为调度程序;qdisc 决定何时以及如何传输数据包。

在 Linux 上,每个接口都有一个与之关联的默认 qdisc。对于仅支持单个传输队列的网络硬件,使用默认 qdisc pfifo_fast。支持多个传输队列的网络硬件使用默认 qdisc mq。您可以运行 tc qdisc 来检查您的系统。

还需要注意的是,有些设备支持硬件流量控制,这可以让管理员将流量控制卸载到网络硬件上,从而节省系统上的 CPU 资源。

现在这些想法已经介绍过了,让我们从 ./net/core/dev.c 继续沿着 dev_queue_xmit 进行。

dev_queue_xmit__dev_queue_xmit

dev_queue_xmit__dev_queue_xmit 的一个简单包装:

int dev_queue_xmit(struct sk_buff *skb)
{
        return __dev_queue_xmit(skb, NULL);
}
EXPORT_SYMBOL(dev_queue_xmit);

在此之后,__dev_queue_xmit 是完成繁重工作的地方。 让我们一步一步地看一下这段代码,继续

static int __dev_queue_xmit(struct sk_buff *skb, void *accel_priv)
{
        struct net_device *dev = skb->dev;
        struct netdev_queue *txq;
        struct Qdisc *q;
        int rc = -ENOMEM;
        skb_reset_mac_header(skb);
        /* Disable soft irqs for various locks below. Also
         * stops preemption for RCU.
         */
        rcu_read_lock_bh();
        skb_update_prio(skb);

上面的代码开始于:

  1. 声明变量。
  2. 调用 skb_reset_mac_header 来准备要处理的 skb。 这将重置 skb 的内部指针,以便可以访问以太网报头。
  3. 调用 rcu_read_lock_bh 来准备读取 RCU 保护的数据结构。阅读更多关于安全使用 RCU 的信息
  4. 如果正在使用网络优先级 cgroup,调用 skb_update_prio 来设置 skb 的优先级。

现在,我们将开始更复杂的数据传输部分 ;)

txq = netdev_pick_tx(dev, skb, accel_priv);

在这里,代码试图确定要使用哪个传输队列。 正如您将在本文后面看到的,一些网络设备公开了多个传输队列来传输数据。 让我们来详细看看这是如何工作的。

netdev_pick_tx

netdev_pick_tx 代码位于 ./net/core/flow_dissector.c 中。 我们来看一下:

struct netdev_queue *netdev_pick_tx(struct net_device *dev,
                                    struct sk_buff *skb,
                                    void *accel_priv)
{
        int queue_index = 0;
        if (dev->real_num_tx_queues != 1) {
                const struct net_device_ops *ops = dev->netdev_ops;
                if (ops->ndo_select_queue)
                        queue_index = ops->ndo_select_queue(dev, skb,
                                                            accel_priv);
                else
                        queue_index = __netdev_pick_tx(dev, skb);
                if (!accel_priv)
                        queue_index = dev_cap_txqueue(dev, queue_index);
        }
        skb_set_queue_mapping(skb, queue_index);
        return netdev_get_tx_queue(dev, queue_index);
}

正如您在上面看到的,如果网络设备只支持单个传输队列,则会跳过更复杂的代码,并返回单个传输队列。 在高端服务器上使用的大多数设备具有多个传输队列。 具有多个传输队列的设备有两种情况:

  1. 驱动程序实现 ndo_select_queue,它可以以硬件或功能特定的方式更智能地选择传输队列,或者
  2. 驱动程序没有实现 ndo_select_queue,所以内核应该自己选择设备。

截止 3.13 内核,实现 ndo_select_queue 的驱动程序并不多。 bnx2x 和 ixgbe 驱动程序实现了此功能,但它仅用于以太网光纤通道(FCoE)。 鉴于此,让我们假设网络设备不实现ndo_select_queue 和/或 FCoE 未被使用。 在这种情况下,内核将选择具有 __netdev_pick_tx

一旦 __netdev_pick_tx 确定了队列的索引,skb_set_queue_mapping 将缓存该值(稍后将在流量控制代码中使用),netdev_get_tx_queue 将查找并返回指向该队列的指针。 在回到 __dev_queue_xmit 之前,让我们看看 __netdev_pick_tx 是如何工作 。

__netdev_pick_tx

让我们来看看内核如何选择传输队列来传输数据。 来自 ./net/core/flow_dissector.c

u16 __netdev_pick_tx(struct net_device *dev, struct sk_buff *skb)
{
        struct sock *sk = skb->sk;
        int queue_index = sk_tx_queue_get(sk);
        if (queue_index < 0 || skb->ooo_okay ||
            queue_index >= dev->real_num_tx_queues) {
                int new_index = get_xps_queue(dev, skb);
                if (new_index < 0)
                        new_index = skb_tx_hash(dev, skb);
                if (queue_index != new_index && sk &&
                    rcu_access_pointer(sk->sk_dst_cache))
                        sk_tx_queue_set(sk, new_index);
                queue_index = new_index;
        }
        return queue_index;
}

代码首先调用 sk_tx_queue_get 检查传输队列是否已经缓存在套接字上。如果没有缓存,则返回 -1

下一个 if 语句检查以下任一项是否为真:

  • queue_index 小于 0。 如果尚未设置队列,则会发生这种情况。
  • ooo_okay 标志置位 。 如果设置了该标志,则意味着现在允许乱序数据包。 协议层必须适当地设置此标志。 在流的所有未完成数据包都已确认时,TCP 协议层会设置此标志。 当这种情况发生时,内核可以为该数据包选择不同的传输队列。 UDP 协议层不设置此标志-因此 UDP 数据包永远不会设置 ooo_okay 为非零值。
  • 队列索引大于队列数。 如果用户最近通过 ethtool 更改了设备上的队列计数,则可能会发生这种情况。 稍后会详细介绍。

以上任一情况下,代码都会进入慢速路径以获取传输队列。首先调用 get_xps_queue,它试图使用用户配置映射传输队列到 CPU。这称为“Transmit Packet Steering(XPS)”。我们稍后将更详细地了解 Transmit Packet Steering(XPS) 是什么以及它是如何工作的。

如果 get_xps_queue 返回 -1,则此内核不支持 XPS,或系统管理员未配置 XPS,或配置的映射指向无效队列,则代码将继续调用 skb_tx_hash

一旦使用 XPS 或内核自动使用 skb_tx_hash 选择了队列,将使用 sk_tx_queue_set 缓存该队列到套接字对象上,并返回。在继续 dev_queue_xmit 之前,让我们看看 XPS 和 skb_tx_hash 是如何工作的。

目录
相关文章
|
5月前
|
监控 Linux
Linux的epoll用法与数据结构data、event
Linux的epoll用法与数据结构data、event
59 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
120 0
|
11月前
|
Linux
linux下的内存查看(virt,res,shr,data的意义)
linux下的内存查看(virt,res,shr,data的意义)
155 0
|
SQL 存储 缓存
译|Monitoring and Tuning the Linux Networking Stack: Sending Data(十)
译|Monitoring and Tuning the Linux Networking Stack: Sending Data(十)
318 1
|
SQL 缓存 监控
译|Monitoring and Tuning the Linux Networking Stack: Sending Data(十一)
译|Monitoring and Tuning the Linux Networking Stack: Sending Data(十一)
131 0
|
5天前
|
Linux Python Perl
Linux命令删除文件里的字符串
Linux命令删除文件里的字符串
17 7
|
5天前
|
Shell Linux
Linux shell编程学习笔记82:w命令——一览无余
Linux shell编程学习笔记82:w命令——一览无余
|
7天前
|
Linux Perl
Linux之sed命令
Linux之sed命令
|
7天前
|
Linux
深入理解Linux中的cp命令:文件与目录的复制利器
深入理解Linux中的cp命令:文件与目录的复制利器
|
7天前
|
Linux Docker 容器
9. 同步执行Linux多条命令
9. 同步执行Linux多条命令
下一篇
无影云桌面