苏州市做网站,东莞网站建设如何做,有哪些网站做简历比较好,网站服务器续费__netif_receive_skb_core 是 Linux 内核网络子系统中一个非常重要的函数#xff0c;它负责将网络设备驱动层接收到的数据包传递到上层协议栈进行处理。以下是对该函数的一些关键点的详细解析#xff1a;
一、函数作用
__netif_receive_skb_core 函数是处理接收到的网络数据…__netif_receive_skb_core 是 Linux 内核网络子系统中一个非常重要的函数它负责将网络设备驱动层接收到的数据包传递到上层协议栈进行处理。以下是对该函数的一些关键点的详细解析
一、函数作用
__netif_receive_skb_core 函数是处理接收到的网络数据包的核心函数之一。它从网络设备驱动接收数据包通常通过 sk_buff 结构体表示并根据注册的协议处理函数通过 packet_type 结构体注册将数据包传递给相应的上层协议栈进行处理如 IP 层、ARP 层等。
二、函数调用关系
在 Linux 内核中数据包的接收通常涉及多个函数的调用。从网络设备驱动层开始数据包可能会经过 netif_receive_skb - netif_receive_skb_internal - __netif_receive_skb在某些内核版本中可能直接调用 __netif_receive_skb_core等函数的传递最终到达 __netif_receive_skb_core 函数进行处理。
三、函数实现细节
记录收包时间和设备 函数首先会记录收包时间并检查是否有包延迟。同时记录收包设备即数据包是从哪个网络设备接收到的。重置各层头部 为了后续协议栈的正确处理函数会重置网络层、传输层和 MAC 层的头部指针。处理 VLAN 报文 如果数据包是 VLAN 报文即带有 VLAN 标签函数会去除 VLAN 头以便后续协议栈能够正确处理。遍历协议处理链表 函数会遍历两个链表ptype_all 和 ptype_base。这两个链表上挂载了多个 packet_type 结构体每个结构体对应一个具体的协议处理函数。对于 ptype_all 链表上的每个 packet_type 结构体函数会调用其对应的协议处理函数但通常最后一个除外以优化性能。对于 ptype_base 链表函数会根据数据包的协议类型选择相应的 packet_type 结构体并调用其处理函数。减少 skb 复制 为了提高性能函数在遍历链表时采用了一种优化策略即利用 pt_prev 变量来减少最后一次协议处理时的 skb 复制。这是通过控制 skb 的引用计数来实现的。统计和错误处理 函数会更新处理包数的统计信息。如果在处理过程中遇到错误如无法分配内存、无法找到合适的协议处理函数等函数会进行相应的错误处理。
四、总结
__netif_receive_skb_core 函数是 Linux 内核网络子系统中处理接收到的网络数据包的关键函数之一。它通过记录收包信息、重置头部指针、处理 VLAN 报文、遍历协议处理链表以及减少 skb 复制等步骤将数据包高效地传递给上层协议栈进行处理。这一过程中涉及了多个内核机制和数据结构的使用如 RCU 读锁保护、sk_buff 结构体、packet_type 结构体等。 static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc) // 将skb传递到上层
{struct packet_type *ptype, *pt_prev;rx_handler_func_t *rx_handler;struct net_device *orig_dev;struct net_device *null_or_dev;bool deliver_exact false;//默认不精确传递int ret NET_RX_DROP;//默认收报失败__be16 type;net_timestamp_check(!netdev_tstamp_prequeue, skb);//记录收包时间netdev_tstamp_prequeue为0表示可能有包延迟 trace_netif_receive_skb(skb);orig_dev skb-dev;//记录收包设备 skb_reset_network_header(skb);//重置network header此时skb指向IP头没有vlan的情况下if (!skb_transport_header_was_set(skb))skb_reset_transport_header(skb);skb_reset_mac_len(skb);// 留下一个节点最后一次向上层传递时不需要再inc引用回调中会free这样相当于少调用了一次freept_prev NULL;another_round:skb-skb_iif skb-dev-ifindex;//设置接收设备索引号 __this_cpu_inc(softnet_data.processed);//处理包数统计 if (skb-protocol cpu_to_be16(ETH_P_8021Q) ||skb-protocol cpu_to_be16(ETH_P_8021AD)) {//vxlan报文处理剥除vxlan头skb skb_vlan_untag(skb);//剥除vxlan头if (unlikely(!skb))goto out;}#ifdef CONFIG_NET_CLS_ACTif (skb-tc_verd TC_NCLS) {skb-tc_verd CLR_TC_NCLS(skb-tc_verd);goto ncls;}
#endifif (pfmemalloc)此类报文不允许ptype_all处理即tcpdump也抓不到goto skip_taps;//先处理 ptype_all 上所有的 packet_type-func() //所有包都会调func对性能影响严重所有有的钩子是随模块加载挂上的。list_for_each_entry_rcu(ptype, ptype_all, list) {//遍历ptye_all链表if (!ptype-dev || ptype-dev skb-dev) {//上面的paket_type.type 为 ETH_P_ALL典型场景就是tcpdump抓包所使用的协议if (pt_prev)//pt_prev提高效率ret deliver_skb(skb, pt_prev, orig_dev);//此函数最终调用paket_type.func()pt_prev ptype;} }skip_taps:
#ifdef CONFIG_NET_CLS_ACTif (static_key_false(ingress_needed)) {skb handle_ing(skb, pt_prev, ret, orig_dev);if (!skb)goto out;}skb-tc_verd 0;
ncls:
#endifif (pfmemalloc !skb_pfmemalloc_protocol(skb))//不支持使用pfmemalloc goto drop;if (skb_vlan_tag_present(skb)) {// 如果是vlan包 if (pt_prev) {/* 处理pt_prev */ret deliver_skb(skb, pt_prev, orig_dev);pt_prev NULL;}if (vlan_do_receive(skb))/* 根据实际的vlan设备调整信息再走一遍 */goto another_round;else if (unlikely(!skb))goto out;}
/*如果一个dev被添加到一个bridge做为bridge的一个接口)这个接口设备的rx_handler将被设置为br_handle_frame函数这是在br_add_if函数中设置的而br_add_if (net/bridge/br_if.c)是在向网桥设备上添加接口时设置的。进入br_handle_frame也就进入了bridge的逻辑代码。*/rx_handler rcu_dereference(skb-dev-rx_handler);/* 如果有注册handler那么调用比如网桥模块 */if (rx_handler) {if (pt_prev) {ret deliver_skb(skb, pt_prev, orig_dev);pt_prev NULL;}switch (rx_handler(skb)) {case RX_HANDLER_CONSUMED:/* 已处理无需进一步处理 */ret NET_RX_SUCCESS;goto out;case RX_HANDLER_ANOTHER:/* 修改了skb-dev在处理一次 */goto another_round;case RX_HANDLER_EXACT:/* 精确传递到ptype-dev skb-dev */deliver_exact true;case RX_HANDLER_PASS:break;default:BUG();}}if (unlikely(skb_vlan_tag_present(skb))) {/* 还有vlan标记说明找不到vlanid对应的设备 */if (skb_vlan_tag_get_id(skb))/* 存在vlanid则判定是到其他设备的包 */skb-pkt_type PACKET_OTHERHOST;/* Note: we might in the future use prio bits* and set skb-priority like in vlan_do_receive()* For the time being, just ignore Priority Code Point*/skb-vlan_tci 0;}/* deliver only exact match when indicated */null_or_dev deliver_exact ? skb-dev : NULL;//指定精确传递的话就精确传递,否则向未指定设备的指定协议全局发送一份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;}}if (pt_prev) {if (unlikely(skb_orphan_frags(skb, GFP_ATOMIC)))goto drop;else//使用pt_prev这里就不需要deliver_skb来inc应用数了, func执行内部会free减少了一次skb_freeret pt_prev-func(skb, skb-dev, pt_prev, orig_dev);/* 传递到上层*/} else {
drop:if (!deliver_exact)atomic_long_inc(skb-dev-rx_dropped);//网卡丢包计数elseatomic_long_inc(skb-dev-rx_nohandler);kfree_skb(skb);/* Jamal, now you will not able to escape explaining* me how you were going to use this. :-)*/ret NET_RX_DROP;}
out:return ret;
} ---------------------------------------------------------------------------------------------------------------------------------
skb-dev结构体
在 Linux 内核中skb-dev 指向的是一个 struct net_device 类型的指针该结构体代表了一个网络设备。struct net_device 是网络子系统中的核心数据结构之一它包含了网络设备几乎所有的配置信息和状态信息。
struct net_device 结构体的定义可能会随着内核版本的不同而有所变化但通常包括以下几个关键部分
设备基本信息 char name[IFNAMSIZ]网络设备的名称如 eth0、lo 等。unsigned long state设备的状态标志如是否已启动、是否正在接收数据等。unsigned long flags设备的标志位用于控制设备的行为如是否支持多播、是否支持巨型帧等。硬件地址 unsigned char dev_addr[MAX_ADDR_LEN]设备的硬件地址MAC 地址。统计信息 结构体中包含了一系列的计数器用于统计接收和发送的数据包数量、错误数量等信息。队列和中断处理 包括接收和发送队列的指针以及中断处理函数的指针。协议处理 struct packet_type *ptype_all 和 struct list_head ptype_all用于挂载全局的协议处理函数链表。struct list_head ptype_specific用于挂载特定于该设备的协议处理函数链表。设备私有数据 void *priv指向设备私有数据的指针该数据对于不同类型的网络设备可能是不同的。设备方法 结构体中包含了一系列的函数指针这些函数指针指向了处理设备特定操作如启动、停止、发送数据包等的函数。其他配置和状态信息 包括 MTU最大传输单元、设备类型、速度、双工模式等信息。
请注意由于内核的不断发展和更新struct net_device 结构体的具体定义可能会发生变化。因此在查看或修改内核代码时最好参考您正在使用的内核版本的源代码。
此外由于 struct net_device 结构体包含了大量的信息和功能因此在实际编程中通常不需要直接操作整个结构体。相反内核提供了丰富的 API 和函数来查询和修改网络设备的状态和行为。 ---------------------------------------------------------------------------------------------------------------------------------
ptype_all 在 Linux 内核网络子系统中是一个非常重要的链表头它挂载了一系列 packet_type 结构体。每个 packet_type 结构体代表了一个协议处理函数用于接收和处理网络数据包。ptype_all 链表是全局的意味着它包含了所有注册到内核的网络协议处理函数这些函数可以接收来自任何网络设备的数据包。
ptype_all 的作用和特点 全局性ptype_all 是一个全局链表头它允许内核中的任何网络协议处理函数注册自己以便接收和处理网络数据包。 可扩展性通过向 ptype_all 链表添加新的 packet_type 结构体可以轻松地扩展内核的网络协议处理能力。这为新协议的开发和现有协议的修改提供了便利。 高效性虽然 ptype_all 链表可能包含大量的协议处理函数但内核通过优化遍历和匹配逻辑确保数据包能够高效地传递给正确的处理函数。
packet_type 结构体
packet_type 结构体通常包含以下关键字段
type指定了数据包类型用于匹配接收到的数据包。func指向协议处理函数的指针当数据包与 packet_type 结构体匹配时将调用此函数来处理数据包。list用于将 packet_type 结构体链接到 ptype_all 链表或其他链表中的双向链表节点。
使用场景
网络协议开发在开发新的网络协议时可以通过注册一个 packet_type 结构体到 ptype_all 链表来接收和处理相应的数据包。网络监控和分析一些网络监控和分析工具可能会注册自己的协议处理函数到 ptype_all 链表以便捕获和分析所有经过内核的网络数据包。网络协议修改当需要修改现有网络协议的行为时可以通过修改或替换已注册的 packet_type 结构体来实现。
注意事项
性能影响由于 ptype_all 链表可能包含大量的协议处理函数因此遍历链表可能会对性能产生一定影响。内核通过优化遍历逻辑来减少这种影响。同步问题在注册或注销 packet_type 结构体时需要确保与数据包接收和处理相关的同步问题得到妥善处理。这通常涉及到使用 RCURead-Copy Update等同步机制来保护链表和相关的数据结构。
结论
ptype_all 是 Linux 内核网络子系统中用于管理和分发网络数据包的核心组件之一。通过向 ptype_all 链表注册协议处理函数内核能够灵活地扩展和修改其网络协议处理能力。
---------------------------------------------------------------------------------------------------------------------------------
packet_type struct packet_type {__be16 type; /* This is really htons(ether_type). */struct net_device *dev; /* NULL is wildcarded here */int (*func) (struct sk_buff *,struct net_device *,struct packet_type *,struct net_device *);bool (*id_match)(struct packet_type *ptype,struct sock *sk);void *af_packet_priv;struct list_head list;
}; static struct packet_type ip_packet_type __read_mostly {.type cpu_to_be16(ETH_P_IP),.func ip_rcv,
}; packet_type结构体的链表管理
在Linux内核中packet_type结构体用于表示网络协议处理函数的接口。这些结构体被组织在链表中以便内核能够高效地遍历它们并将接收到的网络数据包分发给正确的处理函数。
全局链表ptype_all
定义ptype_all是一个全局链表头挂载了所有注册到内核的网络协议处理函数。这些处理函数可以接收来自任何网络设备的数据包。用途主要用于分析目的接收所有到达网络协议栈的数据包无论其协议类型如何。
基于协议类型的链表ptype_base
定义虽然直接称为“packet_base链”的概念可能不存在但内核通常使用类似ptype_base的数组来管理基于协议类型的链表。这个数组的每个元素都是一个链表头指向具有相同协议类型哈希值的packet_type结构体链表。生成方式 当注册一个新的packet_type结构体时通常通过调用dev_add_pack函数内核会根据该结构体的type字段即协议类型计算一个哈希值。然后内核将新的packet_type结构体添加到ptype_base数组中对应哈希值的链表上。用途这种方式允许内核根据数据包的协议类型快速定位到相应的处理函数链表从而提高数据包分发的效率。
示例代码概念性
以下是一个概念性的示例展示了如何将packet_type结构体添加到基于协议类型的链表中注意这不是实际的内核代码 c复制代码
// 假设ptype_base是一个包含多个链表头的数组 struct list_head ptype_base[PTYPE_HASH_SIZE]; // PTYPE_HASH_SIZE是哈希表的大小 // dev_add_pack函数的简化版本 void dev_add_pack(struct packet_type *pt) { unsigned int hash ntohs(pt-type) (PTYPE_HASH_SIZE - 1); // 计算哈希值 list_add_rcu(pt-list, ptype_base[hash]); // 添加到对应哈希值的链表上 }
结论
虽然“packet_base链”不是Linux内核中的一个标准术语但基于协议类型的链表管理机制通常通过类似ptype_base的数组实现是内核网络子系统中的一个重要组成部分。通过这种机制内核能够高效地处理和分发接收到的网络数据包。
--------------------------------------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------------------------------------
ip_rcv static inline int dst_input(struct sk_buff *skb)
{return skb_dst(skb)-input(skb);
} 在Linux内核网络子系统中skb_dst(skb)-input(skb) 这一行代码执行了一个重要的操作但它并不是直接可执行的因为这里涉及到几个步骤和假设我将逐一解释。 首先skb 是指向 struct sk_buff 结构体的指针该结构体用于表示网络数据包。struct sk_buff 包含了数据包的所有信息包括数据本身、元数据如源地址、目的地址、协议类型等以及指向各种相关数据结构如路由缓存项的指针。 skb_dst(skb) 这个宏或函数用于获取与 skb 相关联的路由缓存项routing cache entry。在内核中路由缓存项通常存储在 skb 的一个特定字段中以便快速访问与数据包路由相关的信息。skb_dst(skb) 返回的是一个指向 struct dst_entry 结构体的指针该结构体包含了路由的详细信息如下一跳地址、输出接口等。dst_entry-input struct dst_entry 结构体中有一个 input 字段它是一个函数指针指向一个特定的函数该函数负责处理通过该路由缓存项接收到的数据包。这个 input 函数是协议栈的一部分它根据数据包的协议类型如IP、IPv6等和路由信息来决定如何进一步处理数据包。例如对于IP数据包input 函数可能会将数据包传递给IP层的处理函数。skb_dst(skb)-input(skb) 当这行代码被执行时它实际上是在调用与 skb 相关联的路由缓存项的 input 函数并将 skb 作为参数传递给它。这个调用是数据包在网络协议栈中传递的关键步骤之一它允许协议栈根据路由信息和数据包类型来正确地处理数据包。 需要注意的是skb_dst(skb) 可能会返回 NULL如果 skb 没有与任何路由缓存项相关联例如在数据包刚刚被网络设备接收但尚未进行路由查找的情况下。因此在实际代码中通常会在调用 skb_dst(skb)-input(skb) 之前检查 skb_dst(skb) 是否为 NULL。 报文提交给内核协议栈处理后最终会调用到__netif_receive_skb_core函数如果报文没有被网桥处理函数rx_handler消费掉最终会交给ptype_base中注册的协议处理包括内核注册的协议也包括raw socket等创建的协议处理。本文将分析普通ipv4 报文的处理过程处理入口函数为ip_rcv函数。 主要调用流程ip_rcv--ip_rcv_finish--ip_local_deliver--ip_local_deliver_finish ip_rcv: /** 主要作用
*1类型为ETH_P_IP类型的数据包被传递到三层调用ip_rcv函数* (2) ip_rcv完成基本的校验 主要检查计算的校验和与首部中存储的校验和是否一致和处理工作后**经过PRE_ROUTING钩子点* (3) 经过PRE_ROUTING钩子点之后调用ip_rcv_finish完成数据包接收包括选项处理路由查询并且*根据路由决定数据包是发往本机还是转发*/
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)
{const struct iphdr *iph;u32 len;/* When the interface is in promisc. mode, drop all the crap* that it receives, do not try to analyse it.*/if (skb-pkt_type PACKET_OTHERHOST)//丢弃掉不是发往本机的报文,网卡开启混杂模式会收到此类报文goto drop;IP_UPD_PO_STATS_BH(dev_net(dev), IPSTATS_MIB_IN, skb-len);if ((skb skb_share_check(skb, GFP_ATOMIC)) NULL) {//检查是否skb为share是 则克隆报文IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INDISCARDS);goto out;}if (!pskb_may_pull(skb, sizeof(struct iphdr)))//确保skb还可以容纳标准的报头(即20字节)goto inhdr_error;iph ip_hdr(skb);//得到IP头/** RFC1122: 3.2.1.2 MUST silently discard any IP frame that fails the checksum.** Is the datagram acceptable?** 1. Length at least the size of an ip header* 2. Version of 4* 3. Checksums correctly. [Speed optimisation for later, skip loopback checksums]* 4. Doesnt have a bogus length*/if (iph-ihl 5 || iph-version ! 4)//ip头长度至少为20字节ihl5后面计算头长度会乘4,只支持v4goto inhdr_error;BUILD_BUG_ON(IPSTATS_MIB_ECT1PKTS ! IPSTATS_MIB_NOECTPKTS INET_ECN_ECT_1);BUILD_BUG_ON(IPSTATS_MIB_ECT0PKTS ! IPSTATS_MIB_NOECTPKTS INET_ECN_ECT_0);BUILD_BUG_ON(IPSTATS_MIB_CEPKTS ! IPSTATS_MIB_NOECTPKTS INET_ECN_CE);IP_ADD_STATS_BH(dev_net(dev),IPSTATS_MIB_NOECTPKTS (iph-tos INET_ECN_MASK),max_t(unsigned short, 1, skb_shinfo(skb)-gso_segs));if (!pskb_may_pull(skb, iph-ihl*4))//确保skb还可以容纳实际的报头(ihl*4)goto inhdr_error;iph ip_hdr(skb);if (unlikely(ip_fast_csum((u8 *)iph, iph-ihl)))//ip头csum校验goto csum_error;len ntohs(iph-tot_len);//获取ip分组总长即ip首部加数据的长度if (skb-len len) {//skb的实际总长度小于ip分组总长则dropIP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INTRUNCATEDPKTS);goto drop;} else if (len (iph-ihl*4))//ip头记录的分组长度就大于数据总长则出错goto inhdr_error;/* Our transport medium may have padded the buffer out. Now we know it* is IP we can trim to the true length of the frame.* Note this now means skb-len holds ntohs(iph-tot_len).*/if (pskb_trim_rcsum(skb, len)) {//去除多余的字节IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INDISCARDS);goto drop;} else if (len (iph-ihl*4))goto inhdr_error;/* Our transport medium may have padded the buffer out. Now we know it* is IP we can trim to the true length of the frame.* Note this now means skb-len holds ntohs(iph-tot_len).*/if (pskb_trim_rcsum(skb, len)) {IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INDISCARDS);goto drop;}skb-transport_header skb-network_header iph-ihl*4;//设置传输层header/* Remove any debris in the socket control block */memset(IPCB(skb), 0, sizeof(struct inet_skb_parm));//清空cb即inet_skb_parm值/* Must drop socket now because of tproxy. */skb_orphan(skb);//调用netfilter实现iptables功能通过后调用ip_rcv_finishreturn NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, NULL, skb,dev, NULL,ip_rcv_finish);csum_error:IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_CSUMERRORS);
inhdr_error:IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INHDRERRORS);
drop:kfree_skb(skb);
out:return NET_RX_DROP;
} ip_rcv_finish函数 作用 1、确定数据包是转发还是在本机协议栈上传如果是转发要确定输出网络设备和下一个接受栈的地址。 2、解析和处理部分IP选项 static int ip_rcv_finish(struct sock *sk, struct sk_buff *skb)
{const struct iphdr *iph ip_hdr(skb);struct rtable *rt;int err;/* if ingress device is enslaved to an L3 master device pass the* skb to its handler for processing*/skb l3mdev_ip_rcv(skb);if (!skb)return NET_RX_SUCCESS;if (sysctl_ip_early_demux !skb_dst(skb) skb-sk NULL) {const struct net_protocol *ipprot;int protocol iph-protocol;//得到传输层协议/* 找到early_demux函数如是tcp协议就调用tcp_v4_early_demux */ipprot rcu_dereference(inet_protos[protocol]);if (ipprot ipprot-early_demux) {//对于socket报文可以通过socket快速获取路由表err ipprot-early_demux(skb);/* 调用该函数将路由信息缓存到_skb-refdst */if (unlikely(err))goto drop_error;/* must reload iph, skb-head might have changed */iph ip_hdr(skb);//重新获取ip头}}/** Initialise the virtual path cache for the packet. It describes* how the packet travels inside Linux networking.*//* 1. 为数据包初始化虚拟路径缓存它描述了数据包是如何在linux网络中传播的 2. 通常从外界接收的数据包,skb-dst不会包含路由信息,暂时还不知道在何处会设置这个字段3. skb-dst该数据域包含了如何到达目的地址的路由信息如果该数据域是NULL就通过路由子系统函数ip_route_input_noref路由ip_route_input_noref的输入参数有源IP地址、目的IP地址、服务类型、接受数据包的网络设备根据这5个参数决策路由。*/if (!skb_valid_dst(skb)) {// 路由查询决定后续处理向上传递 ip_local_deliver、转发ip_forward、丢弃err ip_route_input_noref(skb, iph-daddr, iph-saddr,iph-tos, skb-dev);if (unlikely(err))goto drop_error;}#ifdef CONFIG_IP_ROUTE_CLASSIDif (unlikely(skb_dst(skb)-tclassid)) {struct ip_rt_acct *st this_cpu_ptr(ip_rt_acct);u32 idx skb_dst(skb)-tclassid;st[idx0xFF].o_packets;//更新接收数据包数量st[idx0xFF].o_bytes skb-len;//更新接收数据包的长度st[(idx16)0xFF].i_packets;st[(idx16)0xFF].i_bytes skb-len;}
#endifif (iph-ihl 5 ip_rcv_options(skb))goto drop;rt skb_rtable(skb);//得到路由表项统计组播和广播报文if (rt-rt_type RTN_MULTICAST) {IP_UPD_PO_STATS_BH(dev_net(rt-dst.dev), IPSTATS_MIB_INMCAST,skb-len);} else if (rt-rt_type RTN_BROADCAST)IP_UPD_PO_STATS_BH(dev_net(rt-dst.dev), IPSTATS_MIB_INBCAST,skb-len);return dst_input(skb);/*ip_rcv_finish的结束是调用了dst_input实际是调用存放在skb-dst-input的数据域。该函数确定了下一步对数据包的处理根据数据包的目的地地址skb-dst-input字段的信息主要由路由处理流程确定可能是往本地协议栈上传就调用 ip_local_deliver如果是转发就调用ip_forward */drop:kfree_skb(skb);return NET_RX_DROP;drop_error:if (err -EXDEV)NET_INC_STATS_BH(dev_net(skb-dev), LINUX_MIB_IPRPFILTER);goto drop;
} 2.ip_rcv_finish函数 作用 1、确定数据包是转发还是在本机协议栈上传如果是转发要确定输出网络设备和下一个接受栈的地址。 2、解析和处理部分IP选项 static int ip_rcv_finish(struct sock *sk, struct sk_buff *skb)
{const struct iphdr *iph ip_hdr(skb);struct rtable *rt;int err;/* if ingress device is enslaved to an L3 master device pass the* skb to its handler for processing*/skb l3mdev_ip_rcv(skb);if (!skb)return NET_RX_SUCCESS;if (sysctl_ip_early_demux !skb_dst(skb) skb-sk NULL) {const struct net_protocol *ipprot;int protocol iph-protocol;//得到传输层协议/* 找到early_demux函数如是tcp协议就调用tcp_v4_early_demux */ipprot rcu_dereference(inet_protos[protocol]);if (ipprot ipprot-early_demux) {//对于socket报文可以通过socket快速获取路由表err ipprot-early_demux(skb);/* 调用该函数将路由信息缓存到_skb-refdst */if (unlikely(err))goto drop_error;/* must reload iph, skb-head might have changed */iph ip_hdr(skb);//重新获取ip头}}/** Initialise the virtual path cache for the packet. It describes* how the packet travels inside Linux networking.*//* 1. 为数据包初始化虚拟路径缓存它描述了数据包是如何在linux网络中传播的 2. 通常从外界接收的数据包,skb-dst不会包含路由信息,暂时还不知道在何处会设置这个字段3. skb-dst该数据域包含了如何到达目的地址的路由信息如果该数据域是NULL就通过路由子系统函数ip_route_input_noref路由ip_route_input_noref的输入参数有源IP地址、目的IP地址、服务类型、接受数据包的网络设备根据这5个参数决策路由。*/if (!skb_valid_dst(skb)) {// 路由查询决定后续处理向上传递 ip_local_deliver、转发ip_forward、丢弃err ip_route_input_noref(skb, iph-daddr, iph-saddr,iph-tos, skb-dev);if (unlikely(err))goto drop_error;}#ifdef CONFIG_IP_ROUTE_CLASSIDif (unlikely(skb_dst(skb)-tclassid)) {struct ip_rt_acct *st this_cpu_ptr(ip_rt_acct);u32 idx skb_dst(skb)-tclassid;st[idx0xFF].o_packets;//更新接收数据包数量st[idx0xFF].o_bytes skb-len;//更新接收数据包的长度st[(idx16)0xFF].i_packets;st[(idx16)0xFF].i_bytes skb-len;}
#endifif (iph-ihl 5 ip_rcv_options(skb))goto drop;rt skb_rtable(skb);//得到路由表项统计组播和广播报文if (rt-rt_type RTN_MULTICAST) {IP_UPD_PO_STATS_BH(dev_net(rt-dst.dev), IPSTATS_MIB_INMCAST,skb-len);} else if (rt-rt_type RTN_BROADCAST)IP_UPD_PO_STATS_BH(dev_net(rt-dst.dev), IPSTATS_MIB_INBCAST,skb-len);return dst_input(skb);/*ip_rcv_finish的结束是调用了dst_input实际是调用存放在skb-dst-input的数据域。该函数确定了下一步对数据包的处理根据数据包的目的地地址skb-dst-input字段的信息主要由路由处理流程确定可能是往本地协议栈上传就调用 ip_local_deliver如果是转发就调用ip_forward */drop:kfree_skb(skb);return NET_RX_DROP;drop_error:if (err -EXDEV)NET_INC_STATS_BH(dev_net(skb-dev), LINUX_MIB_IPRPFILTER);goto drop;
}
ip_route_input会进行路由表查询该函数直接或间接决定了报文之后要往何处传递。是进行本地传递还是转发。
我们可以看到如果报文没有被drop掉那么报文最终会被dst_input(skb)处理。dst_input(skb)实际上执行的是skb-dst-input(skb)。而这里的input函数其实就是由ip_route_input决定的。
对于应该本地传递的报文input指针会指向ip_local_deliver。对于该转发的报文input会指向ip_forward
/** Deliver IP Packets to the higher protocol layers.*/
int ip_local_deliver(struct sk_buff *skb)
{/* * Reassemble IP fragments.*/if (ip_hdr(skb)-frag_off htons(IP_MF | IP_OFFSET)) {if (ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER))return 0;} return NF_HOOK(PF_INET, NF_INET_LOCAL_IN, skb, skb-dev, NULL,ip_local_deliver_finish);
}
我们知道IPv4要将报文传送给上层协议本地传递那它需要对分段的报文进行重组ip_defrag即完成报文重组。 然后由调用Netfilter决定是否调用ip_local_deliver_finish。
ip_local_deliver_finish
ret ipprot-handler(skb);
static int ip_local_deliver_finish(struct sk_buff *skb)
{struct net *net dev_net(skb-dev);__skb_pull(skb, ip_hdrlen(skb)); /* 跳过IP头部 *//* Point into the IP datagram, just past the header. *//* 设置传输层头部位置 */skb_reset_transport_header(skb);rcu_read_lock();{int protocol ip_hdr(skb)-protocol; //取出ip头中的协议.int hash, raw;const struct net_protocol *ipprot;resubmit:// 若是raw socket发送的需要做相应的处理clone数据包raw raw_local_deliver(skb, protocol); //得到raw socket, 如果不是raw socket,则返回0hash protocol (MAX_INET_PROTOS - 1); // 计算传输层协议处理结构在inet_protos数组hash表中的位置ipprot rcu_dereference(inet_protos[hash]); // 获取传输层协议处理指针if (ipprot ! NULL) {int ret;//主要是ipprot是否有被当前主机注册if (!net_eq(net, init_net) !ipprot-netns_ok) { // 若获取到了对应传输层的处理结构if (net_ratelimit())printk(%s: proto %d isnt netns-ready\n,__func__, protocol);kfree_skb(skb);goto out;}//判断ipsec,并进行相关处理. if (!ipprot-no_policy) {if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {kfree_skb(skb);goto out;}nf_reset(skb);}//调用handler,进入相应的4层协议的处理.ret ipprot-handler(skb);if (ret 0) { // 处理数据包失败再次尝试protocol -ret;goto resubmit;}IP_INC_STATS_BH(net, IPSTATS_MIB_INDELIVERS);// 添加数据包处理统计信息} else {// 若没有找到相应传输层的处理函数if (!raw) {if (xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {IP_INC_STATS_BH(net, IPSTATS_MIB_INUNKNOWNPROTOS);icmp_send(skb, ICMP_DEST_UNREACH,ICMP_PROT_UNREACH, 0);}} elseIP_INC_STATS_BH(net, IPSTATS_MIB_INDELIVERS);kfree_skb(skb);}}out:rcu_read_unlock();return 0;
}
UDP报文接收ret ipprot-handler(skb);
udp_rev
int udp_rcv(struct sk_buff *skb)
{struct net_data_s net_data;net_data.pskb skb;/* ecnt_dp_hook */ECNT_UDP_RCV_HOOK(ECNT_NET_UDP_RCV,net_data);return __udp4_lib_rcv(skb, udp_table, IPPROTO_UDP);
}
EXPORT_SYMBOL(udp_rcv);
__udp4_lib_rcv函数
__udp4_lib_lookup_skb 是根据 skb 来寻找对应的socket当找到以后将数据包放到 socket 的缓存队列⾥。如果没有找到则发送⼀个⽬标不可达的 icmp 包。
ret udp_queue_rcv_skb(sk, skb);
int __udp4_lib_rcv(struct sk_buff *skb, struct udp_table *udptable,int proto)
{struct sock *sk;struct udphdr *uh;unsigned short ulen;struct rtable *rt skb_rtable(skb);__be32 saddr, daddr;struct net *net dev_net(skb-dev);/** Validate the packet.*/if (!pskb_may_pull(skb, sizeof(struct udphdr)))goto drop; /* No space for header. */uh udp_hdr(skb);ulen ntohs(uh-len);saddr ip_hdr(skb)-saddr;daddr ip_hdr(skb)-daddr;if (ulen skb-len)goto short_packet;if (proto IPPROTO_UDP) {/* UDP validates ulen. */if (ulen sizeof(*uh) || pskb_trim_rcsum(skb, ulen))goto short_packet;uh udp_hdr(skb);}if (udp4_csum_init(skb, uh, proto))goto csum_error;sk skb_steal_sock(skb);if (sk) {struct dst_entry *dst skb_dst(skb);int ret;if (unlikely(sk-sk_rx_dst ! dst))udp_sk_rx_dst_set(sk, dst);ret udp_queue_rcv_skb(sk, skb);sock_put(sk);/* a return value 0 means to resubmit the input, but* it wants the return to be -protocol, or 0*/if (ret 0)return -ret;return 0;}if (rt-rt_flags (RTCF_BROADCAST|RTCF_MULTICAST))return __udp4_lib_mcast_deliver(net, skb, uh,saddr, daddr, udptable, proto);sk __udp4_lib_lookup_skb(skb, uh-source, uh-dest, udptable);if (sk) {int ret;if (inet_get_convert_csum(sk) uh-check !IS_UDPLITE(sk))skb_checksum_try_convert(skb, IPPROTO_UDP, uh-check,inet_compute_pseudo);ret udp_queue_rcv_skb(sk, skb);sock_put(sk);/* a return value 0 means to resubmit the input, but* it wants the return to be -protocol, or 0*/if (ret 0)return -ret;return 0;}if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb))goto drop;nf_reset(skb);/* No socket. Drop packet silently, if checksum is wrong */if (udp_lib_checksum_complete(skb))goto csum_error;UDP_INC_STATS_BH(net, UDP_MIB_NOPORTS, proto IPPROTO_UDPLITE);icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0);/** Hmm. We got an UDP packet to a port to which we* dont wanna listen. Ignore it.*/kfree_skb(skb);return 0;short_packet:net_dbg_ratelimited(UDP%s: short packet: From %pI4:%u %d/%d to %pI4:%u\n,proto IPPROTO_UDPLITE ? Lite : ,saddr, ntohs(uh-source),ulen, skb-len,daddr, ntohs(uh-dest));goto drop;csum_error:/** RFC1122: OK. Discards the bad packet silently (as far as* the network is concerned, anyway) as per 4.1.3.4 (MUST).*/net_dbg_ratelimited(UDP%s: bad checksum. From %pI4:%u to %pI4:%u ulen %d\n,proto IPPROTO_UDPLITE ? Lite : ,saddr, ntohs(uh-source), daddr, ntohs(uh-dest),ulen);UDP_INC_STATS_BH(net, UDP_MIB_CSUMERRORS, proto IPPROTO_UDPLITE);
drop:UDP_INC_STATS_BH(net, UDP_MIB_INERRORS, proto IPPROTO_UDPLITE);kfree_skb(skb);return 0;
}/*__udp4_lib_rcv 函数是 Linux 内核中处理 IPv4 UDP 数据包接收的核心函数之一。它负责验证数据包的有效性、查找相关的套接字socket、并将数据包传递给正确的套接字进行处理。以下是对该函数主要逻辑的详细解释验证数据包
首先函数检查是否有足够的空间来拉取 UDP 头部。如果没有则跳转到 drop 标签释放数据包。
提取 UDP 头部和长度信息并检查 UDP 数据包的总长度是否超过了实际接收到的数据包长度。如果是则跳转到 short_packet 标签记录错误并释放数据包。
对于标准的 UDP 协议proto IPPROTO_UDP进一步检查 UDP 长度是否合法并调整数据包长度以匹配 UDP 头部中指定的长度。
校验和检查
调用 udp4_csum_init 函数初始化校验和计算如果需要的话。如果校验和初始化失败则跳转到 csum_error 标签。
快速路径处理已连接的套接字
检查数据包是否已经被绑定到一个套接字通过 skb_steal_sock。如果是则直接将该数据包传递给该套接字处理并返回结果。
多播处理
如果数据包是广播或多播的则调用 __udp4_lib_mcast_deliver 函数进行处理。
查找套接字
使用 __udp4_lib_lookup_skb 函数根据源端口、目的端口和 UDP 表来查找对应的套接字。如果找到了套接字则进行必要的校验和转换如果需要的话并将数据包传递给该套接字处理。
策略检查和防火墙处理
调用 xfrm4_policy_check 函数检查数据包是否符合安全策略。如果不符合则跳转到 drop 标签。
重置网络过滤netfilter标记。
无套接字情况处理
如果没有找到对应的套接字且校验和正确则记录统计信息无端口错误发送 ICMP 端口不可达消息并释放数据包。
错误处理
如果遇到短数据包或校验和错误则记录相应的调试信息和统计信息并释放数据包。
函数通过返回 0 来表示成功处理数据包无论是传递给套接字还是直接丢弃。在快速路径处理中如果 udp_queue_rcv_skb 函数返回一个大于 0 的值表示需要重新提交输入处理但通常是通过返回负数来表示协议错误这里通过返回 -ret 来处理这种情况。然而需要注意的是在标准的 Linux 内核实现中udp_queue_rcv_skb 函数通常不会返回大于 0 的值因此这部分代码可能是一个防御性编程实践或特定于某些定制内核的实现*/ __udp4_lib_rcv 函数是 Linux 内核中处理 IPv4 UDP 数据包接收的核心函数之一。它负责验证数据包的有效性、查找相关的套接字socket、并将数据包传递给正确的套接字进行处理。以下是对该函数主要逻辑的详细解释
验证数据包 首先函数检查是否有足够的空间来拉取 UDP 头部。如果没有则跳转到 drop 标签释放数据包。提取 UDP 头部和长度信息并检查 UDP 数据包的总长度是否超过了实际接收到的数据包长度。如果是则跳转到 short_packet 标签记录错误并释放数据包。对于标准的 UDP 协议proto IPPROTO_UDP进一步检查 UDP 长度是否合法并调整数据包长度以匹配 UDP 头部中指定的长度。校验和检查 调用 udp4_csum_init 函数初始化校验和计算如果需要的话。如果校验和初始化失败则跳转到 csum_error 标签。快速路径处理已连接的套接字 检查数据包是否已经被绑定到一个套接字通过 skb_steal_sock。如果是则直接将该数据包传递给该套接字处理并返回结果。多播处理 如果数据包是广播或多播的则调用 __udp4_lib_mcast_deliver 函数进行处理。查找套接字 使用 __udp4_lib_lookup_skb 函数根据源端口、目的端口和 UDP 表来查找对应的套接字。如果找到了套接字则进行必要的校验和转换如果需要的话并将数据包传递给该套接字处理。策略检查和防火墙处理 调用 xfrm4_policy_check 函数检查数据包是否符合安全策略。如果不符合则跳转到 drop 标签。重置网络过滤netfilter标记。无套接字情况处理 如果没有找到对应的套接字且校验和正确则记录统计信息无端口错误发送 ICMP 端口不可达消息并释放数据包。错误处理 如果遇到短数据包或校验和错误则记录相应的调试信息和统计信息并释放数据包。
函数通过返回 0 来表示成功处理数据包无论是传递给套接字还是直接丢弃。在快速路径处理中如果 udp_queue_rcv_skb 函数返回一个大于 0 的值表示需要重新提交输入处理但通常是通过返回负数来表示协议错误这里通过返回 -ret 来处理这种情况。然而需要注意的是在标准的 Linux 内核实现中udp_queue_rcv_skb 函数通常不会返回大于 0 的值因此这部分代码可能是一个防御性编程实践或特定于某些定制内核的实现 udp_queue_rcv_skb
int udp_queue_rcv_skb(struct sock *sk, struct sk_buff *skb)
{struct udp_sock *up udp_sk(sk);int rc;int is_udplite IS_UDPLITE(sk);/** Charge it to the socket, dropping if the queue is full.*/if (!xfrm4_policy_check(sk, XFRM_POLICY_IN, skb))goto drop;nf_reset(skb);if (static_key_false(udp_encap_needed) up-encap_type) {int (*encap_rcv)(struct sock *sk, struct sk_buff *skb);/** This is an encapsulation socket so pass the skb to* the sockets udp_encap_rcv() hook. Otherwise, just* fall through and pass this up the UDP socket.* up-encap_rcv() returns the following value:* 0 if skb was successfully passed to the encap* handler or was discarded by it.* 0 if skb should be passed on to UDP.* 0 if skb should be resubmitted as proto -N*//* if were overly short, let UDP handle it */encap_rcv ACCESS_ONCE(up-encap_rcv);if (encap_rcv) {int ret;/* Verify checksum before giving to encap */if (udp_lib_checksum_complete(skb))goto csum_error;ret encap_rcv(sk, skb);if (ret 0) {UDP_INC_STATS_BH(sock_net(sk),UDP_MIB_INDATAGRAMS,is_udplite);return -ret;}}/* FALLTHROUGH -- its a UDP Packet */}/** UDP-Lite specific tests, ignored on UDP sockets*/if ((is_udplite UDPLITE_RECV_CC) UDP_SKB_CB(skb)-partial_cov) {/** MIB statistics other than incrementing the error count are* disabled for the following two types of errors: these depend* on the application settings, not on the functioning of the* protocol stack as such.** RFC 3828 here recommends (sec 3.3): There should also be a* way ... to ... at least let the receiving application block* delivery of packets with coverage values less than a value* provided by the application.*/if (up-pcrlen 0) { /* full coverage was set */net_dbg_ratelimited(UDPLite: partial coverage %d while full coverage %d requested\n,UDP_SKB_CB(skb)-cscov, skb-len);goto drop;}/* The next case involves violating the min. coverage requested* by the receiver. This is subtle: if receiver wants x and x is* greater than the buffersize/MTU then receiver will complain* that it wants x while sender emits packets of smaller size y.* Therefore the above ...()-partial_cov statement is essential.*/if (UDP_SKB_CB(skb)-cscov up-pcrlen) {net_dbg_ratelimited(UDPLite: coverage %d too small, need min %d\n,UDP_SKB_CB(skb)-cscov, up-pcrlen);goto drop;}}if (rcu_access_pointer(sk-sk_filter) udp_lib_checksum_complete(skb))goto csum_error;if (sk_rcvqueues_full(sk, sk-sk_rcvbuf)) {UDP_INC_STATS_BH(sock_net(sk), UDP_MIB_RCVBUFERRORS,is_udplite);goto drop;}rc 0;ipv4_pktinfo_prepare(sk, skb);bh_lock_sock(sk);if (!sock_owned_by_user(sk))rc __udp_queue_rcv_skb(sk, skb);else if (sk_add_backlog(sk, skb, sk-sk_rcvbuf)) {bh_unlock_sock(sk);goto drop;}bh_unlock_sock(sk);return rc;csum_error:UDP_INC_STATS_BH(sock_net(sk), UDP_MIB_CSUMERRORS, is_udplite);
drop:UDP_INC_STATS_BH(sock_net(sk), UDP_MIB_INERRORS, is_udplite);atomic_inc(sk-sk_drops);kfree_skb(skb);return -1;
}
sock_owned_by_user 判断的是⽤户是不是正在这个 socket 上进⾏系统调⽤ socket 被占⽤。 如果没有那就可以直接放到 socket 的接收队列中。 如果有那就通过 sk_add_backlog 把数据包添加到 backlog 队列。 当⽤户释放的 socket 的时候内核会检查 backlog 队列如果有数据再移动到接收队列中。 sk_rcvqueues_full 接收队列如果满了的话将直接把包丢弃。接收队列⼤⼩受内核参数 net.core.rmem_max 和 net.core.rmem_default 影响 udp_queue_rcv_skb 函数是 Linux 内核中用于处理接收到的 UDP 数据包并将其排队到相应套接字接收队列的函数。以下是该函数主要逻辑的详细解释
安全策略检查 使用 xfrm4_policy_check 函数检查数据包是否符合安全策略。如果不符合则跳转到 drop 标签释放数据包。重置网络过滤标记 调用 nf_reset 函数重置数据包的网络过滤netfilter标记。封装处理 如果套接字被配置为需要封装通过 udp_encap_needed 静态键和 up-encap_type 字段检查则调用套接字的 encap_rcv 钩子函数处理数据包。如果 encap_rcv 钩子函数存在且返回非正值表示数据包应该继续传递给 UDP 层处理。如果返回 0 或负值则根据返回值进行相应的处理记录统计信息或重新提交数据包。UDP-Lite 特定处理 如果套接字是 UDP-Lite 类型的并且启用了部分校验和覆盖UDPLITE_RECV_CC且数据包的实际校验和覆盖长度小于套接字请求的校验和覆盖长度则记录错误并跳转到 drop 标签。校验和检查 如果套接字上安装了过滤器sk_filter并且数据包的校验和检查失败则跳转到 csum_error 标签。接收队列检查 检查套接字的接收队列是否已满。如果已满则记录错误并跳转到 drop 标签。数据包排队 如果套接字当前没有被用户进程锁定即不在阻塞接收操作中则调用 __udp_queue_rcv_skb 函数将数据包添加到套接字的接收队列中。如果套接字被用户进程锁定则尝试将数据包添加到套接字的backlog队列中。如果backlog队列也满了则跳转到 drop 标签。错误处理和统计 如果在处理过程中遇到校验和错误或需要丢弃数据包则跳转到 csum_error 或 drop 标签。在这些标签中会记录相应的统计信息如校验和错误、接收错误等并释放数据包。返回值 如果数据包成功排队到套接字接收队列中则返回 0。如果需要重新提交数据包这在实际的内核实现中不常见因为 __udp_queue_rcv_skb 通常不会返回这样的值则通过返回 -ret其中 ret 是 __udp_queue_rcv_skb 的返回值但这里有一个逻辑上的不匹配因为标准的 __udp_queue_rcv_skb 实现不会返回大于 0 的值来处理。然而请注意这里的注释和代码实现之间可能存在不一致实际的内核行为可能与此描述略有不同。
总的来说udp_queue_rcv_skb 函数负责接收到的 UDP 数据包的校验、封装处理如果需要的话、队列检查以及最终的排队操作。如果处理过程中出现任何错误数据包将被丢弃并记录相应的统计信息。 __udp_queue_rcv_skb
__udp_queue_rcv_skb()将skb添加到sk-sk_receive_queue队列上
static int __udp_queue_rcv_skb(struct sock *sk, struct sk_buff *skb)
{int rc;if (inet_sk(sk)-inet_daddr) {sock_rps_save_rxhash(sk, skb);sk_mark_napi_id(sk, skb);sk_incoming_cpu_update(sk);}rc sock_queue_rcv_skb(sk, skb);if (rc 0) {int is_udplite IS_UDPLITE(sk);/* Note that an ENOMEM error is charged twice */if (rc -ENOMEM)UDP_INC_STATS_BH(sock_net(sk), UDP_MIB_RCVBUFERRORS,is_udplite);UDP_INC_STATS_BH(sock_net(sk), UDP_MIB_INERRORS, is_udplite);kfree_skb(skb);trace_udp_fail_queue_rcv_skb(rc, sk);return -1;}return 0;} __udp_queue_rcv_skb 函数是 Linux 内核中用于将接收到的 UDP 数据包排队到套接字接收队列的辅助函数。这个函数是 UDP 数据包接收路径中的一部分负责处理数据包的一些前期准备工作并将其传递给 sock_queue_rcv_skb 函数进行实际的排队操作。
以下是该函数的主要逻辑解释 目的地址检查 如果套接字的目的地址inet_sk(sk)-inet_daddr非零表示这是一个已连接的 UDP 套接字即已经通过 connect 系统调用绑定了远程地址的套接字。对于这种情况函数会执行一些与接收处理相关的额外步骤包括 使用 sock_rps_save_rxhash 函数保存数据包的接收哈希值这有助于后续的数据包在接收处理时的负载均衡。调用 sk_mark_napi_id 函数将套接字与数据包的 NAPI ID 关联起来这同样有助于接收处理时的性能优化。调用 sk_incoming_cpu_update 函数更新套接字的入站 CPU 信息这有助于在多核处理器上优化数据包的处理。 排队操作 调用 sock_queue_rcv_skb 函数尝试将数据包添加到套接字的接收队列中。sock_queue_rcv_skb 函数会检查接收队列是否有足够的空间如果有则将数据包添加到队列中并返回 0否则返回错误码通常是 -ENOMEM表示内存不足。 错误处理 如果 sock_queue_rcv_skb 函数返回错误码__udp_queue_rcv_skb 函数会执行相应的错误处理 对于 -ENOMEM 错误会特别记录一个额外的统计信息UDP_MIB_RCVBUFERRORS以指示接收缓冲区错误。这是因为内存不足错误可能会被多次记录例如在尝试重新分配缓冲区时。无论错误码是什么都会记录一个接收错误统计信息UDP_MIB_INERRORS并释放数据包通过调用 kfree_skb。使用 trace_udp_fail_queue_rcv_skb 函数如果启用了跟踪记录错误跟踪信息。最后函数返回 -1 表示数据包无法被排队。
需要注意的是尽管函数名以双下划线开头__这通常表示该函数是内部的或私有的不应在模块外部直接调用但在 UDP 数据包的接收路径中这个函数是被 udp_queue_rcv_skb 或其他类似函数调用的。
此外随着 Linux 内核的发展网络子系统的实现细节可能会发生变化因此建议参考您正在使用的内核版本的源代码和文档 sock_queue_rcv_skb
--------------------------------------------------------------------------------------------------------------------------------
recvfrom 系统调⽤实现
用户如何收取报文?(Linux内核分析 - 网络[十二]UDP模块 - 收发_udp recvfrom 提取报文-CSDN博客)
用户可以调用sys_recvfrom()或sys_recv()来接收报文所不同的是sys_recvfrom()可能通过参数获得报文的来源地址而sys_recv()则不可以但对接收报文并没有影响。在用户调用recvfrom()或recv()接收报文前发给该socket的报文都会被添加到sk-sk_receive_queue上recvfrom()和recv()要做的就是从sk_receive_queue上取出报文拷贝到用户空间供用户使用
代码⾥调⽤的 recvfrom 是⼀个 glibc 的库函数该函数在执⾏后会将⽤户进⾏陷⼊到内核态进⼊到 Linux 实现的系统调⽤ sys_recvfrom socket 数据结构中的 const struct proto_ops 对应的是协议的⽅法集合。每个协议都会实现不同的⽅法集对于IPv4 Internet 协议族来说,每种协议都有对应的处理⽅法如下 对于 udp 来说是通过 inet_dgram_ops 来定义的其中注册了 inet_recvmsg ⽅法。 ---------------------------------------------------------------------------------------------------------------------------------
相关的文章
https://blog.csdn.net/qy532846454/article/details/6744252
https://blog.csdn.net/qy532846454/category_1385933.html
深入理解Linux网络技术内幕——IPv4 报文的接收转发与本地传递_ipv4协议报文交互-CSDN博客
linux协议栈 https://blog.csdn.net/qy532846454/category_1385933.html