本文做者張彥飛,原題「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問:緩存
上面這幾個問題,相信包括即時通信老鳥們在內,都是看似很熟悉,但實則仍然沒法透徹講清楚的話題。此次,咱們就來完全搞清楚!markdown
本文是系列文章中的第13篇,本系列文章的大綱以下:網絡
《鮮爲人知的網絡編程(一):淺析TCP協議中的疑難雜症(上篇)》負載均衡
《鮮爲人知的網絡編程(二):淺析TCP協議中的疑難雜症(下篇)》socket
《鮮爲人知的網絡編程(三):關閉TCP鏈接時爲何會TIME_WAIT、CLOSE_WAIT》ide
《鮮爲人知的網絡編程(七):如何讓不可靠的UDP變的可靠?》
《鮮爲人知的網絡編程(九):理論聯繫實際,全方位深刻理解DNS》
《鮮爲人知的網絡編程(十):深刻操做系統,從內核理解網絡包的接收過程(Linux篇)》
《鮮爲人知的網絡編程(十一):從底層入手,深度分析TCP鏈接耗時的祕密》
在開始講述本機通訊過程以前,咱們先看看跨機網絡通訊(以Linux系統內核中的實現爲例來說解)。
從 send 系統調用開始,直到網卡把數據發送出去,總體流程以下:
在上面這幅圖中,咱們看到用戶數據被拷貝到內核態,而後通過協議棧處理後進入到了 RingBuffer 中。隨後網卡驅動真正將數據發送了出去。當發送完成的時候,是經過硬中斷來通知 CPU,而後清理 RingBuffer。
不過上面這幅圖並無很好地把內核組件和源碼展現出來,咱們再從代碼的視角看一遍。
等網絡發送完畢以後。網卡在發送完畢的時候,會給 CPU 發送一個硬中斷來通知 CPU。收到這個硬中斷後會釋放 RingBuffer 中使用的內存。
當數據包到達另一臺機器的時候,Linux 數據包的接收過程開始了(更詳細的講解能夠看看《深刻操做系統,從內核理解網絡包的接收過程(Linux篇)》)。
▲ 上圖引用自《深刻操做系統,從內核理解網絡包的接收過程(Linux篇)》
當網卡收到數據之後,CPU發起一箇中斷,以通知 CPU 有數據到達。當CPU收到中斷請求後,會去調用網絡驅動註冊的中斷處理函數,觸發軟中斷。ksoftirqd 檢測到有軟中斷請求到達,開始輪詢收包,收到後交由各級協議棧處理。當協議棧處理完並把數據放到接收隊列的以後,喚醒用戶進程(假設是阻塞方式)。
咱們再一樣從內核組件和源碼視角看一遍。
關於跨機網絡通訊的理解,能夠通俗地用下面這張圖來總結一下:
在上一節中,咱們看到了跨機時整個網絡數據的發送過程 。
在本機網絡 IO 的過程當中,流程會有一些差異。爲了突出重點,本節將再也不介紹總體流程,而是隻介紹和跨機邏輯不一樣的地方。有差別的地方總共有兩個,分別是路由和驅動程序。
發送數據會進入協議棧到網絡層的時候,網絡層入口函數是 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)。
網絡設備子系統的入口函數是 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);
...
}
對於真實的 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);
}
只有觸發完軟中斷,發送過程就算是完成了。
在跨機的網絡包的接收過程當中,須要通過硬中斷,而後才能觸發軟中斷。
而在本機的網絡 IO 過程當中,因爲並不真的過網卡,因此網卡實際傳輸,硬中斷就都省去了。直接從軟中斷開始,通過 process_backlog 後送進協議棧,大致過程以下圖。
接下來咱們再看更詳細一點的過程。
在軟中斷被觸發之後,會進入到 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…)