深入理解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设备了

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
深入理解Linux内核带书签的PDF是一本非常有价值的学习资源。Linux内核是操作系统的核心,负责管理硬件资源和提供用户与计算机硬件交互的接口。深入理解Linux内核可以帮助我们更好地理解操作系统的工作原理,并能够进行系统调优和故障排除。 这本书通过详细的介绍和分析,深入探讨了Linux内核的各个方面,包括进程管理、内存管理、文件系统、设备驱动、网络协议栈等等。通过学习这本书,我们可以了解内核的内部运行机制、数据结构和算法,并且了解它是如何处理各种操作系统任务的。 另外,这本书提供了书签功能,这对于学习者来说非常方便。可以使用书签功能来标记我们感兴趣或重要的内容,以便以后翻阅和复习。这样有助于我们更好地掌握书中的知识,加深理解,并能够更快地找到我们需要的信息。 带有书签的PDF版本的好处是可以轻松地在电子设备上阅读,比如电脑、平板电脑或手机。它具有可搜索的特性,这使得我们可以快速地查找特定的主题或关键词。此外,它还具有可扩展性,可以添加自己的笔记和注释,以便更好地组织知识。 总之,深入理解Linux内核带书签的PDF是一本非常有益的学习资源。它可以帮助我们深入学习和理解Linux内核,提升我们的技术水平,并且可以方便地进行知识的复习和查找。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

邋遢的流浪剑客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值