linux 网络子系统
__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结构体时,需要确保与数据包接收和处理相关的同步问题得到妥善处理。这通常涉及到使用 RCU(Read-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. Doesn't have a bogus length*/if (iph->ihl < 5 || iph->version != 4)//ip头长度至少为20字节(ihl>=5,后面计算头长度会乘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[idx&0xFF].o_packets++;//更新接收数据包数量st[idx&0xFF].o_bytes += skb->len;//更新接收数据包的长度st[(idx>>16)&0xFF].i_packets++;st[(idx>>16)&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[idx&0xFF].o_packets++;//更新接收数据包数量st[idx&0xFF].o_bytes += skb->len;//更新接收数据包的长度st[(idx>>16)&0xFF].i_packets++;st[(idx>>16)&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 isn't 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* don't 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 头部中指定的长度。
- 首先,函数检查是否有足够的空间来拉取 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 socket's 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 we're 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 -- it's 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标签。
- 如果套接字是 UDP-Lite 类型的,并且启用了部分校验和覆盖(
- 校验和检查:
- 如果套接字上安装了过滤器(
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
相关文章:
linux 网络子系统
__netif_receive_skb_core 是 Linux 内核网络子系统中一个非常重要的函数,它负责将网络设备驱动层接收到的数据包传递到上层协议栈进行处理。以下是对该函数的一些关键点的详细解析: 一、函数作用 __netif_receive_skb_core 函数是处理接收到的网络数据…...
JVM:垃圾回收器演进
文章目录 一、演进二、Shenandoah三、ZGC 一、演进 二、Shenandoah Shenandoah是由Red Hat开发的一款低延迟的垃圾收集器,Shenandoah并发执行大部分GC工作,包括并发的整理,堆大小对STW的时间基本没有影响。 三、ZGC ZGC是一种可扩展的低延…...
全新微软语音合成网页版源码,短视频影视解说配音网页版系统-仿真人语音
源码介绍 最新微软语音合成网页版源码,可以用来给影视解说和短视频配音。它是TTS文本转语言,API接口和PHP源码。 这个微软语音合成接口的源码,超级简单,就几个文件搞定。用的是官方的API,试过了,合成速度…...
大语言模型-对比学习-Contrastive Learning
一、对比学习概念 对比学习是一种特殊的无监督学习方法。 旨在通过拉近相关样本的距离并且推远不相关样本的距离,来学习数据表示。 通常使用一种高自由度、自定义的规则来生成正负样本。在模型预训练中有着广泛的应用。 二、对比学习小案例 对比学习主要分为三个…...
C++ 封装的用法
C(七)封装 封装,可以达到,对外提供接口,屏蔽数据,对内开放数据。 权限控制 struct 中所有行为和属性都是 public 的(默认),此举也是为了 C兼容 C 语言, 因为 C 语言中没有权限的概念。 C中的 class 可以…...
【C++11:异常】
目录 抛异常标准书写格式 抛异常如何执行? 指定抛出异常类型: noexcept 关键字:throw 抛异常标准书写格式 抛异常如何执行? 当212行的异常被抛出,程序会重新返回函数func中,在函数中去寻找catch 语句的…...
Dify中HTTP请求节点的常见操作
HTTP节点包括API请求类型(GET、POST、HEAD、PATCH、PUT、DELETE),鉴权类型(无、API-Key基础、API-Key Bearer、API-Key自定义),HEADERS键值设置,PARAMS键值设置,BODY(non…...
《大语言模型(赵鑫)》知识框图
...
【Android】性能实践—编码优化与布局优化学习笔记
编码优化 使用场景 如果需要拼接字符串,优先使用StringBuffer和StringBuilder进行凭借,他们的性能优于直接用加号进行拼接,因为使用加号连接符会创建多余的对象一般情况下使用基本数据类来代替封装数据类型(比如int优于Integer&…...
如何合规与安全地利用专业爬虫工具,构建企业数据竞争优势
摘要: 本文深入探讨了在当今大数据时代,企业如何通过合规且安全的方式运用专业爬虫工具,有效收集并分析海量信息,进而转化为企业独有的数据优势。我们不仅会介绍最佳实践,还会讨论关键技术和策略,帮助企业…...
自动驾驶三维车道线检测系列—OpenLane数据集介绍
文章目录 1. 背景介绍2. OpenLane数据集详细描述2.1 数据集特点2.2 坐标系定义 3. 使用方法4. 结论 1. 背景介绍 自动驾驶技术的发展日新月异,而3D车道感知是其核心之一。本文将深入介绍OpenLane数据集——迄今为止规模最大、最接近真实世界的3D车道数据集。我们将…...
CMakeList学习笔记
设置项目:project project(planning VERSION 1.0.0 LANGUAGES CXX) # 项目的名字 版本 1.1.0 编程语言 CXX 设置包含目录:include_directories、targer_include_directories 设置编译类型:add_executable、add_library add_executable(demo d…...
将git默认的编辑器设置为vin
git默认编辑器现状 如下,很多linux发行版,未加修改的情况下,git的默认编辑器使用起来不太方便 Signed-off-by: root <rootxxx.COM># Please enter the commit message for your changes. Lines starting # with # will be ignored, a…...
ros2_control 6 自由度机械臂
系列文章目录 前言 ros2_control 是一个实时控制框架,专为普通机器人应用而设计。标准的 c 接口用于与硬件交互和查询用户定义的控制器命令。这些接口增强了代码的模块化和与机器人无关的设计。具体的应用细节,例如使用什么控制器、机器人有多少个关节以…...
Python 在自动化中的实际应用:用 Python 简化繁琐任务
文章目录 1、概述2、自动化文件和目录管理3.数据处理与分析4.网页爬虫5. 系统管理6。定时任务7.结语 1、概述 这篇文章将深入探讨Python在自动化中的实际应用,帮助您用Python简化繁琐任务。 我们将从多个方面入手,展示如何利用Python进行文件管理、数据…...
解释 Spring 框架的核心模块(如 IoC 容器、AOP )及其工作原理。描述如何使用 Spring Boot 快速搭建一个 RESTful Web服务?
Spring框架是一个广泛使用的Java企业级应用程序开发框架,它提供了一系列的模块来帮助开发者构建健壮、可测试、可维护的应用程序。 其中,最核心的模块包括IoC容器和AOP(Aspect Oriented Programming,面向切面编程)。 …...
数据分析详解
一、数据分析教程 1. 入门教程 在线课程:如Coursera、Udemy、网易云课堂等平台提供了大量数据分析的入门课程,涵盖统计学基础、Python/R语言编程、数据可视化等内容。书籍推荐:《Python数据分析实战》、《R语言实战》等书籍是数据分析入门的…...
SpringCloud之@FeignClient()注解的使用方式
FeignClient介绍 FeignClient 是 Spring Cloud 中用于声明一个 Feign 客户端的注解。由于SpringCloud采用分布式微服务架构,难免在各个子模块下存在模块方法互相调用的情况。比如订单服务要调用库存服务的方法,FeignClient()注解就是为了解决这个问题的…...
20.rabbitmq插件实现延迟队列
问题 前面谈到基于死信的延迟队列,存在的问题:如果第一个消息延时时间很长,而第二个消息延时时间很短,第二个消息并不会优先得到执行。 下载插件 地址:https://github.com/rabbitmq/rabbitmq-delayed-message-excha…...
TS如何处理js模块的类型?
现在很多插件都直接用ts开发了,本身包含了类型定义常见的第三方插件,都有’types/xxx’包,安装即可使用其他的,可通过declare module定义类型 比如: // someModule.js export function greet(name) {return Hello, $…...
MPNet:旋转机械轻量化故障诊断模型详解python代码复现
目录 一、问题背景与挑战 二、MPNet核心架构 2.1 多分支特征融合模块(MBFM) 2.2 残差注意力金字塔模块(RAPM) 2.2.1 空间金字塔注意力(SPA) 2.2.2 金字塔残差块(PRBlock) 2.3 分类器设计 三、关键技术突破 3.1 多尺度特征融合 3.2 轻量化设计策略 3.3 抗噪声…...
HTML 语义化
目录 HTML 语义化HTML5 新特性HTML 语义化的好处语义化标签的使用场景最佳实践 HTML 语义化 HTML5 新特性 标准答案: 语义化标签: <header>:页头<nav>:导航<main>:主要内容<article>&#x…...
相机Camera日志实例分析之二:相机Camx【专业模式开启直方图拍照】单帧流程日志详解
【关注我,后续持续新增专题博文,谢谢!!!】 上一篇我们讲了: 这一篇我们开始讲: 目录 一、场景操作步骤 二、日志基础关键字分级如下 三、场景日志如下: 一、场景操作步骤 操作步…...
12.找到字符串中所有字母异位词
🧠 题目解析 题目描述: 给定两个字符串 s 和 p,找出 s 中所有 p 的字母异位词的起始索引。 返回的答案以数组形式表示。 字母异位词定义: 若两个字符串包含的字符种类和出现次数完全相同,顺序无所谓,则互为…...
Java毕业设计:WML信息查询与后端信息发布系统开发
JAVAWML信息查询与后端信息发布系统实现 一、系统概述 本系统基于Java和WML(无线标记语言)技术开发,实现了移动设备上的信息查询与后端信息发布功能。系统采用B/S架构,服务器端使用Java Servlet处理请求,数据库采用MySQL存储信息࿰…...
排序算法总结(C++)
目录 一、稳定性二、排序算法选择、冒泡、插入排序归并排序随机快速排序堆排序基数排序计数排序 三、总结 一、稳定性 排序算法的稳定性是指:同样大小的样本 **(同样大小的数据)**在排序之后不会改变原始的相对次序。 稳定性对基础类型对象…...
JavaScript 数据类型详解
JavaScript 数据类型详解 JavaScript 数据类型分为 原始类型(Primitive) 和 对象类型(Object) 两大类,共 8 种(ES11): 一、原始类型(7种) 1. undefined 定…...
Webpack性能优化:构建速度与体积优化策略
一、构建速度优化 1、升级Webpack和Node.js 优化效果:Webpack 4比Webpack 3构建时间降低60%-98%。原因: V8引擎优化(for of替代forEach、Map/Set替代Object)。默认使用更快的md4哈希算法。AST直接从Loa…...
深入浅出WebGL:在浏览器中解锁3D世界的魔法钥匙
WebGL:在浏览器中解锁3D世界的魔法钥匙 引言:网页的边界正在消失 在数字化浪潮的推动下,网页早已不再是静态信息的展示窗口。如今,我们可以在浏览器中体验逼真的3D游戏、交互式数据可视化、虚拟实验室,甚至沉浸式的V…...
Java数组Arrays操作全攻略
Arrays类的概述 Java中的Arrays类位于java.util包中,提供了一系列静态方法用于操作数组(如排序、搜索、填充、比较等)。这些方法适用于基本类型数组和对象数组。 常用成员方法及代码示例 排序(sort) 对数组进行升序…...
