一、为什么会想到这个问题
主要是想测试下当接收方接收窗口满了之后,此时发送的检测包报文的格式。然后就想到了一个极端的问题:当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("
Usage: %s <ip of server>
",argv[0]);
return 1;
}
memset(recvBuff, '0',sizeof(recvBuff));
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
printf("
Error : Could not create socket
");
return 1;
}
/*
int iset = 1;设置linger2时间为1,从而便于快速释放本地port
iset = setsockopt(sockfd, SOL_TCP, TCP_LINGER2, &iset,sizeof(iset));
printf("iset %d
", 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("
inet_pton error occured
");
return 1;
}
if( connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
{
printf("
Error : Connect Failed
");
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..@.@.l............[..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..@.@.l............[..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);
}