[转]linux下tcp选项TCP_DEFER_ACCEPT详解

原文连接:http://www.pagefault.info/?p=346服务器

TCP_DEFER_ACCEPT这个选项可能你们都知道,不过我这里会从源码和数据包来详细的分析这个选项。要注意,这里我所使用的内核版本是3.0.socket

首先看man手册中的介绍(man 7 tcp):tcp

TCP_DEFER_ACCEPT (since Linux 2.4)
Allow a listener to be awakened only when data arrives on the socket. Takes an integer value (seconds), this can bound the maximum number of attempts TCP will make to complete the connection. This option should not be used in code intended to be portable.函数

我先来简单介绍下,这个选项主要是针对server端的服务器,通常来讲咱们三次握手,当客户端发送syn,而后server端接收到,而后发送 syn + ack,而后client接收到syn+ack以后,再次发送ack(client进入establish状态),最终server端收到最后一个 ack,进入establish状态。this

而当正确的设置了TCP_DEFER_ACCEPT选项以后,server端会在接收到最后一个ack以后,并不进入establish状态,而只 是将这个socket标记为acked,而后丢掉这个ack。此时server端这个socket仍是处于syn_recved,而后接下来就是等待 client发送数据, 而因为这个socket仍是处于syn_recved,所以此时就会被syn_ack定时器所控制,对syn ack进行重传,而重传次数是由咱们设置TCP_DEFER_ACCEPT传进去的值以及TCP_SYNCNT选项,proc文件系统的 tcp_synack_retries一块儿来决定的(后面分析源码会看到如何来计算这个值).而咱们知道咱们传递给TCP_DEFER_ACCEPT的是 秒,而在内核里面会将这个东西转换为重传次数.

咱们来看抓包,这里server端设置deffer accept,而后客户端connect并不发送数据,咱们来看会发生什么:spa

//客户端发送syn
19:38:20.631611 IP T-diaoliang.60277 > T-diaoliang.sunproxyadmin: Flags [S], seq 2500439144, win 32792, options [mss 16396,sackOK,TS val 9008384 ecr 0,nop,wscale 4], length 0
//server回了syn+ack
19:38:20.631622 IP T-diaoliang.sunproxyadmin > T-diaoliang.60277: Flags [S.], seq 1342179593, ack 2500439145, win 32768, options [mss 16396,sackOK,TS val 9008384 ecr 9008384,nop,wscale 4], length 0

//client发送最后一个ack
19:38:20.631629 IP T-diaoliang.60277 > T-diaoliang.sunproxyadmin: Flags [.], ack 1, win 2050, options [nop,nop,TS val 9008384 ecr 9008384], length 0

//这里注意时间,能够看到过了大概1分半以后,server从新发送了syn+ack
19:39:55.035893 IP T-diaoliang.sunproxyadmin > T-diaoliang.60277: Flags [S.], seq 1342179593, ack 2500439145, win 32768, options [mss 16396,sackOK,TS val 9036706 ecr 9008384,nop,wscale 4], length 0
19:39:55.035899 IP T-diaoliang.60277 > T-diaoliang.sunproxyadmin: Flags [.], ack 1, win 2050, options [nop,nop,TS val 9036706 ecr 9036706,nop,nop,sack 1 {0:1}], length 0

//再过了1分钟,server close掉这条链接。
19:40:55.063435 IP T-diaoliang.sunproxyadmin > T-diaoliang.60277: Flags [F.], seq 1, ack 1, win 2048, options [nop,nop,TS val 9054714 ecr 9036706], length 0

19:40:55.063692 IP T-diaoliang.60277 > T-diaoliang.sunproxyadmin: Flags [F.], seq 1, ack 2, win 2050, options [nop,nop,TS val 9054714 ecr 9054714], length 0

19:40:55.063701 IP T-diaoliang.sunproxyadmin > T-diaoliang.60277: Flags [.], ack 2, win 2048, options [nop,nop,TS val 9054714 ecr 9054714], length 0

接下来就来看内核的代码。code

先从设置TCP_DEFER_ACCEPT开始,设置TCP_DEFER_ACCEPT是经过setsockopt来作的,而传递给内核的值是秒, 下面就是内核中对应的do_tcp_setsockopt函数,它用来设置tcp相关的option,下面咱们能看到主要就是将传递进去的val转换为将 要重传的次数。server

case TCP_DEFER_ACCEPT:
		/* Translate value in seconds to number of retransmits */
