Linux-4.20.8内核桥收包源码解析(四)----------netif_receive_skb

2024-01-23 20:58

本文主要是介绍Linux-4.20.8内核桥收包源码解析(四)----------netif_receive_skb,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

作者:lwyang?
内核版本:Linux-4.20.8

netif_receive_skb实现了报文递交到上层协议模块,具体递交方法为由指针func指向的函数确定。首先会遍历ptype_all链表,输入一份报文到ptype_all链表的输入接口,然后通过桥转发报文,若转发成功则无需输入到本地,否则遍历ptype_base链表,根据接受报文注册的协议类型调用对应的报文接受例程。

比如IP协议使用ip_packet_type变量来注册,把func设置为ip_rcv函数,如果数据包的上层类型是ETH_P_IP,pt_prev->func必然调用ip_rcv函数,把数据包递交到IP层协议模块

前文提到网卡驱动最终会调用__netif_receive_skb 进入上层协议处理

static int __netif_receive_skb(struct sk_buff *skb)
{int ret;if (sk_memalloc_socks() && skb_pfmemalloc(skb)) {...} elseret = __netif_receive_skb_one_core(skb, false);return ret;
}
static int __netif_receive_skb_one_core(struct sk_buff *skb, bool pfmemalloc)
{...//这里的pt_prev(packet_type) 为传出参数ret = __netif_receive_skb_core(skb, pfmemalloc, &pt_prev);if (pt_prev)//这里不是调用deliver_skb,而是直接调用pt_prev->func,减少了一次增加skb->users,从而减少了一次skb的释放ret = pt_prev->func(skb, skb->dev, pt_prev, orig_dev);return ret;
}

接下来才是真正进行处理的函数__netif_receive_skb_core

