从Linux源码看TIME_WAIT状态的持续时间

从Linux源码看TIME_WAIT状态的持续时间

前言

笔者一直以为在Linux下TIME_WAIT状态的Socket持续状态是60s左右。线上实际却存在TIME_WAIT超过100s的Socket。由于这牵涉到最近出现的一个复杂Bug的分析。所以,笔者就去Linux源码里面,一探究竟。

首先介绍下Linux环境

TIME_WAIT这个参数通常和五元组重用扯上关系。在这里,笔者先给出机器的内核参数设置,以免和其它问题相混淆。

cat /proc/sys/net/ipv4/tcp_tw_reuse 0
cat /proc/sys/net/ipv4/tcp_tw_recycle 0
cat /proc/sys/net/ipv4/tcp_timestamps 1

可以看到,我们设置了tcp_tw_recycle为0,这可以避免NAT下tcp_tw_recycle和tcp_timestamps同时开启导致的问题。具体问题可以看笔者的以往博客。

https://my.oschina.net/alchemystar/blog/3119992

TIME_WAIT状态转移图

提到Socket的TIME_WAIT状态,不得就不亮出TCP状态转移图了: 
持续时间就如图中所示的2MSL。但图中并没有指出2MSL到底是多长时间,但笔者从Linux源码里面翻到了下面这个宏定义。

#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT
				  * state, about 60 seconds	*/

如英文字面意思所示,60s后销毁TIME_WAIT状态,那么2MSL肯定就是60s喽?

持续时间真如TCP_TIMEWAIT_LEN所定义么?

笔者之前一直是相信60秒TIME_WAIT状态的socket就能够被Kernel回收的。甚至笔者自己做实验telnet一个端口号,人为制造TIME_WAIT,自己计时,也是60s左右即可回收。 
但在追查一个问题时候,发现,TIME_WAIT有时候能够持续到111s,不然完全无法解释问题的现象。这就逼得笔者不得不推翻自己的结论,重新细细阅读内核对于TIME_WAIT状态处理的源码。当然,这个追查的问题也会写成博客分享出来,敬请期待^_^。

TIME_WAIT定时器源码

谈到TIME_WAIT何时能够被回收,不得不谈到TIME_WAIT定时器,这个就是专门用来销毁到期的TIME_WAIT Socket的。而每一个Socket进入TIME_WAIT时,必然会经过下面的代码分支:

tcp_v4_rcv
	|->tcp_timewait_state_process
		/* 将time_wait状态的socket链入时间轮
		|->inet_twsk_schedule

由于我们的kernel并没有开启tcp_tw_recycle,所以最终的调用为:

/* 这边TCP_TIMEWAIT_LEN 60 * HZ */
inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN,
					 TCP_TIMEWAIT_LEN);

好了,让我们按下这个核心函数吧。

inet_twsk_schedule

