鲜为人知的网络编程(十三):深刻操做系统,完全搞懂127.0.0.1本机网络通讯

本文做者张彦飞,原题“127.0.0.1 之本机网络通讯过程知多少 ”,首次发布于“开发内功修炼”,转载请联系做者。本次有改动。php

一、引言

继《你真的了解127.0.0.1和0.0.0.0的区别?》以后,这是我整理的第2篇有关本机网络方面的网络编程基础文章。html

此次的文章由做者张彦飞原创分享,写做本文的缘由是如今本机网络 IO 应用很是广。在 php 中 通常 Nginx 和 php-fpm 是经过 127.0.0.1 来进行通讯的;在微服务中,因为 side car 模式的应用,本机网络请求更是愈来愈多。因此,若是能深度理解这个问题在各类网络通讯应用的技术实践中将很是的有意义。编程

今天我们就把 127.0.0.1 本机网络通讯相关问题搞搞清楚!api

为了方便讨论,我把这个问题拆分红3问:缓存

  • 1)127.0.0.1 本机网络 IO 须要通过网卡吗?
  • 2)和外网网络通讯相比,在内核收发流程上有啥差异?
  • 3)使用 127.0.0.1 能比 192.168.x 更快吗?

上面这几个问题,相信包括即时通信老鸟们在内,都是看似很熟悉,但实则仍然没法透彻讲清楚的话题。此次,咱们就来完全搞清楚!markdown

二、系列文章

本文是系列文章中的第13篇,本系列文章的大纲以下:网络

鲜为人知的网络编程(一):浅析TCP协议中的疑难杂症(上篇)负载均衡

鲜为人知的网络编程(二):浅析TCP协议中的疑难杂症(下篇)socket

鲜为人知的网络编程(三):关闭TCP链接时为何会TIME_WAIT、CLOSE_WAITide

鲜为人知的网络编程(四):深刻研究分析TCP的异常关闭

鲜为人知的网络编程(五):UDP的链接性和负载均衡

鲜为人知的网络编程(六):深刻地理解UDP协议并用好它

鲜为人知的网络编程(七):如何让不可靠的UDP变的可靠?

鲜为人知的网络编程(八):从数据传输层深度解密HTTP

鲜为人知的网络编程(九):理论联系实际,全方位深刻理解DNS

鲜为人知的网络编程(十):深刻操做系统,从内核理解网络包的接收过程(Linux篇)

鲜为人知的网络编程(十一):从底层入手,深度分析TCP链接耗时的秘密

鲜为人知的网络编程(十二):完全搞懂TCP协议层的KeepAlive保活机制

鲜为人知的网络编程(十三):深刻操做系统,完全搞懂127.0.0.1本机网络通讯》(* 本文

三、做为对比,先看看跨机网路通讯

在开始讲述本机通讯过程以前,咱们先看看跨机网络通讯(以Linux系统内核中的实现为例来说解)。

3.1 跨机数据发送

从 send 系统调用开始,直到网卡把数据发送出去,总体流程以下:

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

不过上面这幅图并无很好地把内核组件和源码展现出来,咱们再从代码的视角看一遍。

等网络发送完毕以后。网卡在发送完毕的时候,会给 CPU 发送一个硬中断来通知 CPU。收到这个硬中断后会释放 RingBuffer 中使用的内存。

3.2 跨机数据接收

当数据包到达另一台机器的时候,Linux 数据包的接收过程开始了(更详细的讲解能够看看《深刻操做系统,从内核理解网络包的接收过程(Linux篇)》)。

▲ 上图引用自《深刻操做系统,从内核理解网络包的接收过程(Linux篇)

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

咱们再一样从内核组件和源码视角看一遍。

3.3 跨机网络通讯汇总

关于跨机网络通讯的理解,能够通俗地用下面这张图来总结一下:

四、本机网络数据的发送过程

在上一节中,咱们看到了跨机时整个网络数据的发送过程 。

在本机网络 IO 的过程当中,流程会有一些差异。为了突出重点,本节将再也不介绍总体流程,而是只介绍和跨机逻辑不一样的地方。有差别的地方总共有两个,分别是路由和驱动程序。

4.1 网络层路由

发送数据会进入协议栈到网络层的时候,网络层入口函数是 ip_queue_xmit。在网络层里会进行路由选择,路由选择完毕后,再设置一些 IP 头、进行一些 netfilter 的过滤后,将包交给邻居子系统。

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

咱们来详细看看路由网络层里这段路由相关工做过程。从网络层入口函数 ip_queue_xmit 看起。

//file: net/ipv4/ip_output.c

intip_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(...);

sk_setup_caps(sk, &rt->dst);

}

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

//file: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 上使用命令名能够查看到这两个路由表, 这里只看 local 路由表(由于本机网络 IO 查询到这个表就终止了)。

