当前位置: 首页 > article >正文

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

本文为《深入理解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.kler.cn/news/109157.html

相关文章:

  • 【技能树笔记】网络篇——练习题解析(十)
  • STM32-通用定时器
  • SpringBoot 整合 Nacos 实现统一配置中心
  • Azure云工作站上做Machine Learning模型开发 - 全流程演示
  • DVWA-SQL Injection SQL注入
  • 当『后设学习』碰上『工程学思维』
  • iOS iGameGuardian修改器检测方案
  • Python requests之Cookie
  • 大数据Flink(一百零五):SQL性能调优
  • 常见的配置文件格式:yaml,json,xml,ini,csv等
  • 一、灵动mm32单片机_开发环境的搭建(Keil)
  • jvm对象内存划分
  • C++设计模式_17_Mediator 中介者
  • 2023.10.28 关于 synchronized 原理
  • SDL事件处理以及线程使用(2)
  • 模型对象CSS2DObject始终在画布的左上角(问题解决)
  • LeetCode 541 反转字符串 II 简单
  • Python——PyQt5以及Pycharm相关配置
  • MyBatis的使用(XML映射文件)
  • review-java-basis
  • Centos7 Linux系统下生成https的crt和key证书
  • 【已解决】VSCode运行C#控制台乱码显示
  • IDE的组成
  • 解决:谷歌浏览器访问http时,自动转https访问的问题
  • Jtti:Apache服务的反向代理及负载均衡怎么配置
  • 宝塔安装mongodb插件失败的解决办法
  • RabbitMQ如何保证消息不丢失呢?
  • 在 Windows 用 Chrome System Settings 设置代理
  • WebClient, HttpClient, OkHttp: 三个Java HTTP客户端的比较
  • 设计模式——策略模式(Strategy Pattern)+ Spring相关源码