本文作者张彦飞,原题“127.0.0.1 之本机网络通信过程知多少”,首次公布于“开发内功修炼”,转载请分割作者。本次有改变。
1、引言
继《你真的理解 127.0.0.1 和 0.0.0.0 的区别?》之后,这是我整顿的第 2 篇无关本机网络方面的网络编程根底文章。
这次的文章由作者张彦飞原创分享,写作本文的起因是当初本机网络 IO 利用十分广。在 php 中 个别 Nginx 和 php-fpm 是通过 127.0.0.1 来进行通信的;在微服务中,因为 side car 模式的利用,本机网络申请更是越来越多。所以,如果能深度了解这个问题在各种网络通信利用的技术实际中将十分的有意义。
明天咱们就把 127.0.0.1 本机网络通信相干问题搞搞分明!
为了不便探讨,我把这个问题拆分成 3 问:
1)127.0.0.1 本机网络 IO 须要通过网卡吗?
2)和外网网络通信相比,在内核收发流程上有啥差异?
3)应用 127.0.0.1 能比 192.168.x 更快吗?
下面这几个问题,置信包含常期混迹于即时通讯网的即时通讯老鸟们在内,都是看似很相熟,但实则依然无奈透彻讲清楚的话题。这次,咱们就来彻底搞清楚!
(本文同步公布于:http://www.52im.net/thread-3600-1-1.html)
2、系列文章
本文是系列文章中的第 13 篇,本系列文章的纲要如下:
《鲜为人知的网络编程(一):浅析 TCP 协定中的疑难杂症(上篇)》
《鲜为人知的网络编程(二):浅析 TCP 协定中的疑难杂症(下篇)》
《鲜为人知的网络编程(三):敞开 TCP 连贯时为什么会 TIME_WAIT、CLOSE_WAIT》
《鲜为人知的网络编程(四):深入研究剖析 TCP 的异样敞开》
《鲜为人知的网络编程(五):UDP 的连接性和负载平衡》
《鲜为人知的网络编程(六):深刻地了解 UDP 协定并用好它》
《鲜为人知的网络编程(七):如何让不牢靠的 UDP 变的牢靠?》
《鲜为人知的网络编程(八):从数据传输层深度解密 HTTP》
《鲜为人知的网络编程(九):实践联系实际,全方位深刻了解 DNS》
《鲜为人知的网络编程(十):深刻操作系统,从内核了解网络包的接管过程(Linux 篇)》
《鲜为人知的网络编程(十一):从底层动手,深度剖析 TCP 连贯耗时的机密》
《鲜为人知的网络编程(十二):彻底搞懂 TCP 协定层的 KeepAlive 保活机制》
《鲜为人知的网络编程(十三):深刻操作系统,彻底搞懂 127.0.0.1 本机网络通信》(* 本文)
3、作为比照,先看看跨机网路通信
在开始讲述本机通信过程之前,咱们先看看跨机网络通信(以 Linux 零碎内核中的实现为例来解说)。
3.1 跨机数据发送
从 send 零碎调用开始,直到网卡把数据发送进来,整体流程如下:
在下面这幅图中,咱们看到用户数据被拷贝到内核态,而后通过协定栈解决后进入到了 RingBuffer 中。随后网卡驱动真正将数据发送了进来。当发送实现的时候,是通过硬中断来告诉 CPU,而后清理 RingBuffer。
不过下面这幅图并没有很好地把内核组件和源码展现进去,咱们再从代码的视角看一遍。
等网络发送结束之后。网卡在发送结束的时候,会给 CPU 发送一个硬中断来告诉 CPU。收到这个硬中断后会开释 RingBuffer 中应用的内存。
3.2 跨机数据接管
当数据包达到另外一台机器的时候,Linux 数据包的接管过程开始了(更具体的解说能够看看《深刻操作系统,从内核了解网络包的接管过程(Linux 篇)》)。
▲ 上图援用自《深刻操作系统,从内核了解网络包的接管过程(Linux 篇)》
当网卡收到数据当前,CPU 发动一个中断,以告诉 CPU 有数据达到。当 CPU 收到中断请求后,会去调用网络驱动注册的中断处理函数,触发软中断。ksoftirqd 检测到有软中断请求达到,开始轮询收包,收到后交由各级协定栈解决。当协定栈解决完并把数据放到接管队列的之后,唤醒用户过程(假如是阻塞形式)。
咱们再同样从内核组件和源码视角看一遍。
3.3 跨机网络通信汇总
对于跨机网络通信的了解,能够艰深地用上面这张图来总结一下:
4、本机网络数据的发送过程
在上一节中,咱们看到了跨机时整个网络数据的发送过程。
在本机网络 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、本机网络数据的接管过程
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 大节)。
网络再往后顺次是传输层,最初唤醒用户过程,这里就不多开展了。
6、本机网络通信过程小结
咱们来总结一下本机网络通信的内核执行流程:
回忆下跨机网络 IO 的流程是:
好了,回到正题,咱们终于能够在独自的章节里答复开篇的三个问题啦。
7、开篇三个问题的答案
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 一样,也是不必过物理网卡的,所以拜访它们性能开销根本一样!
附录:更多网络编程系列文章
如果您感觉本系列文章过于业余,您可先浏览《网络编程懒人入门》系列文章,该系列目录如下:
《网络编程懒人入门(一):疾速了解网络通信协定(上篇)》
《网络编程懒人入门(二):疾速了解网络通信协定(下篇)》
《网络编程懒人入门(三):疾速了解 TCP 协定一篇就够》
《网络编程懒人入门(四):疾速了解 TCP 和 UDP 的差别》
《网络编程懒人入门(五):疾速了解为什么说 UDP 有时比 TCP 更有劣势》
《网络编程懒人入门(六):史上最艰深的集线器、交换机、路由器性能原理入门》
《网络编程懒人入门(七):深入浅出,全面了解 HTTP 协定》
《网络编程懒人入门(八):手把手教你写基于 TCP 的 Socket 长连贯》
《网络编程懒人入门(九):艰深解说,有了 IP 地址,为何还要用 MAC 地址?》
《网络编程懒人入门(十):一泡尿的工夫,疾速读懂 QUIC 协定》
《网络编程懒人入门(十一):一文读懂什么是 IPv6》
《网络编程懒人入门(十二):疾速读懂 Http/ 3 协定,一篇就够!》
本站的《脑残式网络编程入门》也适宜入门学习,本系列纲要如下:
《脑残式网络编程入门(一):跟着动画来学 TCP 三次握手和四次挥手》
《脑残式网络编程入门(二):咱们在读写 Socket 时,到底在读写什么?》
《脑残式网络编程入门(三):HTTP 协定必知必会的一些常识》
《脑残式网络编程入门(四):疾速了解 HTTP/ 2 的服务器推送(Server Push)》
《脑残式网络编程入门(五):每天都在用的 Ping 命令,它到底是什么?》
《脑残式网络编程入门(六):什么是公网 IP 和内网 IP?NAT 转换又是什么鬼?》
《脑残式网络编程入门(七):面视必备,史上最艰深计算机网络分层详解》
《脑残式网络编程入门(八):你真的理解 127.0.0.1 和 0.0.0.0 的区别?》
《脑残式网络编程入门(九):面试必考,史上最艰深大小端字节序详解》
以下材料来自《TCP/IP 详解》,入门者必读:
《TCP/IP 详解 – 第 11 章·UDP:用户数据报协定》
《TCP/IP 详解 – 第 17 章·TCP:传输控制协议》
《TCP/IP 详解 – 第 18 章·TCP 连贯的建设与终止》
《TCP/IP 详解 – 第 21 章·TCP 的超时与重传》
以下系列适宜服务端网络编程开发者浏览:
《高性能网络编程(一):单台服务器并发 TCP 连接数到底能够有多少》
《高性能网络编程(二):上一个 10 年,驰名的 C10K 并发连贯问题》
《高性能网络编程(三):下一个 10 年,是时候思考 C10M 并发问题了》
《高性能网络编程(四):从 C10K 到 C10M 高性能网络应用的实践摸索》
《高性能网络编程(五):一文读懂高性能网络编程中的 I / O 模型》
《高性能网络编程(六):一文读懂高性能网络编程中的线程模型》
《高性能网络编程(七):到底什么是高并发?一文即懂!》
《从根上了解高性能、高并发(一):深刻计算机底层,了解线程与线程池》
《从根上了解高性能、高并发(二):深刻操作系统,了解 I / O 与零拷贝技术》
《从根上了解高性能、高并发(三):深刻操作系统,彻底了解 I / O 多路复用》
《从根上了解高性能、高并发(四):深刻操作系统,彻底了解同步与异步》
《从根上了解高性能、高并发(五):深刻操作系统,了解高并发中的协程》
《从根上了解高性能、高并发(六):通俗易懂,高性能服务器到底是如何实现的》
《从根上了解高性能、高并发(七):深刻操作系统,一文读懂过程、线程、协程》
以下系列适宜挪动端资深网络通信开发者浏览:
《IM 开发者的零根底通信技术入门(一):通信替换技术的百年发展史(上)》
《IM 开发者的零根底通信技术入门(二):通信替换技术的百年发展史(下)》
《IM 开发者的零根底通信技术入门(三):国人通信形式的百年变迁》
《IM 开发者的零根底通信技术入门(四):手机的演进,史上最全挪动终端发展史》
《IM 开发者的零根底通信技术入门(五):1G 到 5G,30 年挪动通信技术演进史》
《IM 开发者的零根底通信技术入门(六):挪动终端的接头人——“基站”技术》
《IM 开发者的零根底通信技术入门(七):挪动终端的千里马——“电磁波”》
《IM 开发者的零根底通信技术入门(八):零根底,史上最强“天线”原理扫盲》
《IM 开发者的零根底通信技术入门(九):无线通信网络的中枢——“核心网”》
《IM 开发者的零根底通信技术入门(十):零根底,史上最强 5G 技术扫盲》
《IM 开发者的零根底通信技术入门(十一):为什么 WiFi 信号差?一文即懂!》
《IM 开发者的零根底通信技术入门(十二):上网卡顿?网络掉线?一文即懂!》
《IM 开发者的零根底通信技术入门(十三):为什么手机信号差?一文即懂!》
《IM 开发者的零根底通信技术入门(十四):高铁上无线上网有多难?一文即懂!》
《IM 开发者的零根底通信技术入门(十五):了解定位技术,一篇就够》
本文已同步公布于“即时通讯技术圈”公众号。
▲ 本文在公众号上的链接是:点此进入。同步公布链接是:http://www.52im.net/thread-3600-1-1.html