#ip route list table local

local10.143.x.y dev eth0 proto kernel scope host src 10.143.x.y

local127.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 继续。

//file: 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 命令就能够查到,普通网卡通常为 1500,而 lO 虚拟接口能有 65535。

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

4.2 网络设备子系统

网络设备子系统的入口函数是 dev_queue_xmit。简单回忆下以前讲述跨机发送过程的时候,对于真的有队列的物理设备,在该函数中进行了一系列复杂的排队等处理之后,才调用 dev_hard_start_xmit,从这个函数 再进入驱动程序来发送。

在这个过程当中,甚至还有可能会触发软中断来进行发送,流程如图:

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

咱们来看下详细的过程,从网络设备子系统的入口 dev_queue_xmit 看起。

//file: 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) {

dev_hard_start_xmit(skb, dev, txq, ...);

...

}

}

在 dev_hard_start_xmit 中仍是将调用设备驱动的操做函数。

//file: 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.3 “驱动”程序

对于真实的 igb 网卡来讲,它的驱动代码都在

drivers/net/ethernet/intel/igb/igb_main.c

文件里。顺着这个路子,我找到了 loopback 设备的“驱动”代码位置:

drivers/net/loopback.c

在 drivers/net/loopback.c:

//file: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 是一个纯软件性质的虚拟接口,并无真正意义上的驱动,它的工做流程大体如图。

咱们再来看详细的代码。

//file: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)) {

}

}

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

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

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

//file: 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 来触发软中断。

//file: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);

}

只有触发完软中断,发送过程就算是完成了。

五、本机网络数据的接收过程

5.1 主要过程

在跨机的网络包的接收过程当中,须要通过硬中断,而后才能触发软中断。

而在本机的网络 IO 过程当中,因为并不真的过网卡,因此网卡实际传输,硬中断就都省去了。直接从软中断开始,通过 process_backlog 后送进协议栈,大致过程以下图。

5.2 详细过程

接下来咱们再看更详细一点的过程。

在软中断被触发之后,会进入到 NET_RX_SOFTIRQ 对应的处理方法 net_rx_action 中(至于细节参见《深刻操做系统,从内核理解网络包的接收过程(Linux篇)》一文中的 4.2 小节)。

//file: 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 中找到了蛛丝马迹。

//file: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 函数,来看看它都干了啥。

static int process_backlog(struct napi_struct *napi, int quota)

{

while(){

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, __skb_dequeue 是从 sd->process_queue 上取下来包来处理。这样和前面发送过程的结尾处就对上了。发送过程是把包放到了 input_pkt_queue 队列里,接收过程是在从这个队列里取出 skb。

最后调用 __netif_receive_skb 将 skb(数据) 送往协议栈。在此以后的调用过程就和跨机网络 IO 又一致了。

送往协议栈的调用链是 __netif_receive_skb => __netif_receive_skb_core => deliver_skb 后 将数据包送入到 ip_rcv 中(详情参见《深刻操做系统,从内核理解网络包的接收过程(Linux篇)》一文中的 4.3 小节)。

网络再日后依次是传输层,最后唤醒用户进程,这里就很少展开了。

六、本机网络通讯过程小结

咱们来总结一下本机网络通讯的内核执行流程:

回想下跨机网络 IO 的流程是:

好了,回到正题,咱们终于能够在单独的章节里回答开篇的三个问题啦。

七、开篇三个问题的答案

1)问题1:127.0.0.1 本机网络 IO 须要通过网卡吗?

经过本文的叙述,咱们肯定地得出结论,不须要通过网卡。即便了把网卡拔了本机网络是否还能够正常使用的。

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

总的来讲,本机网络 IO 和跨机 IO 比较起来,确实是节约了一些开销。发送数据不须要进 RingBuffer 的驱动队列,直接把 skb 传给接收协议栈(通过软中断)。

可是在内核其它组件上但是一点都没少:系统调用、协议栈(传输层、网络层等)、网络设备子系统、邻居子系统整个走了一个遍。连“驱动”程序都走了(虽然对于回环设备来讲只是一个纯软件的虚拟出来的东东)。因此

即便是本机网络 IO,也别误觉得没啥开销

3)问题3:使用 127.0.0.1 能比 192.168.x 更快吗?

**先说结论:**我认为这两种使用方法在性能上没有啥差异。

我以为有至关大一部分人都会认为访问本机 Server 的话,用 127.0.0.1 更快。缘由是直觉上认为访问 IP 就会通过网卡。

其实内核知道本机上全部的 IP,只要发现目的地址是本机 IP 就能够全走 loopback 回环设备了。本机其它 IP 和 127.0.0.1 同样,也是不用过物理网卡的,因此访问它们性能开销基本同样!(本文同步发布于:www.52im.net/thread-3600…

相关文章
相关标签/搜索