static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc,struct packet_type **ppt_prev)
{struct packet_type *ptype, *pt_prev;rx_handler_func_t *rx_handler;struct net_device *orig_dev;bool deliver_exact = false;int ret = NET_RX_DROP;__be16 type;//记录收包时间,netdev_tstamp_prequeue为0,表示可能有包延迟net_timestamp_check(!netdev_tstamp_prequeue, skb);//记录原始收包网络设备orig_dev = skb->dev;//此时data指针是指向IP层头部的(没有vlan的情况下)//设置network_header指针 skb->network_header = skb->data - skb->head;skb_reset_network_header(skb);if (!skb_transport_header_was_set(skb))//设置transport_header指针,这里也是指向IP层skb_reset_transport_header(skb);//设置mac_len的值为以太网报文头部长度,一般为mac_len = 14// skb->mac_len = skb->network_header - skb->mac_header;skb_reset_mac_len(skb);//指向前一个packet_type 的指针为NULL,设置此指针的目的是为了提高效率//这样相当于最后一个pt_prev 指向的函数未被执行,最后一次向上层传递时,不需要在inc引用,回调中会free,这样相当于少调用了一次freept_prev = NULL;another_round://设置skb的skb_iif,记录数据包收包网络设备的索引号skb->skb_iif = skb->dev->ifindex;//将处理此数据包cpu的softnet_data结构统计已处理数据的字段processed加1__this_cpu_inc(softnet_data.processed);//如果报文为带vlan报文,在eth_type_trans中设置的skb->protocol 为 ETH_P_8021Q 或 ETH_P_8021ADif (skb->protocol == cpu_to_be16(ETH_P_8021Q) ||skb->protocol == cpu_to_be16(ETH_P_8021AD)) {//剥离vlan标签 // // |---2 bytes---|---|-|------------| 一共4字节vlan信息// 前两个字节为标签协议标识TPID(Tag Protocol Identifier),值为0x8100,后// 后两个字节为标签控制信息TCI(Tag Control Information),前三位Priority表明帧的优先级,接下来的一位cfi用于以太网与FDDI和令牌环网交换数据时的帧格式,最后12位VLAN ID,一共4096个//首先判断skb = skb_share_check(skb, GFP_ATOMIC);判断skb是否共享(skb->users!=1 ?),如果共享则克隆一份,因为后续会修改skb的network_header,transport_header,vlan等信息//并将原skb的引用计数-1(skb->users-1),如果不克隆则会影响共享此skb的其他函数,如果此skb为不共享,则直接返回此skb//然后__vlan_hwaccel_put_tag(skb, skb->protocol, vlan_tci);设置skb->vlan_proto,skb->vlan_tci = VLAN_TAG_PRESENT | vlan_tci;//记录vlan协议到vlan_proto,以及vlan控制信息到vlan_tci 并将VLAN_TAG_PRESENT位置为1//接下来vlan_set_encap_proto  设置skb->protocol 为真正的三层协议//skb_reorder_vlan_header 将vlan信息从数据包中剥离,具体做法为从2层头部到vlan域的信息整体(目的mac+源mac)向后移4字节(vlan信息长度)//最后就是重置skb的network_header,transport_header,mac_len信息skb = skb_vlan_untag(skb);if (unlikely(!skb))goto out;}//如果tc_skip_classify为1,则跳过ETH_P_ALL 的协议处理,跳过流分类处理if (skb_skip_tc_classify(skb))goto skip_classify;//如果pfmemalloc 为真,则跳过ETH_P_ALL 的协议处理if (pfmemalloc)goto skip_taps;//如抓包程序未指定设备,遍历ptype_all链表,输入一份报文到ptype_all链表中的协议族,处理ETH_P_ALL类型的数据包list_for_each_entry_rcu(ptype, &ptype_all, list) {if (pt_prev)ret = deliver_skb(skb, pt_prev, orig_dev);pt_prev = ptype;}//遍历skb收包网络设备的ptype_all链表,处理与具体dev绑定的ETH_P_ALL协议处理例程list_for_each_entry_rcu(ptype, &skb->dev->ptype_all, list) {if (pt_prev)ret = deliver_skb(skb, pt_prev, orig_dev);pt_prev = ptype;}skip_taps:
#ifdef CONFIG_NET_INGRESS...//流分类处理
#endif//skb->tc_redirected置为0skb_reset_tc(skb);
skip_classify:if (pfmemalloc && !skb_pfmemalloc_protocol(skb))goto drop;//判断是否为vlan报文,并且vlan_tci 的VLAN_TAG_PRESENT位为1(skb_vlan_untag中进行过设置)if (skb_vlan_tag_present(skb)) {//若pt_prev 不为空,则表示进行过ETH_P_ALL 协议类型处理,执行刚刚链表的最后一个协议处理函数,并将pt_prev 置为NULLif (pt_prev) {ret = deliver_skb(skb, pt_prev, orig_dev);pt_prev = NULL;}//vlan处理函数// skb_share_check判断skb是否共享,共享则克隆// skb->dev = vlan_dev; 设置skb的dev为vlan网络设备,一般如eth0.10(vlan为10的设备)// 若skb->dev和vlan_dev的mac地址不同,则还需要判断skb目的mac地址是否等于vlan设备的目的mac,若等于则设置skb->pkt_type = PACKET_HOST// skb->priority 根据vlan_tci中的优先级信息设置skb的优先级// skb->vlan_tci = 0; 将vlan控制信息置为0,不需要此信息了// 更新vlan设备的收包统计信息if (vlan_do_receive(&skb))// 完成vlan处理后,改变了skb->dev,跳转到another_round重新执行// 此时有一个问题:是否会重复执行ETH_P_ALL 协议处理函数,答案:不会。因为一般会判断orig_dev和skb->dev一否一致,此时已经不一致了goto another_round;else if (unlikely(!skb))goto out;}//若rx_handler 不为NULL,则进入桥处理,rx_handler 在br_add_if中注册的这个函数rx_handler = rcu_dereference(skb->dev->rx_handler);if (rx_handler) {//若pt_prev 不为空,则表示进行过ETH_P_ALL 协议类型处理,执行刚刚链表的最后一个协议处理函数,并将pt_prev 置为NULLif (pt_prev) {ret = deliver_skb(skb, pt_prev, orig_dev);pt_prev = NULL;}//执行rx_handler 函数,为br_handle_frame 函数,在br_add_if中注册的这个函数// 下面根据桥处理的返回值进行下一步处理switch (rx_handler(&skb)) {//skb was consumed by rx_handler, do not process it further.// 桥已经处理该数据包,该数据包会以其他的方式传送case RX_HANDLER_CONSUMED:ret = NET_RX_SUCCESS;goto out;//Do another round in receive path. This is indicated in case skb->dev was changed by rx_handler//桥改变的数据包的skb->dev,需要another_round进行再一次的处理case RX_HANDLER_ANOTHER:goto another_round;//Force exact delivery, no wildcard//数据包只会传送到注册为具体网络设备(ptype->dev == skb->dev)的协议处理例程case RX_HANDLER_EXACT:deliver_exact = true;//Do nothing, pass the skb as if no rx_handler was called//正常传送case RX_HANDLER_PASS:break;default:BUG();}}...//记录IP层的协议类型type = skb->protocol;/* deliver only exact match when indicated *///若未设置精确传送,向未指定设备的协议处理例程传送一份数据if (likely(!deliver_exact)) {deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type,&ptype_base[ntohs(type) &PTYPE_HASH_MASK]);}//交给与原设备的绑定的具体处理协议例程处理deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type,&orig_dev->ptype_specific);//如果skb的当前设备与原设备不同(进行过vlan处理或桥处理),则交给绑定当前设备的具体处理协议函数处理if (unlikely(skb->dev != orig_dev)) {deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type,&skb->dev->ptype_specific);}//如果pt_prev 不为空,表明上面链表处理过程中还留下最后一个协议处理函数还没有执行//此时就将这个协议处理函数传出到外层函数__netif_receive_skb_one_core调用pt_prev->func进行处理//外层函数处理时就不需要deliver_skb来增加skb->users,减少了一次skb的释放if (pt_prev) {if (unlikely(skb_orphan_frags_rx(skb, GFP_ATOMIC)))goto drop;*ppt_prev = pt_prev;} 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;
}

