本文作者張彥飛,原題“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