//注意参数
		icsk->icsk_accept_queue.rskq_defer_accept =
			secs_to_retrans(val, TCP_TIMEOUT_INIT / HZ,
					TCP_RTO_MAX / HZ);
		break;

这里能够看到经过调用secs_to_retrans来将秒转换为重传次数。接下来就来看这个函数,它有三个参数,第一个是将要转换的秒,第二个是 RTO的初始值,第三个是RTO的最大值。 能够看到这里都是依据RTO来计算的,这是由于这个重传次数是syn_ack的重传次数。blog

这个函数实现很简单,就是一个定时器退避的计算过程(定时器退避能够看我前面的blog的介绍),每次乘2,而后来计算重传次数。队列

static u8 secs_to_retrans(int seconds, int timeout, int rto_max)
{
	u8 res = 0;

	if (seconds > 0) {
		int period = timeout;
//重传次数
		res = 1;
//开始遍历
		while (seconds > period && res < 255) {
			res++;
//定时器退避
			timeout <<= 1;
			if (timeout > rto_max)
				timeout = rto_max;
//定时器的秒数
			period += timeout;
		}
	}
	return res;
}

而后来看当server端接收到最后一个ack的处理,这里只关注defer_accept的部分,这个函数是tcp_check_req,它主要用来检测SYN_RECV状态接收到包的校验。

req->retrans表示已经重传的次数。
acked标记主要是为了syn_ack定时器来使用的。

//两个条件,一个是重传次数小于defer_accept,一个是序列号,这两个都必须知足。
	if (req->retrans < inet_csk(sk)->icsk_accept_queue.rskq_defer_accept &&
	    TCP_SKB_CB(skb)->end_seq == tcp_rsk(req)->rcv_isn + 1) {
//此时设置acked。
		inet_rsk(req)->acked = 1;
		NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPDEFERACCEPTDROP);
		return NULL;
	}

而当tcp_check_req返回以后,在tcp_v4_do_rcv中会丢掉这个包,让socket继续保存在半链接队列中。

而后来看syn ack定时器,这个定时器我之前有分析过(http://simohayha.iteye.com/admin/blogs/481989)
,所以我这里只是简要的再次分析下。若是须要更详细的分析,能够看我上面的连接,这个定时器会调用inet_csk_reqsk_queue_prune函数,在这个函数中作相关的处理。

这里咱们就主要关注重试次数。其中icsk_syn_retries是TCP_SYNCNT这个option设置的。这个值会比 sysctl_tcp_synack_retries优先.而后是rskq_defer_accept,它又比icsk_syn_retries优先.

void inet_csk_reqsk_queue_prune(struct sock *parent,
				const unsigned long interval,
				const unsigned long timeout,
				const unsigned long max_rto)
{
........................
//最大的重试次数
	int max_retries = icsk->icsk_syn_retries ? : sysctl_tcp_synack_retries;
	int thresh = max_retries;
	unsigned long now = jiffies;
	struct request_sock **reqp, *req;
	int i, budget;

....................................
//更新设置最大的重试次数。
	if (queue->rskq_defer_accept)
		max_retries = queue->rskq_defer_accept;

	budget = 2 * (lopt->nr_table_entries / (timeout / interval));
	i = lopt->clock_hand;

	do {
		reqp=&lopt->syn_table[i];
		while ((req = *reqp) != NULL) {
			if (time_after_eq(now, req->expires)) {
				int expire = 0, resend = 0;
//这个函数主要是判断超时和是否从新发送syn ack,而后保存在expire和resend这个变量中。
				syn_ack_recalc(req, thresh, max_retries,
					       queue->rskq_defer_accept,
					       &expire, &resend);
....................................................
				if (!expire &&
				    (!resend ||
				     !req->rsk_ops->rtx_syn_ack(parent, req, NULL) ||
				     inet_rsk(req)->acked)) {
					unsigned long timeo;
//更新重传次数.
					if (req->retrans++ == 0)
						lopt->qlen_young--;
					timeo = min((timeout << req->retrans), max_rto);
					req->expires = now + timeo;
					reqp = &req->dl_next;
					continue;
				}
//若是超时,则丢掉这个请求,并对应的关闭链接.
				/* Drop this request */
				inet_csk_reqsk_queue_unlink(parent, req, reqp);
				reqsk_queue_removed(queue, req);
				reqsk_free(req);
				continue;
			}
			reqp = &req->dl_next;
		}

		i = (i + 1) & (lopt->nr_table_entries - 1);

	} while (--budget > 0);
...............................................
}
相关文章
相关标签/搜索