【补充一个小细节】
带有vlan信息时,在skb_vlan_untag(skb);函数中,是怎么获取到vlan信息的?

//vlan头部结构体
struct vlan_hdr {//vlan 控制信息(2字节)__be16 h_vlan_TCI;//报文实际协议类型(2字节)__be16 h_vlan_encapsulated_proto;
};

eth_type_trans函数中,将data指针下移14字节(skb_pull_inline(skb, ETH_HLEN);),如果此时报文带vlan,vlan信息4个字节,前两个字节为标签协议标识TPID(Tag Protocol Identifier),值为0x8100,后两个字节为标签控制信息TCI(Tag Control Information),那么此时data就指向的是TCI控制信息,因为以太网源和目的mac地址12字节,加上vlan标签协议标识2字节正好14字节
在这里插入图片描述
vhdr = (struct vlan_hdr *)skb->data; 这个函数将data后的四字节数据赋给vlan_hdr,那么h_vlan_TCI就为vlan标签控制信息,h_vlan_encapsulated_proto 即为真正的以太网协议类型。后续函数vlan_set_encap_proto 会设置skb->protocol = vhdr->h_vlan_encapsulated_proto

协议处理例程的注册
packet_type结构作为网络层的输入接口,系统支持多种协议族,因此每个协议族都会实现一个报文处理例程,此结构的功能时在链路层和网络层之间起到了桥梁的作用,在以太网上,以太网帧到达主机后,会根据协议族的报文类型调用相应的网络层接受处理函数

