• 深入理解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);
    	}
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    查找路由项的函数是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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在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
    
    • 1
    • 2
    • 3

    从上述结果可以看出,对于目的是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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    对于本机的网络请求,设备将全部使用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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在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
    
    • 1
    • 2
    • 3

    很多人在看到这个路由表的时候就被它迷惑了,以为上面的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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    // net/ipv4/fib_frontend.c
    void fib_add_ifaddr(struct in_ifaddr *ifa)
    {
    	...
    	fib_magic(RTM_NEWROUTE, RTN_LOCAL, addr, 32, prim);
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    所以即使本机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);
    		...
    	}
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    在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);
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    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,
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    所以对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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在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);
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

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

    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);
    		...	
    	}
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    对于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;
    		...
    	}
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    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);
    		...
    	}
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    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设备了

  • 相关阅读:
    Adv. Intell. Syst. | 青岛大学发表基于深度学习和宿主信息嵌入的微生物组多标签疾病检测研究...
    Unity:InputField账号密码输入
    使用 @FastNative 和 @CriticalNative 的区别
    SSL证书报错类型及解决方法
    Spring Boot 3.0正式发布及新特性解读
    CANoe-以太网Link up问题、如何打开TC8参数配置文件
    Android8.0 后台服务保活的一种思路
    lint题目120 单词接龙 实现
    GESP 四级急救包(1):指针与地址
    STM32定时器的One Pulse Mode,OPM应用
  • 原文地址:https://blog.csdn.net/qq_40378034/article/details/134099794