linux内核协议栈 协议栈收包入口 netif_receive_skb

2023-11-01 05:20

本文主要是介绍linux内核协议栈 协议栈收包入口 netif_receive_skb,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

1 协议栈入口 __netif_receive_skb_core()

2 vlan 操作 

2.1 vlan报文信息 struct vlan_hdr

2.2 vlan 剥离 vlan_untag()

2.3 vlan 添加 eth_type_trans()

3 三层协议处理钩子注册

3.1 各种三层协议处理函数注册过程

3.2 桥处理函数注册


网络收包流程从网卡驱动开始,一直往上,涉及NAPI、GRO、RPS等特性,通常是经过硬件中断后在经由软中断处理,在内核软中断的最后一步就是调用 netif_receive_skb 开始报文协议栈处理。

1 协议栈入口 __netif_receive_skb_core()

高版本内核(本文基于 linux-3.10.0)都会对 netif_receive_skb 函数进行了封装,一般最后都会调用 __netif_receive_skb_core 函数,主要逻辑如下:

  1. vlan报文的处理,主要是循环把vlan头剥掉,如果qinq场景,两个vlan都会被剥掉;
  2. 交给rx_handler处理,例如OVS、linux bridge 等;
  3. ptype_all处理,例如抓包程序、raw socket等;
  4. ptype_base处理,交给协议栈处理,例如ip、arp、rarp 等;
static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc)
{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;//记录收包时间,netdev_tstamp_prequeue为0,表示可能有包延迟net_timestamp_check(!netdev_tstamp_prequeue, skb);trace_netif_receive_skb(skb);/* if we've gotten here through NAPI, check netpoll */if (netpoll_receive_skb(skb))goto out;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))skb_reset_transport_header(skb);//设置transport_header指针,这里也是指向IP层/*设置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,这样相当于少调用了一次free*/pt_prev = NULL;rcu_read_lock();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)) {skb = vlan_untag(skb);//剥离vlan标签 if (unlikely(!skb))goto unlock;}#ifdef CONFIG_NET_CLS_ACTif (skb->tc_verd & TC_NCLS) {skb->tc_verd = CLR_TC_NCLS(skb->tc_verd);goto ncls;}
#endifif (pfmemalloc)goto skip_taps;//如抓包程序未指定设备,遍历ptype_all链表,输入一份报文到ptype_all链表中的协议族,处理ETH_P_ALL类型的数据包list_for_each_entry_rcu(ptype, &ptype_all, list) {if (!ptype->dev || ptype->dev == skb->dev) {if (pt_prev)ret = deliver_skb(skb, pt_prev, orig_dev);pt_prev = ptype;}}skip_taps:
#ifdef CONFIG_NET_CLS_ACTskb = handle_ing(skb, &pt_prev, &ret, orig_dev);if (!skb)goto unlock;
ncls:
#endifif (pfmemalloc && !skb_pfmemalloc_protocol(skb))goto drop;//判断是否为vlan报文,并且vlan_tci 的VLAN_TAG_PRESENT位为1(skb_vlan_untag中进行过设置)if (vlan_tx_tag_present(skb)) {/*若pt_prev 不为空,则表示进行过ETH_P_ALL 协议类型处理,*执行刚刚链表的最后一个协议处理函数,并将pt_prev 置为NULL*/if (pt_prev) {ret = deliver_skb(skb, pt_prev, orig_dev);pt_prev = NULL;}if (vlan_do_receive(&skb))//vlan处理函数/*完成vlan处理后,改变了skb->dev,跳转到another_round重新执行*此时有一个问题:是否会重复执行ETH_P_ALL 协议处理函数,*答案:不会。因为一般会判断orig_dev和skb->dev一否一致,此时已经不一致了*/ goto another_round;else if (unlikely(!skb))goto unlock;}//若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 置为NULL*/if (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)) {//桥已经处理该数据包,该数据包会以其他的方式传送case RX_HANDLER_CONSUMED:ret = NET_RX_SUCCESS;goto unlock;//桥改变的数据包的skb->dev,需要another_round进行再一次的处理	case RX_HANDLER_ANOTHER:goto another_round;//数据包只会传送到注册为具体网络设备(ptype->dev == skb->dev)的协议处理例程	case RX_HANDLER_EXACT:deliver_exact = true;//正常传送	case RX_HANDLER_PASS:break;default:BUG();}}if (vlan_tx_nonzero_tag_present(skb))skb->pkt_type = PACKET_OTHERHOST;/* deliver only exact match when indicated */null_or_dev = deliver_exact ? skb->dev : NULL;//记录三层的协议类型(ip、arp等)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;}}/*如果pt_prev 不为空,表明上面链表处理过程中还留下最后一个协议处理函数还没有执行*此时就将这个协议处理函数传出到外层函数__netif_receive_skb_one_core调用pt_prev->func进行处理*外层函数处理时就不需要deliver_skb来增加skb->users,减少了一次skb的释放*/if (pt_prev) {if (unlikely(skb_orphan_frags(skb, GFP_ATOMIC)))goto drop;elseret = pt_prev->func(skb, skb->dev, pt_prev, orig_dev);} else {
drop:atomic_long_inc(&skb->dev->rx_dropped);kfree_skb(skb);/* Jamal, now you will not able to escape explaining* me how you were going to use this. :-)*/ret = NET_RX_DROP;}unlock:rcu_read_unlock();
out:return ret;
}