为向上层协议递交设备驱动收到的数据包,内核提供了表结构ptype_baseptype_all,它们都是struct packet_type类型,ptype_base负责把不同类型(协议)的数据包递交给对应的上层协议模块,ptype_all表不区分包的协议类型,负责把所有数据包递交给某个注册的上层模块
在这里插入图片描述

struct packet_type {//网络层数据包协议类型__be16			type;	/* This is really htons(ether_type). */bool			ignore_outgoing;//接受从指定网络设备输入的数据包,若为NULL,则表示接受全部网络设备的数据包struct net_device	*dev;	/* NULL is wildcarded here	     *///协议入口处理函数int			(*func) (struct sk_buff *,struct net_device *,struct packet_type *,struct net_device *);void			(*list_func) (struct list_head *,struct packet_type *,struct net_device *);bool			(*id_match)(struct packet_type *ptype,struct sock *sk);//存储各协议族私有数据void			*af_packet_priv;//链接不同协议族报文接受例程的指针struct list_head	list;
};

在协议初始化时,内核调用dev_add_pack函数来注册某类数据包的递交方法

void dev_add_pack(struct packet_type *pt)
{struct list_head *head = ptype_head(pt);spin_lock(&ptype_lock);//将此packet_type 添加到对应的协议处理链表中list_add_rcu(&pt->list, head);spin_unlock(&ptype_lock);
}static inline struct list_head *ptype_head(const struct packet_type *pt)
{//若packet_type的协议类型为ETH_P_ALL并且dev不为空,则将此packet_type添加到对应dev的ptype_all链表中,否则加到全局ptype_all链表//若packet_type的协议类型不为ETH_P_ALL并且dev不为空,则将此packet_type添加到对应dev的ptype_specific链表,否则就加到全局ptype_base链表中if (pt->type == htons(ETH_P_ALL))return pt->dev ? &pt->dev->ptype_all : &ptype_all;elsereturn pt->dev ? &pt->dev->ptype_specific :&ptype_base[ntohs(pt->type) & PTYPE_HASH_MASK];
}

例如IP协议处理例程的注册

static int __init inet_init(void)
{...dev_add_pack(&ip_packet_type);...
}//ip_packet_type变量为上层IP数据包提供递交方法
static struct packet_type ip_packet_type __read_mostly = {//定义其注册的协议类型为ETH_P_IP.type = cpu_to_be16(ETH_P_IP),//确定ETH_P_IP的协议处理函数为ip_rcv.func = ip_rcv,.list_func = ip_list_rcv,
};

桥处理函数的注册
br_handle_frame这个函数的初始注册地点是在桥添加接口的时候,注册在桥某一个接口上

int br_add_if(struct net_bridge *br, struct net_device *dev, struct netlink_ext_ack *extack)
{struct net_bridge_port *p;int err = 0;unsigned br_hr, dev_hr;bool changed_addr;...//创建一个新的桥接口 p->br = br; p->dev = dev;p = new_nbp(br, dev); ...//register receive handler,将br_handle_frame 函数注册到此桥接口上err = netdev_rx_handler_register(dev, br_handle_frame, p);...
}int netdev_rx_handler_register(struct net_device *dev,rx_handler_func_t *rx_handler,void *rx_handler_data)
{...//将net_bridge_port 赋给dev网络设备的rx_handler_datarcu_assign_pointer(dev->rx_handler_data, rx_handler_data);//将br_handle_frame 赋给dev网络设备的rx_handlerrcu_assign_pointer(dev->rx_handler, rx_handler);...
}

接下来再看br_handle_frame 桥处理流程, 见下节…


以上仅代表个人理解,如若觉得有理解不当的地方还请不吝赐教?

这篇关于Linux-4.20.8内核桥收包源码解析(四)----------netif_receive_skb的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/637501

相关文章

线上Java OOM问题定位与解决方案超详细解析

