一、爲什麼會想到這個問題
主要是想測試下當接收方接收窗口滿了之後,此時發送的檢測包報文的格式。然後就想到了一個極端的問題:當tcp連接建立起來之後,假設說一方比較缺德(或者說程序有bug),對建立的socket數據不做任何讀取操作,這樣就讓發送方非常尷尬了,因爲發送方終究會感知到對方的接收窗口已經滿了,並且自覺的不再發送數據給對端。
但是既然接收端非常的極端,發送方也也可以任性一點,比方說強行關掉這個socket,或者說直接退出進程(由操作系統來close這個socket)。此時整個底層的流程如何執行?這裏要注意的是,此時接收方的window是滿的,所以發送方按照約定是不能給對方發送數據的,包括這個十萬火急的FIN字段。
二、客戶端的write何時阻塞
對於發送端來說,它需要考慮兩個問題:一個是這個socket可以使用的系統緩衝區的大小,由於對方在接收窗口滿了之後不再給發送方數據以響應,如果發送方繼續向socket中寫入數據,最終會導致socket的發送緩衝區被耗盡,進而阻塞。那麼當發送端判斷對方的接收window已經滿了之後,此時socket的write系統調用會阻塞嗎?
這個問題其實顯而易見,不應該阻塞。當用戶態向socket write數據的時候,此時只要操作系統能夠爲這個socket分配一個skbuff並把用戶態的數據複製到skbuff中,此時write系統調用就可以返回,不會出現阻塞的情況。具體代碼在
tcp_sendmsg
……
new_segment:
/* Allocate new segment. If the interface is SG,
* allocate skb fitting to single page.
*/
if (!sk_stream_memory_free(sk))
goto wait_for_sndbuf;
……
wait_for_sndbuf:
set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);
wait_for_memory:
if (copied)
tcp_push(sk, tp, flags & ~MSG_MORE, mss_now, TCP_NAGLE_PUSH);
if ((err = sk_stream_wait_memory(sk, &timeo)) != 0)
goto do_error;
在這個過程中,還根本沒有考慮到任何TCP中所謂的窗口的概念,這裏主要處理的還是發送緩衝區的問題,主要允許並可以爲這次發送分配skbuff則繼續執行,否則阻塞等待。當整個skbuff放在了發送隊列之後,纔會檢測TCP協議中相關的流程。此時就進入了tcp_push--->>__tcp_push_pending_frames--->>tcp_write_xmit
while ((skb = sk->sk_send_head)) {
unsigned int limit;
tso_segs = tcp_init_tso_segs(sk, skb, mss_now);
BUG_ON(!tso_segs);
cwnd_quota = tcp_cwnd_test(tp, skb);
if (!cwnd_quota)
break;
……
在函數tcp_cwnd_test中檢測了對方的接收窗口並進行流量控制:
三、當擁塞之後發送端close socket時系統如何表現
假設說發送端一直髮送,接收方始終不作任何接收,直到把發送方感知到對方的接收緩衝區滿,並自動阻塞發送。此時發送端關閉socket,也就是需要把一個FIN發送給對方,注意:此時的接收端接收窗口爲0,發送端是不能發送任何數據的,是否對這個FIN網開一面呢?
tcp_close--->>tcp_send_fin(sk)
struct sk_buff *skb = skb_peek_tail(&sk->sk_write_queue);
……
if (sk->sk_send_head != NULL) {
TCP_SKB_CB(skb)->flags |= TCPCB_FLAG_FIN;
TCP_SKB_CB(skb)->end_seq++;
tp->write_seq++;
} else {
……
}
__tcp_push_pending_frames(sk, tp, mss_now, TCP_NAGLE_OFF);
在函數中,如果發送隊列中有數據,會把這個FIN標誌位追加在“skb_peek_tail(&sk->sk_write_queue)”中,也就是發送隊列中最後一個發送報文的文件頭中(這裏還有一個細節:FIN標誌位也佔用一個發送byte,雖然它只是佔用了一個BIT)。追加在這個發送隊列的最後__tcp_push_pending_frames--->>tcp_write_xmit--->>>tcp_cwnd_test
/* Don't be strict about the congestion window for the final FIN. */
if ((TCP_SKB_CB(skb)->flags & TCPCB_FLAG_FIN) &&
tcp_skb_pcount(skb) == 1)
return 1;
雖然這個地方對於這個FIN包做了特殊處理,但是由於FIN是追加在隊列的最後一個數據包上的,所以並不能啓動這個特權,依然不會給對方發送任何數據。
四、寫個代碼測試下
tsecer@harry: cat server.cpp
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <time.h>
int main(int argc, char *argv[])
{
int listenfd = 0, connfd = 0;
struct sockaddr_in serv_addr;
char sendBuff[1025];
time_t ticks;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
memset(&serv_addr, '0', sizeof(serv_addr));
memset(sendBuff, '0', sizeof(sendBuff));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(5555);
bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
listen(listenfd, 10);
while(1)
{
connfd = accept(listenfd, (struct sockaddr*)NULL, NULL);
}
}
tsecer@harry: cat client.cpp
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h>
#include <netinet/tcp.h>
int main(int argc, char *argv[])
{
int sockfd = 0, n = 0;
char recvBuff[1024];
struct sockaddr_in serv_addr;
if(argc != 2)
{
printf("\n Usage: %s <ip of server> \n",argv[0]);
return 1;
}
memset(recvBuff, '0',sizeof(recvBuff));
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
printf("\n Error : Could not create socket \n");
return 1;
}
/*
int iset = 1;設置linger2時間爲1,從而便於快速釋放本地port
iset = setsockopt(sockfd, SOL_TCP, TCP_LINGER2, &iset,sizeof(iset));
printf("iset %d\n", iset);
*/
memset(&serv_addr, '0', sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(5555);
if(inet_pton(AF_INET, argv[1], &serv_addr.sin_addr)<=0)
{
printf("\n inet_pton error occured\n");
return 1;
}
if( connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
{
printf("\n Error : Connect Failed \n");
return 1;
}
char buff[] = "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH";
while (1)
write(sockfd, (void*)buff, sizeof(buff));
return 0;
}
tsecer@harry:
編譯出各自的二進制之後,客戶端通過本機的127.0.0.1地址連接到服務器上(這樣便於抓包時指定特殊的loopbback網絡設備)。在客戶端把接收端窗口撐滿之後,通過ctrl+c來關閉客戶端,之後查看系統socket狀態
tsecer@harry: netstat -anp | grep 5555
tcp 0 0 0.0.0.0:5555 0.0.0.0:* LISTEN 1945/server
tcp 71213 0 127.0.0.1:5555 127.0.0.1:40539 ESTABLISHED 1945/server
tcp 0 306433 127.0.0.1:40539 127.0.0.1:5555 FIN_WAIT1 -
tsecer@harry:
可以看到這個5555端口的socket已經不屬於任何進程,並且狀態處於FIN_WAIT1,這個狀態會持續多久呢?只要接收端進程依然存在則這個socket就一直存在,並且依然是FIN_WAIT1。
我們再通過tcpdump抓包看下這些0窗口probe的數據包:
tsecer@harry: tcpdump -ni lo port 5555 -Ss0 -Avv
tcpdump: listening on lo, link-type EN10MB (Ethernet), capture size 65535 bytes
17:53:38.318745 IP (tos 0x0, ttl 64, id 12190, offset 0, flags [DF], proto: TCP (6), length: 52) 127.0.0.1.40539 > 127.0.0.1.5555: ., cksum 0x1411 (correct), 635682346:635682346(0) ack 2781628213 win 257 <nop,nop,timestamp 904950765
904920765>
$.........[..%..*..G5...........
5.s.5...
17:53:38.319016 IP (tos 0x0, ttl 64, id 53200, offset 0, flags [DF], proto: TCP (6), length: 52) 127.0.0.1.5555 > 127.0.0.1.40539: ., cksum 0x8aec (correct), 2781628213:2781628213(0) ack 635682347 win 0 <nop,nop,timestamp 904950765
903907569>
E..4..@[email protected]............[..G5%..+...........
5.s.5...
17:55:38.318701 IP (tos 0x0, ttl 64, id 12191, offset 0, flags [DF], proto: TCP (6), length: 52) 127.0.0.1.40539 > 127.0.0.1.5555: ., cksum 0x29b0 (correct), 635682346:635682346(0) ack 2781628213 win 257 <nop,nop,timestamp 904980765
904950765>
#.........[..%..*..G5....)......
5...5.s.
17:55:38.318716 IP (tos 0x0, ttl 64, id 53201, offset 0, flags [DF], proto: TCP (6), length: 52) 127.0.0.1.5555 > 127.0.0.1.40539: ., cksum 0x15bc (correct), 2781628213:2781628213(0) ack 635682347 win 0 <nop,nop,timestamp 904980765
903907569>
E..4..@[email protected]............[..G5%..+...........
5...5...
這裏可以看到,系統中對於0window的探測大概是以2分鐘爲步長進行探測,探測時探測包的seq爲已確認seq-1,這一點從回包中可以看到,發送方的數據包的開始和結束序列號爲635682346:635682346(0),而對方給出的對方的確認報爲ack 635682347。這從另一個側面說明了probe0的實現機制。
這個2分鐘的由來:
void tcp_send_probe0(struct sock *sk)
inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0,
min(icsk->icsk_rto << icsk->icsk_backoff, TCP_RTO_MAX),
TCP_RTO_MAX);
#define TCP_RTO_MAX
((unsigned)(120*HZ))
也就是120秒。由於我是隔了很久纔開始抓包的,所以探測間隔已經修改爲最大值2分鐘,在開始的時候,按照指數退讓的執行邏輯,開始的探測間隔應該會更快一些。
五、再極端一步
假設說發送和接收兩端都是愣頭青,大家都只攻不守,都是一直向socket中write數據並且不read數據,那麼會不會兩端socket就這麼死鎖在這裏嗎?同樣是在tcp_close函數
……
/* We need to flush the recv. buffs. We do this only on the
* descriptor close, not protocol-sourced closes, because the
* reader process may not have drained the data yet!
*/
while ((skb = __skb_dequeue(&sk->sk_receive_queue)) != NULL) {
u32 len = TCP_SKB_CB(skb)->end_seq - TCP_SKB_CB(skb)->seq -
skb->h.th->fin;
data_was_unread += len;
__kfree_skb(skb);
}
……
/* As outlined in draft-ietf-tcpimpl-prob-03.txt, section
* 3.10, we send a RST here because data was lost. To
* witness the awful effects of the old behavior of always
* doing a FIN, run an older 2.1.x kernel or 2.0.x, start
* a bulk GET in an FTP client, suspend the process, wait
* for the client to advertise a zero window, then kill -9
* the FTP client, wheee... Note: timeout is always zero
* in such a case.
*/
if (data_was_unread) {
/* Unread data was tossed, zap the connection. */
NET_INC_STATS_USER(LINUX_MIB_TCPABORTONCLOSE);
tcp_set_state(sk, TCP_CLOSE);
tcp_send_active_reset(sk, GFP_KERNEL);
}
當出現在這種情況的時候,後關閉的一方必然會檢測到自己的接收緩衝區中有數據還沒有被用戶態讀取到,此時操作系統就採用了暴力的做法,直接向對方發送了一個reset,導致對方連接流產,所以不存在死鎖的問題。
六、回頭再看下reset的發送
這個的發送乾脆利落,不管三七二十一,自己直接單獨申請一個skbuff。如果這個至關重要的skbuff分配失敗怎麼辦呢?呵呵,記錄個日誌就行了。
void tcp_send_active_reset(struct sock *sk, gfp_t priority)
{
struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *skb;
/* NOTE: No TCP options attached and we never retransmit this. */
skb = alloc_skb(MAX_TCP_HEADER, priority);
if (!skb) {
NET_INC_STATS(LINUX_MIB_TCPABORTFAILED);
return;
}
……
if (tcp_transmit_skb(sk, skb, 0, priority))
NET_INC_STATS(LINUX_MIB_TCPABORTFAILED);
}