作者:小林coding
圖解計算機基礎網站:https://xiaolincoding.com
大家好,我是小林。
週末的時候,有位讀者疑惑爲什麼 Linux man 手冊中關於 netstat
命令中的 tcp listen 狀態下的 Recv-Q 和 Send-Q 這兩個信息的描述跟我的圖解網絡寫的不一樣?
我看了源碼後,確認了這個 man 手冊寫的不對。沒想到 Linux 的 man 手冊也會出錯。
首先,先給大家介紹下 netstat 命令。netstat 命令是查看網絡狀態很常見的 Linux 命令。
比如,如果我們想查看系統中的進程監聽了哪些 TCP 端口,則可以使用下面這個命令:
接下來,小林帶大家分析,爲什麼我說 man 手冊寫錯了 netstat 命令中 Recv-Q 和 Send-Q 的描述?
疑惑提出
讀者提出的疑惑: 我先給大家翻譯一下,man 手冊(https://man7.org/linux/man-pages/man8/netstat.8.html)是怎麼說的:
- Recv-Q:如果 TCP 連接狀態處於 Established,Recv-Q 的數值表示接收緩衝區中還沒拷貝到應用層的數據大小;如果 TCP 連接狀態處於 Listen 狀態,Recv-Q 的數值表示當前 syn 半連接隊列的大小(自內核版本 2.6.18 起)
- Send-Q:如果 TCP 連接狀態處於 Established,Send-Q的數值表示發送緩衝區中已發送但未被確認的數據大小;如果 TCP 連接狀態處於 Listen 狀態,Send-Q 的數值表示 syn 半連接隊列的容量(自內核版本 2.6.18 起)。
而我通過查閱內核 2.6.18 版本的源碼,得到的結論如下:
- Recv-Q:如果 TCP 連接狀態處於 Established,Recv-Q 的數值表示接收緩衝區中還沒拷貝到應用層的數據大小;如果 TCP 連接狀態處於 Listen 狀態,Recv-Q 的數值表示
當前 syn 半連接隊列的大小當前全連接隊列的大小; - Send-Q:如果 TCP 連接狀態處於 Established,Send-Q的數值表示發送緩衝區中已發送但未被確認的數據大小;
如果 TCP 連接狀態處於 Listen 狀態,Send-Q 的數值表示 syn 半連接隊列的容量
上面被我劃掉的部分,就是我與 man 手冊差異的地方。
什麼是 TCP 半連接隊列和全鏈接隊列?
在 TCP 三次握手的時候,Linux 內核會維護兩個隊列,分別是:
- 半連接隊列,也稱 SYN 隊列;
- 全連接隊列,也稱 accept 隊列;
服務端收到客戶端發起的 SYN 請求後,內核會把該連接存儲到半連接隊列,並向客戶端響應 SYN+ACK,接着客戶端會返回 ACK,服務端收到第三次握手的 ACK 後,內核會把連接從半連接隊列移除,然後創建新的完全的連接,並將其添加到全連接隊列,等待進程調用 accept 函數時把連接取出來。
如果你想知道 TCP 半連接和全連接溢出會發生什麼?可以看看這篇文章:TCP 半連接隊列和全連接隊列滿了會發生什麼?又該如何應對?
源碼分析
netstat 工具在獲取 TCP 連接的信息的時候,實際上是讀取了 /proc/net/tcp
文件裏的數據,而這個文件的數據是由內核由 net/ipv4/tcp_ipv4.c 文件中的 tcp4_seq_show()
函數打印的。
所以,我們直接看 tcp4_seq_show() 函數是根據什麼信息打印出 Recv-Q 和 Send-Q 的數據。
有一個網站可以在線看 Linux 內核代碼:https://elixir.bootlin.com/,每個內核版本的代碼都有,平常我都是在這裏看。
這次,我們選擇內核版本爲 2.6.18 查看 tcp4_seq_show() 函數的實現,如下:
static int tcp4_seq_show(struct seq_file *seq, void *v)
{
.....
switch (st->state) {
case TCP_SEQ_STATE_LISTENING:
case TCP_SEQ_STATE_ESTABLISHED:
get_tcp4_sock(v, tmpbuf, st->num);
break;
.......
}
...
return 0;
}
我們只分析 tcp 連接狀態爲 ESTABLISHED 和 LISTENING 時打印的信息,所以接下來看 get_tcp4_sock
函數。
get_tcp4_sock 函數中,打印信息的代碼如下: 我在圖中標紅了兩行代碼,這兩行代碼分別是 Recv-Q 和 Send-Q 的數據。
我單獨把這兩行代碼抽了出來:
// Send-Q 打印的數據
tp->write_seq - tp->snd_una,
//Recv-Q 打印的數據
(sp->sk_state == TCP_LISTEN) ? sp->sk_ack_backlog : (tp->rcv_nxt - tp->copied_seq),
可以看到, 不管 TCP 連接狀態是什麼, Send-Q 都是發送緩衝區中已發送但未被確認的數據大小。
然後針對 Recv-Q ,在 TCP 連接狀態爲 LISTEN 時,打印的是 sk_ack_backlog 的值。
那 sk_ack_backlog 的值代表什麼意思呢?
下面這個是判斷全連接隊列是否溢出的函數: 可以得知,sk_ack_backlog 其實是當前全連接隊列的大小,也就是經歷三次握手後等待被應用層 accpet() 的連接的數量。
所以,從上面的源碼分析過,得到的結論如下:
- netstat 命令中的 Recv-Q:如果 TCP 連接狀態處於 Established,Recv-Q 的數值表示接收緩衝區中還沒拷貝到應用層的數據大小;如果 TCP 連接狀態處於 Listen 狀態,Recv-Q 的數值表示當前全連接隊列的大小;
- netstat 命令中的 Send-Q:表示發送緩衝區中已發送但未被確認的數據大小(不管 TCP 是 Listen 狀態還是 Established 狀態都表示這個意思);
好了,至此就分析完了。
最後
看到這,大家肯定會說:小林你太強了吧,爲什麼對 Linux 內核源碼那麼熟,這都能分析出來。
其實,我並沒有熟讀過 Linux 內核源碼啦,其實只要大家有好奇心,其實你也能分析出來。
我也是通過網上的資料,一點一點分析出來的,並不是直接就在內核源碼裏查,不然那真是大海撈針。
我是這樣一步一步查資料分析的:
- 先網上查下 netstat 源碼,看是根據什麼信息打印 Send-Q 和 Recv-Q,然後看到網上有人說是讀 /proc/net/tcp 這個文件;
- 接着,就網上查 /proc/net/tcp 這個文件是怎麼打印的,然後看到網上有人說是由 net/ipv4/tcp_ipv4.c 文件中的 tcp4_seq_show() 函數打印的;
- 最後,再自己去看 tcp4_seq_show 函數的實現,這個函數的代碼也不多,就幾十行,所以很容易就分析出來了。
你看,其實我也是通過「搜索」一步一步分析出來的,其實並沒有什麼難度。
只是我比較細節一點。