深入理解Linux网络笔记(五):深度理解本机网络IO

news2024/11/27 20:34:04

本文为《深入理解Linux网络》学习笔记,使用的Linux源码版本是3.10,网卡驱动默认采用的都是Intel的igb网卡驱动

Linux源码在线阅读:https://elixir.bootlin.com/linux/v3.10/source

4、深度理解本机网络IO

1)、跨机网络通信过程
1)跨机数据发送

数据包的发送过程如下图:

用户数据被拷贝到内核态,然后经过协议栈处理后进入RingBuffer。随后网卡驱动真正将数据发送了出去。当发送完成的时候,是通过硬中断来通知CPU,然后清理RingBuffer

从代码的视角得到的流程如下图:

等网络发送完毕,网卡会给CPU发送一个硬中断来通知CPU。收到这个硬中断后会释放RingBuffer中使用的内存,如下图所示:

2)跨机数据接收

数据包的接收过程如下图:

当网卡收到数据以后,向CPU发起一个中断,以通知CPU有数据到达。当CPU收到中断请求后,会去调用网络驱动注册的中断处理函数,触发软中断。ksoftirqd检测到有软中断请求到达,开始轮询收包,收到后交由各级协议栈处理。当协议栈处理完并把数据放到接收队列之后,唤醒用户进程(假设是阻塞方式)

从内核组件和源码视角来看,流程如下图:

3)跨机网络通信汇总

那么汇总起来,一次跨机网络通信的过程如下图所示:

2)、本机发送过程

本机网络IO和跨机网络IO有差异的地方总共有两处,分别是路由和驱动程序

1)网络层路由

发送数据进入协议栈到达网络层的时候,网络层入口函数是ip_queue_xmit。在网络层里会进行路由选择,路由选择完毕,再设置IP头,进行netfilter的过滤,将包交给邻居子系统。网络层工作流程如下图所示:

对于本机网络IO来说,特殊之处在于在local路由表中就能找到路由项,对应的设备都将使用loopback网卡,也就是常说的lo设备

网络层入口函数ip_queue_xmit源码如下:

// net/ipv4/ip_output.c
int ip_queue_xmit(struct sk_buff *skb, struct flowi *fl)
{
	...
	// 检查socket中是否有缓存的路由表
	rt = (struct rtable *)__sk_dst_check(sk, 0);
	if (rt == NULL) {
		...
		// 没有缓存则展开查找
		// 查找路由项,并缓存到socket中
		rt = ip_route_output_ports(sock_net(sk), fl4, sk,
					   daddr, inet->inet_saddr,
					   inet->inet_dport,
					   inet->inet_sport,
					   sk->sk_protocol,
					   RT_CONN_FLAGS(sk),
					   sk->sk_bound_dev_if);
		...
		sk_setup_caps(sk, &rt->dst);
	}
	...
}

查找路由项的函数是ip_route_output_ports,它又依次调用ip_route_output_flow、__ip_route_output_key、fib_lookup函数。调用过程略过,直接看fib_lookup的关键代码

// include/net/ip_fib.h
static inline int fib_lookup(struct net *net, const struct flowi4 *flp,
			     struct fib_result *res)
{
	struct fib_table *table;

	table = fib_get_table(net, RT_TABLE_LOCAL);
	if (!fib_table_lookup(table, flp, res, FIB_LOOKUP_NOREF))
		return 0;

	table = fib_get_table(net, RT_TABLE_MAIN);
	if (!fib_table_lookup(table, flp, res, FIB_LOOKUP_NOREF))
		return 0;
	return -ENETUNREACH;
}

在fib_lookup中将会对local和main两个路由表展开查询,并且先查询local后查询main。我们在Linux上使用ip命令可以查看到这两个路由表,这里只看local路由表(因为本机网络IO查询到这个表就终止了)

$ ip route list table local
local 10.143.x.y dev eth0 proto kernel scope host src 10.143.x.y 
local 127.0.0.1 dev lo proto kernel scope host src 127.0.0.1

从上述结果可以看出,对于目的是127.0.0.1的路由在local路由表中就能够找到。fib_lookup的工作完成,返回__ip_route_output_key函数继续执行

// net/ipv4/route.c
struct rtable *__ip_route_output_key(struct net *net, struct flowi4 *fl4)
{
	...
	if (fib_lookup(net, fl4, &res)) {
		...
	}

	if (res.type == RTN_LOCAL) {
		...
		dev_out = net->loopback_dev;
		...
	}
	...
	rth = __mkroute_output(&res, fl4, orig_oif, dev_out, flags);
	...
	return rth;
}