2 vlan 操作 

2.1 vlan报文信息 struct vlan_hdr

/** 	struct vlan_hdr - vlan header* 	@h_vlan_TCI: priority and VLAN ID*	@h_vlan_encapsulated_proto: packet type ID or len*/
struct vlan_hdr {__be16	h_vlan_TCI;	//vlan 控制信息(2字节)__be16	h_vlan_encapsulated_proto;//报文实际协议类型(2字节)
};

前两个字节为标签协议标识TPID(Tag Protocol Identifier),值为0x8100,后
后两个字节为标签控制信息TCI(Tag Control Information),前三位Priority表明帧的优先级,接下来的一位cfi用于以太网与FDDI和令牌环网交换数据时的帧格式,最后12位VLAN ID,一共4096个

报文信息如下:

2.2 vlan 剥离 vlan_untag()

  1. 首先,判断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
  2. 次之,__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
  3. 再次,vlan_set_encap_proto  设置skb->protocol 为真正的三层协议 skb_reorder_vlan_header 将vlan信息从数据包中剥离,具体做法为从2层头部到vlan域的信息整体(目的mac+源mac)向后移4字节(vlan信息长度)
  4. 最后,重置skb的 network_header,transport_header,mac_len信息
struct sk_buff *vlan_untag(struct sk_buff *skb)
{struct vlan_hdr *vhdr;u16 vlan_tci;if (unlikely(vlan_tx_tag_present(skb))) {/* vlan_tci is already set-up so leave this for another time */return skb;}skb = skb_share_check(skb, GFP_ATOMIC);if (unlikely(!skb))goto err_free;if (unlikely(!pskb_may_pull(skb, VLAN_HLEN)))goto err_free;vhdr = (struct vlan_hdr *) skb->data;vlan_tci = ntohs(vhdr->h_vlan_TCI);__vlan_hwaccel_put_tag(skb, skb->protocol, vlan_tci);skb_pull_rcsum(skb, VLAN_HLEN);vlan_set_encap_proto(skb, vhdr);skb = vlan_reorder_header(skb);if (unlikely(!skb))goto err_free;skb_reset_network_header(skb);skb_reset_transport_header(skb);skb_reset_mac_len(skb);return skb;err_free:kfree_skb(skb);return NULL;
}

2.3 vlan 添加 eth_type_trans()

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

3 三层协议处理钩子注册

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

struct packet_type {//网络层数据包协议类型__be16			type;	/* This is really htons(ether_type). *///接受从指定网络设备输入的数据包,若为NULL,则表示接受全部网络设备的数据包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;//链接不同协议族报文接受例程的指针
};

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

 

3.1 各种三层协议处理函数注册过程

参见《linux内核协议栈 三 / 四层协议接收数据处理函数以及相关的全局 hash表 / 数组》

3.2 桥处理函数注册

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);...
}
 

这篇关于linux内核协议栈 协议栈收包入口 netif_receive_skb的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

linux-基础知识3

