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