对于本机的网络请求,设备将全部使用net->loopback_dev,也就是lo虚拟网卡

接下来的网络层仍然和跨机网络IO一样,最终会经过ip_finish_output,进入邻居子系统的入口函数dst_neigh_output

本机网络IO需要进行IP分片吗?

因为和正常的网络层处理过程一样,会经过ip_finish_output函数,在这个函数中,如果skb大于MTU,仍然会进行分片。只不过lo虚拟网卡的MTU比Ethernet要大很多。通过ifconfig命令就可以查到,物理网卡MTU一般为1500,而lo虚拟接口能有65535个

在邻居子系统函数中经过处理后,进入网络设备子系统(入口函数是dev_queue_xmit)

2)本机IP路由

问题:用本机IP(例如192.168.x.x)和用127.0.0.1在性能上有差别吗?

前面讲过,选用哪个设备是路由相关函数__ip_route_output_key确定的

// net/ipv4/route.c
struct rtable *__ip_route_output_key(struct net *net, struct flowi4 *fl4)
{
	...
	if (fib_lookup(net, fl4, &res)) {
		...
	}

	if (res.type == RTN_LOCAL) {
		...
		dev_out = net->loopback_dev;
		...
	}
	...
	rth = __mkroute_output(&res, fl4, orig_oif, dev_out, flags);
	...
	return rth;
}

在fib_lookup函数里会查询到local路由表

$ ip route list table local
local 10.162.*.* dev eth0 proto kernel scope host src 10.162.*.*
local 127.0.0.1 dev lo proto kernel scope host src 127.0.0.1

很多人在看到这个路由表的时候就被它迷惑了,以为上面的10.162.*.*真的会被路由到eth0(其中10.162.*.*是我的本机局域网IP,后面两段用*号隐藏起来了)

但其实内核在初始化local路由表的时候,把local路由表里所有的路由项都设置成了RTN_LOCAL,不只是127.0.0.1。这个过程是在设置本机IP的时候,调用fib_inetaddr_event函数完成设置的

// net/ipv4/fib_frontend.c
static int fib_inetaddr_event(struct notifier_block *this, unsigned long event, void *ptr)
{
	...
	switch (event) {
	case NETDEV_UP:
		fib_add_ifaddr(ifa);
		...
		break;
	case NETDEV_DOWN:
		fib_del_ifaddr(ifa, NULL);
		...
		break;
	}
	return NOTIFY_DONE;
}
// net/ipv4/fib_frontend.c
void fib_add_ifaddr(struct in_ifaddr *ifa)
{
	...
	fib_magic(RTM_NEWROUTE, RTN_LOCAL, addr, 32, prim);
	...
}

所以即使本机IP不用127.0.0.1,内核在路由项查找的时候判断类型是RTN_LOCAL,仍然会使用net->loopback_dev,也就是lo虚拟网卡

3)网络设备子系统

网络设备子系统的入口函数是dev_hard_start_xmit。之前讲述跨机发送过程时介绍过,对于真的有队列的物理设备,该函数进行了一系列复杂的排队等处理后,才调用dev_hard_start_xmit,从这个函数再进入驱动程序来发送。在这个过程中,甚至还有可能出发软中断进行发送,流程如下图:

但是对于启动状态的回环设备(q->enqueue判断为false)来说,就简单多了。没有队列的问题,直接进入dev_hard_start_xmit。接着进入回环设备的驱动里发送回调函数loopback_xmit,将skb发送出去,如下图所示:

下面来看看详细的过程,从网络设备子系统的入口函数dev_queue_xmit看起

// net/core/dev.c
int dev_queue_xmit(struct sk_buff *skb)
{
	...
	q = rcu_dereference_bh(txq->qdisc);
	...
	if (q->enqueue) { // 回环设备这里为false
		rc = __dev_xmit_skb(skb, q, dev, txq);
		goto out;
	}

	// 开始回环设备处理
	if (dev->flags & IFF_UP) {
		...
				rc = dev_hard_start_xmit(skb, dev, txq);
		...
	}
	...
}

在dev_queue_xmit函数中还将调用设备驱动的操作函数

// net/core/dev.c
int dev_hard_start_xmit(struct sk_buff *skb, struct net_device *dev,
			struct netdev_queue *txq)
{
	// 获取设备驱动的回调函数集合ops
	const struct net_device_ops *ops = dev->netdev_ops;
	...
		// 调用驱动的ndo_start_xmit进行发送
		rc = ops->ndo_start_xmit(skb, dev);
	...
}
4)驱动程序

回环设备的驱动程序的工作流程如下图:

loopback(回环)设备的驱动代码在drivers/net/loopback.c文件里

// drivers/net/loopback.c
static const struct net_device_ops loopback_ops = {
	.ndo_init      = loopback_dev_init,
	.ndo_start_xmit= loopback_xmit,
	.ndo_get_stats64 = loopback_get_stats64,
};

所以对dev_hard_start_xmit调用实际上执行的是loopback驱动里的loopback_xmit(loopback是一个纯软件性质的虚拟接口,并没有真正意义上对物理设备的驱动)

// drivers/net/loopback.c
static netdev_tx_t loopback_xmit(struct sk_buff *skb,
				 struct net_device *dev)
{
	...
	// 剥离掉和原socket的联系
	skb_orphan(skb);
	...
	// 调用netif_rx
	if (likely(netif_rx(skb) == NET_RX_SUCCESS)) {
		...
	}

	return NETDEV_TX_OK;
}

在skb_orphan中先把skb上的socket指针去掉了(剥离出来)

注意,在本机网络IO发送的过程中,传输层下面的skb就不需要释放了,直接给接收方传过去就行,总算是省了一点点开销。不过可惜传输层的skb同样节约不了,还是要频繁地申请和释放

接着调用netif_rx,在该方法中最终会执行到enqueue_to_backlog(netif_rx->enqueue_to_backlog)

// net/core/dev.c
static int enqueue_to_backlog(struct sk_buff *skb, int cpu,
			      unsigned int *qtail)
{
	...
	sd = &per_cpu(softnet_data, cpu);
	...
			__skb_queue_tail(&sd->input_pkt_queue, skb);
			...
				____napi_schedule(sd, &sd->backlog);
	...
}

在enqueue_to_backlog函数中,把要发送的skb插入softnet_data->input_pkt_queue队列 并调用____napi_schedule来触发软中断

// net/core/dev.c
static inline void ____napi_schedule(struct softnet_data *sd,
				     struct napi_struct *napi)
{
	list_add_tail(&napi->poll_list, &sd->poll_list);
	__raise_softirq_irqoff(NET_RX_SOFTIRQ);
}

只有触发完软中断,发送过程才算完成了

3)、本机接收过程

发送过程触发软中断后,会进入软中断处理函数net_rx_action,如下图所示:

在跨机的网络包的接收过程中,需要经过硬中断,然后才能触发软中断。而在本机的网络IO过程中,由于并不真的过网卡,所以网卡的发送过程、硬中断就都省去了,直接从软中断开始

在软中断被触发以后,会进入NET_RX_SOFTIRQ对应的处理方法net_rx_action中

// net/core/dev.c
static void net_rx_action(struct softirq_action *h)
{
	...
	while (!list_empty(&sd->poll_list)) {
		...
			work = n->poll(n, weight);
		...	
	}
	...
}

对于igb网卡来说,poll实际调用的是igb_poll函数。那么loopback网卡的poll函数是哪个呢?由于poll_list里面是struct softnet_data对象,在net_dev_init中找到了对应的处理函数

// net/core/dev.c
static int __init net_dev_init(void)
{
	...
	for_each_possible_cpu(i) {
		...
		sd->backlog.poll = process_backlog;
		...
	}
	...
}

struct softnet_data默认的poll在初始化的时候设置成了process_backlog函数

// net/core/dev.c
static int process_backlog(struct napi_struct *napi, int quota)
{
	...
	while (work < quota) {
		...
		while ((skb = __skb_dequeue(&sd->process_queue))) {
			...
			__netif_receive_skb(skb);
			...
		}
		...
		// skb_queue_splice_tail_init()函数用于将链表a连接到链表b上,
		// 形成一个新的链表b,并将原来a的头编程空链表
		qlen = skb_queue_len(&sd->input_pkt_queue);
		if (qlen)
			skb_queue_splice_tail_init(&sd->input_pkt_queue,
						   &sd->process_queue);
		...
	}
	...
}

skb_queue_splice_tail_init是把sd->input_pkt_queue里的skb链到sd->process_queue链表上去,__skb_dequeue是从sd->process_queue取下来包进行处理。这样和前面发送过程的结尾处就对上,发送过程是把包放到了input_pkt_queue队列里,如下图所示:

最后调用__netif_receive_skb将数据送往协议栈。在此之后的调用过程就和跨机网络IO又一致了。送往协议栈的调用链是__netif_receive_skb=>__netif_receive_skb_core=>deliver_skb,然后将数据包送入ip_rcv中。网络层再往后是传输层,最后唤醒用户进程

4)、总结

本机网络IO的内核总体执行流程如下图:

1)127.0.0.1本机网络IO需要经过网卡吗?

不需要经过网卡。即使把网卡拔了,本机网络还是可以正常使用的

2)数据包在内核中是什么走向,和外网发送相比流程上有什么差别?

总的来说,本机网络IO和跨机网络IO比较起来,确实是节约了驱动上的一些开销。发送数据不需要进RingBuffer的驱动队列,直接把skb传给接收协议栈(经过软中断)。但是在内核其他组件上,可是一点儿都没少,系统调用、协议栈(传输层、网络层等)、设备子系统整个走了一遍。连驱动程序都走了(虽然对于回环设备来说只是一个纯软件的虚拟出来的东西)。所以即使是本机网络IO,切忌误认为没啥开销就滥用

3)访问本机服务时,使用127.0.0.1能比使用本机IP(例如192.168.x.x)更快吗?

使用本机IP和127.0.0.1没有差别,都是走虚拟的回环设备lo。这是因为内核在设置IP的时候,把所有的本机IP都初始化到local路由表里了,类型写死了是RTN_LOCAL。在后面的路由项选择的时候发现类型是RTN_LOCAL就会选择lo设备了

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1146332.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

快速排序——及其改进

hoare版本&#xff08;原始版本&#xff09;&#xff1a; 思想&#xff1a;树的遍历思想&#xff0c;先把数组第一个数作为KEY,然后left从左到右&#xff0c;right从右到左一起走&#xff0c;当left找到比key大的值时停下来&#xff0c;当right找到比key小的值时停下来&#xf…

通讯网关软件030——利用CommGate X2Modbus实现Modbus RTU访问Mysql服务器

本文介绍利用CommGate X2Modbus实现Modbus RTU访问Mysql数据库。CommGate X2MODBUS是宁波科安网信开发的网关软件&#xff0c;软件可以登录到网信智汇(http://wangxinzhihui.com)下载。 【案例】如下图所示&#xff0c;实现上位机通过Modbus RTU来获取Mysql数据库的数据。 【解…

IOC课程整理-16

1. Java 泛型基础 Java中的泛型擦除&#xff08;Type Erasure&#xff09;是Java编译器为了兼容之前的非泛型代码而采用的一种机制。在编译过程中&#xff0c;Java编译器会将泛型类型转换为原始类型&#xff0c;并在必要时插入强制类型转换。 泛型擦除有以下几个主要特点&…

深度学习_1 介绍;安装环境

深度学习 学习自李沐老师的课程。笔记主要以总结老师所讲解的内容以及我个人的想法为主&#xff0c;侵删&#xff01; 课程链接&#xff1a;课程安排 - 动手学深度学习课程 (d2l.ai) 介绍 AI地图&#xff1a; 我们以前写的非 AI 类程序基本都是人自己去想会遇到什么样的问题…

【PyQt学习篇 · ③】:QObject - 神奇的对象管理工具

文章目录 QObject类型判定常用的API应用场景&#xff1a;过滤筛选控件 QObject定时器常用API应用场景 QObject类型判定 常用的API isWidgetType()方法&#xff1a; 使用方式&#xff1a;obj.isWidgetType()作用&#xff1a;判断一个对象是否为QWidget及其子类的实例。QWidget…

4.5 final修饰符

在Java中&#xff0c;final修饰符可以修饰类、属性和方法&#xff0c;final有“最终”、“不可更改”的含义&#xff0c;所以在使用final关键字时需要注意以下几点&#xff1a; 使用final修饰类&#xff0c;则该类就为最终类&#xff0c;最终类不能被继承。 使用final修饰方法…

C++----模板进阶

文章目录 非类型模板参数STL知识补充 类模板的特化函数模板特化类模板特化偏特化 模板的分离编译模板总结 非类型模板参数 模板参数分为类型形参与非类型形参。 类型形参&#xff1a;出现在模板参数列表中&#xff0c;跟在class或者typename之类的参数类型名称。 非类型形参…

Vue性能优化:加速你的应用

目录 1. 使用虚拟DOM 2. 合理使用计算属性和侦听器 3. 懒加载组件 4. 合理使用v-if和v-show 5. 使用Key管理列表渲染 6. 避免不必要的Watcher 7. 缓存响应式数据 8. 使用异步组件 9. 使用Webpack进行代码优化 10. 监控性能并进行优化 Vue.js是一款流行的JavaScript框…

东软集团:看似低调,却有了19年的AI坚持