打包和压缩 zip 安装zip软件包 yum -y install zip unzip 压缩打包命令: zip -q -r -d -u 压缩包文件名 目录和文件名列表 -q:不显示命令执行过程-r:递归处理,打包各级子目录和文件-u:把文件增加/替换到压缩包中-d:从压缩包中删除指定的文件 解压:unzip 压缩包名 打包文件 把压缩包从服务器下载到本地 把压缩包上传到服务器(zip

Linux 网络编程 --- 应用层

一、自定义协议和序列化反序列化 代码: 序列化反序列化实现网络版本计算器 二、HTTP协议 1、谈两个简单的预备知识 https://www.baidu.com/ --- 域名 --- 域名解析 --- IP地址 http的端口号为80端口,https的端口号为443 url为统一资源定位符。CSDNhttps://mp.csdn.net/mp_blog/creation/editor

【Python编程】Linux创建虚拟环境并配置与notebook相连接

1.创建 使用 venv 创建虚拟环境。例如,在当前目录下创建一个名为 myenv 的虚拟环境: python3 -m venv myenv 2.激活 激活虚拟环境使其成为当前终端会话的活动环境。运行: source myenv/bin/activate 3.与notebook连接 在虚拟环境中,使用 pip 安装 Jupyter 和 ipykernel: pip instal

内核启动时减少log的方式

内核引导选项 内核引导选项大体上可以分为两类:一类与设备无关、另一类与设备有关。与设备有关的引导选项多如牛毛,需要你自己阅读内核中的相应驱动程序源码以获取其能够接受的引导选项。比如,如果你想知道可以向 AHA1542 SCSI 驱动程序传递哪些引导选项,那么就查看 drivers/scsi/aha1542.c 文件,一般在前面 100 行注释里就可以找到所接受的引导选项说明。大多数选项是通过"_

Linux_kernel驱动开发11

一、改回nfs方式挂载根文件系统         在产品将要上线之前,需要制作不同类型格式的根文件系统         在产品研发阶段,我们还是需要使用nfs的方式挂载根文件系统         优点:可以直接在上位机中修改文件系统内容,延长EMMC的寿命         【1】重启上位机nfs服务         sudo service nfs-kernel-server resta

【Linux 从基础到进阶】Ansible自动化运维工具使用

Ansible自动化运维工具使用 Ansible 是一款开源的自动化运维工具,采用无代理架构(agentless),基于 SSH 连接进行管理,具有简单易用、灵活强大、可扩展性高等特点。它广泛用于服务器管理、应用部署、配置管理等任务。本文将介绍 Ansible 的安装、基本使用方法及一些实际运维场景中的应用,旨在帮助运维人员快速上手并熟练运用 Ansible。 1. Ansible的核心概念

Linux服务器Java启动脚本

Linux服务器Java启动脚本 1、初版2、优化版本3、常用脚本仓库 本文章介绍了如何在Linux服务器上执行Java并启动jar包, 通常我们会使用nohup直接启动,但是还是需要手动停止然后再次启动, 那如何更优雅的在服务器上启动jar包呢,让我们一起探讨一下吧。 1、初版 第一个版本是常用的做法,直接使用nohup后台启动jar包, 并将日志输出到当前文件夹n

[Linux]:进程(下)

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ 🎈🎈养成好习惯,先赞后看哦~🎈🎈 所属专栏:Linux学习 贝蒂的主页:Betty’s blog 1. 进程终止 1.1 进程退出的场景 进程退出只有以下三种情况: 代码运行完毕,结果正确。代码运行完毕,结果不正确。代码异常终止(进程崩溃)。 1.2 进程退出码 在编程中,我们通常认为main函数是代码的入口,但实际上它只是用户级

【Linux】应用层http协议

一、HTTP协议 1.1 简要介绍一下HTTP        我们在网络的应用层中可以自己定义协议,但是,已经有大佬定义了一些现成的,非常好用的应用层协议,供我们直接使用,HTTP(超文本传输协议)就是其中之一。        在互联网世界中,HTTP(超文本传输协议)是一个至关重要的协议,他定义了客户端(如浏览器)与服务器之间如何进行通信,以交换或者传输超文本(比如HTML文档)。