《线上JavaOOM问题定位与解决方案超详细解析》OOM是JVM抛出的错误,表示内存分配失败,:本文主要介绍线上JavaOOM问题定位与解决方案的相关资料,文中通过代码介绍的非常详细,需要的朋... 目录一、OOM问题核心认知1.1 OOM定义与技术定位1.2 OOM常见类型及技术特征二、OOM问题定位工具

防止Linux rm命令误操作的多场景防护方案与实践

《防止Linuxrm命令误操作的多场景防护方案与实践》在Linux系统中,rm命令是删除文件和目录的高效工具,但一旦误操作,如执行rm-rf/或rm-rf/*,极易导致系统数据灾难,本文针对不同场景... 目录引言理解 rm 命令及误操作风险rm 命令基础常见误操作案例防护方案使用 rm编程 别名及安全删除

Linux下MySQL数据库定时备份脚本与Crontab配置教学

《Linux下MySQL数据库定时备份脚本与Crontab配置教学》在生产环境中,数据库是核心资产之一,定期备份数据库可以有效防止意外数据丢失,本文将分享一份MySQL定时备份脚本,并讲解如何通过cr... 目录备份脚本详解脚本功能说明授权与可执行权限使用 Crontab 定时执行编辑 Crontab添加定

使用docker搭建嵌入式Linux开发环境

《使用docker搭建嵌入式Linux开发环境》本文主要介绍了使用docker搭建嵌入式Linux开发环境,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面... 目录1、前言2、安装docker3、编写容器管理脚本4、创建容器1、前言在日常开发全志、rk等不同

深度解析Python中递归下降解析器的原理与实现

《深度解析Python中递归下降解析器的原理与实现》在编译器设计、配置文件处理和数据转换领域,递归下降解析器是最常用且最直观的解析技术,本文将详细介绍递归下降解析器的原理与实现,感兴趣的小伙伴可以跟随... 目录引言:解析器的核心价值一、递归下降解析器基础1.1 核心概念解析1.2 基本架构二、简单算术表达

深度解析Java @Serial 注解及常见错误案例

《深度解析Java@Serial注解及常见错误案例》Java14引入@Serial注解,用于编译时校验序列化成员,替代传统方式解决运行时错误,适用于Serializable类的方法/字段,需注意签... 目录Java @Serial 注解深度解析1. 注解本质2. 核心作用(1) 主要用途(2) 适用位置3

Java MCP 的鉴权深度解析

《JavaMCP的鉴权深度解析》文章介绍JavaMCP鉴权的实现方式,指出客户端可通过queryString、header或env传递鉴权信息,服务器端支持工具单独鉴权、过滤器集中鉴权及启动时鉴权... 目录一、MCP Client 侧(负责传递,比较简单)(1)常见的 mcpServers json 配置

从原理到实战解析Java Stream 的并行流性能优化

《从原理到实战解析JavaStream的并行流性能优化》本文给大家介绍JavaStream的并行流性能优化:从原理到实战的全攻略,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的... 目录一、并行流的核心原理与适用场景二、性能优化的核心策略1. 合理设置并行度:打破默认阈值2. 避免装箱

Maven中生命周期深度解析与实战指南

《Maven中生命周期深度解析与实战指南》这篇文章主要为大家详细介绍了Maven生命周期实战指南,包含核心概念、阶段详解、SpringBoot特化场景及企业级实践建议,希望对大家有一定的帮助... 目录一、Maven 生命周期哲学二、default生命周期核心阶段详解(高频使用)三、clean生命周期核心阶

linux系统上安装JDK8全过程

《linux系统上安装JDK8全过程》文章介绍安装JDK的必要性及Linux下JDK8的安装步骤,包括卸载旧版本、下载解压、配置环境变量等,强调开发需JDK,运行可选JRE,现JDK已集成JRE... 目录为什么要安装jdk?1.查看linux系统是否有自带的jdk:2.下载jdk压缩包2.解压3.配置环境