【科技明说 &#xff5c; 重磅专题】 在AI领域的专注与研究&#xff0c;东软集团是一个低调的存在。 可能很多人不太了解东软集团对于AI的专心与专注以及专业。三专可以简单概括东软集团的AI雄心壮志。 专注在于&#xff0c;早在2004年&#xff0c;东软就开始启动人工智能技…

【Apache Flink】流式分析的多种应用场景

文章目录 0. 前言1. 数据处理架构的演进2. 传统数据处理架构3. 事务型处理4. 分析型处理用于数据分析的传统数据仓架构 状态化流处理5. 事件驱动型应用什么是事件驱动型应用&#xff1f; 6. 数据管道什么是数据管道&#xff1f;Flink 如何支持数据管道应用&#xff1f;典型的数…

二叉树三种遍历的递归与非递归写法

目录 ​编辑 一&#xff0c;前序遍历 题目接口&#xff1a; 递归解法&#xff1a; 非递归解法&#xff1a; 二&#xff0c;中序遍历 题目接口&#xff1a; 递归解法&#xff1a; 非递归写法&#xff1a; 三&#xff0c;后序遍历 题目接口&#xff1a; 递归解法&…

IOC课程整理-17 Spring事件

1. Java 事件/监听器编程模型 2. 面向接口的事件/监听器设计模式 3. 面向注解的事件/监听器设计模式 4. Spring 标准事件-ApplicationEvent 5. 基于接口的 Spring 事件监听器 6. 基于注解的 Spring 事件监听器 7. 注册 Spring ApplicationListener 8. Spring 事件发布器 9. Spr…

基于VectorGrid加载GeoServer发布的矢量瓦片实例

目录 前言 一、关于VectorGrid 1、开源地址 2、本地示例 二、与LeafLet集成 1、新建html页面 2、地图初始化 3、pbf瓦片地址配置 4、pbf初始化 三、GeoServer跨域问题 1、web.xml配置 2、重启tomcat 总结 前言 回望10月&#xff0c;发生了一些变动&#xff0c;面向未…

状态机图和活动图

在面向对象软件分析过程中&#xff0c;状态机图和活动图用于建立软件的动态模型&#xff0c;主要描述系统随时间变化的行为。 1.状态图 1.1概念 状态图用来描述对象状态和事件之间的关系&#xff0c;强调一个实体基于事件反应的动态行为。状态图适合用于表述在不同用例之间的…

C语言#error和#line

C语言#error和#line #error #error用于生成一个编译错误消息&#xff0c;并停止编译 示例&#xff1a; 随便找了一个工程测试下#error 看图中我圈起来的部分&#xff0c;编译器提示warning和error。看我的程序如果没有定义TEST_#ERROR这个宏&#xff0c;编译器会报错You di…

我是如何快速从python小白达到20k?

前言 首先说一下我自己的情况&#xff0c;我之前是学JAVA的&#xff0c;JAVA亡了只好转行python 很多新手就在好奇自己明明都认认真真的学习了python&#xff0c;但就是感觉很杂很乱&#xff0c;按照我这个流程&#xff0c;至少可以省一大半时间&#xff0c;完整的知识体系很重…

LED主流光源-环形光源

1&#xff09;产品特点&#xff1a; ① 环形光源提供不同角度照射&#xff0c;能突出物体的三维信息有效的解决对角照射阴 影问题&#xff1b; ② 周围表面采用滚花设计&#xff0c;扩大散热面积保障光源的使用寿命&#xff1b; ③ 根据客户不同需求可 选配不同漫射板&#xff…

AIGC如何助力产品研发的创新和性能提升

1、现有的产品和系统的升级 a&#xff09;、关键算法的替换&#xff0c;用深度学习来替换&#xff0c;用数学来描述&#xff1a; 需要定义好中间状态的和&#xff0c;总体过程是,中间的过程,替换为。 总体过程表示成下面的方式: 完成替换过程&#xff1a; 。 b&#xff09;…

vivado窗口使用与分析2-IDE 中的逻辑分析

逻辑分析 包括 &#xff1a; • “ Netlist ”窗口 • “ Hierarchy ”窗口 • “ Schematic ”窗口 1、 “ Netlist ”窗口 “ Netlist ” &#xff08; 网表 &#xff09; 窗口显示了网表中由综合工具所处理的设计层级。 根据综合设置 &#xff0c; 网表层级与原始 RT…

【网络】序列化反序列化

序列化反序列化 一、序列化反序列化1、概念2、序列化作用3、序列化框架的选择 二、Json1、介绍2、简单使用 一、序列化反序列化 1、概念 在前文《网络编程套接字》中&#xff0c;我们实现了服务器与客户端之间的字符串通信&#xff0c;这是非常简单的通信&#xff0c;在实际使…