__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.c 的 inet_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 协议层中看到的以下模式:
- 调用
ip_rcv
做一些初始簿记。 - 移交数据包给 netfilter 进行处理,并带有一个指针,指向处理完成时要执行的回调。
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 层的入口点。 让我们继续旅程。