在阅读源码前,先看下大致的处理流程。Linux内核是通过时间轮来处理到期的TIME_WAIT socket,如下图所示: 
内核将60s的时间分为8个slot(INET_TWDR_RECYCLE_SLOTS),每个slot处理7.5(60/8)范围

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是Linux系统中的ping命令的源代码: ``` #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <signal.h> #include <errno.h> #include <sys/time.h> #include <sys/socket.h> #include <sys/types.h> #include <arpa/inet.h> #include <netdb.h> #include <netinet/ip.h> #include <netinet/ip_icmp.h> #define PACKET_SIZE 4096 #define MAX_WAIT_TIME 5 #define MAX_NO_PACKETS 3 char sendpacket[PACKET_SIZE]; char recvpacket[PACKET_SIZE]; int sockfd, datalen = 56; int nsend = 0, nreceived = 0; struct sockaddr_in dest_addr; pid_t pid; struct timeval tvrecv; void statistics(int signo); unsigned short cal_chksum(unsigned short *addr, int len); int pack(int pack_no); void send_packet(); void recv_packet(); void tv_sub(struct timeval *out, struct timeval *in); void sig_int(int signo); int main(int argc, char *argv[]) { struct hostent *host; struct protoent *protocol; unsigned long inaddr = 0l; int size = 50 * 1024; if (argc < 2) { printf("usage: %s hostname/IP address\n", argv[0]); exit(1); } if ((protocol = getprotobyname("icmp")) == NULL) { perror("getprotobyname"); exit(1); } if ((sockfd = socket(AF_INET, SOCK_RAW, protocol->p_proto)) < 0) { perror("socket error"); exit(1); } setuid(getuid()); setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size)); bzero(&dest_addr, sizeof(dest_addr)); dest_addr.sin_family = AF_INET; if ((inaddr = inet_addr(argv[1])) == INADDR_NONE) { if ((host = gethostbyname(argv[1])) == NULL) { perror("gethostbyname error"); exit(1); } memcpy((char *)&dest_addr.sin_addr, host->h_addr, host->h_length); } else { dest_addr.sin_addr.s_addr = inaddr; } pid = getpid(); signal(SIGALRM, statistics); signal(SIGINT, sig_int); printf("PING %s (%s): %d data bytes\n", argv[1], inet_ntoa(dest_addr.sin_addr), datalen); send_packet(); recv_packet(); return 0; } void statistics(int signo) { printf("\n%d packets transmitted, %d received, %%%d lost\n", nsend, nreceived, (nsend - nreceived) / nsend * 100); close(sockfd); exit(0); } unsigned short cal_chksum(unsigned short *addr, int len) { int nleft = len; int sum = 0; unsigned short *w = addr; unsigned short answer = 0; while (nleft > 1) { sum += *w++; nleft -= 2; } if (nleft == 1) { *(unsigned char *)(&answer) = *(unsigned char *)w; sum += answer; } sum = (sum >> 16) + (sum & 0xffff); sum += (sum >> 16); answer = ~sum; return answer; } int pack(int pack_no) { int packsize; struct icmp *icmp; struct timeval *tval; icmp = (struct icmp *)sendpacket; icmp->icmp_type = ICMP_ECHO; icmp->icmp_code = 0; icmp->icmp_cksum = 0; icmp->icmp_seq = pack_no; icmp->icmp_id = pid; packsize = 8 + datalen; tval = (struct timeval *)icmp->icmp_data; gettimeofday(tval, NULL); icmp->icmp_cksum = cal_chksum((unsigned short *)icmp, packsize); return packsize; } void send_packet() { int packetsize; while (nsend < MAX_NO_PACKETS) { nsend++; packetsize = pack(nsend); if (sendto(sockfd, sendpacket, packetsize, 0, (struct sockaddr *)&dest_addr, sizeof(dest_addr)) < 0) { perror("sendto error"); continue; } sleep(1); } } void recv_packet() { int n, fromlen; extern int errno; struct sockaddr_in from; fromlen = sizeof(from); while (nreceived < nsend) { alarm(MAX_WAIT_TIME); if ((n = recvfrom(sockfd, recvpacket, sizeof(recvpacket), 0, (struct sockaddr *)&from, &fromlen)) < 0) { if (errno == EINTR) { continue; } perror("recvfrom error"); continue; } gettimeofday(&tvrecv, NULL); recv_packet(); } } void tv_sub(struct timeval *out, struct timeval *in) { if ((out->tv_usec -= in->tv_usec) < 0) { --out->tv_sec; out->tv_usec += 1000000; } out->tv_sec -= in->tv_sec; } void sig_int(int signo) { statistics(signo); } ``` 这是一个简单的程序,使用了原始套接字来发送和接收 ICMP 报文,实现了 ping 命令的功能。它通过发送 ICMP ECHO 请求报文,然后等待接收 ICMP ECHO 应答报文来判断目标主机的状态。其中,变量 nsend 表示已发送的报文数量,变量 nreceived 表示已接收的报文数量,变量 sockfd 表示套接字描述符,变量 dest_addr 表示目标主机的地址信息,变量 datalen 表示 ICMP 数据负载的长度,变量 pid 表示进程 ID。函数 pack 是用来构造 ICMP 报文,函数 send_packet 是用来发送报文,函数 recv_packet 是用来接收报文,函数 tv_sub 是用来计算时间差的。当用户按下 Ctrl+C 组合键时,程序会调用函数 sig_int 来输出统计信息并退出。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值