kk Blog —— 通用基础


date [-d @int|str] [+%s|"+%F %T"]
netstat -ltunp
sar -n DEV 1

内核tcp的定时器管理

在内核中tcp协议栈有6种类型的定时器:
1
2
3
4
5
6
7
1 重传定时器。
2 delayed ack定时器
3 零窗口探测定时器
上面三种定时器都是作为tcp状态机的一部分来实现的。
4 keep-alive 定时器 主要是管理established状态的连接。
5 time_wait定时器 主要是用来客户端关闭时的time_wait状态用到。
6 syn-ack定时器(主要是用在listening socket) 管理新的连接请求时所用到。
而在内核中,tcp协议栈管理定时器主要有下面4个函数:
1
2
3
4
5
inet_csk_reset_xmit_timer    这个函数是用来重启定时器
inet_csk_clear_xmit_timer 这个函数用来删除定时器。
上面两个函数都是针对状态机里面的定时器。
tcp_set_keepalive 这个函数是用来管理keepalive 定时器的接口。
tcp_synack_timer  这个函数是用来管理syn_ack定时器的接口。
先来看定时器的初始化。

首先是在tcp_v4_init_sock中对定时器的初始化,它会调用tcp_init_xmit_timers,我们就先来看这个函数:

1
2
3
4
void tcp_init_xmit_timers(struct sock *sk)
{
	inet_csk_init_xmit_timers(sk, &tcp_write_timer, &tcp_delack_timer, &tcp_keepalive_timer);
}

可以看到这个函数很简单,就是调用inet_csk_init_xmit_timers,然后把3个定时器的回掉函数传递进去,下面我们来看inet_csk_init_xmit_timers。

1
2
3
4
5
6
7
8
9
10
11
12
13
void inet_csk_init_xmit_timers(struct sock *sk,
					void (*retransmit_handler)(unsigned long),
					void (*delack_handler)(unsigned long),
					void (*keepalive_handler)(unsigned long))
{
	struct inet_connection_sock *icsk = inet_csk(sk);

	//安装定时器,设置定时器的回掉函数。
	setup_timer(&icsk->icsk_retransmit_timer, retransmit_handler, (unsigned long)sk);
	setup_timer(&icsk->icsk_delack_timer, delack_handler, (unsigned long)sk);
	setup_timer(&sk->sk_timer, keepalive_handler, (unsigned long)sk);
	icsk->icsk_pending = icsk->icsk_ack.pending = 0;
}

我 们可以看到icsk->icsk_retransmit_timer定时器,也就是重传定时器的回调函数是tcp_write_timer,而 icsk->icsk_delack_timer定时器也就是delayed-ack 定时器的回调函数是tcp_delack_timer,最后sk->sk_timer也就是keepalive定时器的回掉函数是 tcp_keepalive_timer.
这里还有一个要注意的,tcp_write_timer还会处理0窗口定时器。
这里有关内核定时器的一些基础的东西我就不介绍了,想了解的可以去看下ldd第三版。
接下来我们就来一个个的分析这6个定时器,首先是重传定时器。
我们知道4层最终调用tcp_xmit_write来讲数据发送到3层,并且tcp是字节流的,因此每次他总是发送一段数据到3层,而每次当它发送完毕(返回正确),则它就会启动重传定时器,我们来看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static int tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,
			  int push_one, gfp_t gfp)
{
	struct tcp_sock *tp = tcp_sk(sk);
	struct sk_buff *skb;
	unsigned int tso_segs, sent_pkts;
	int cwnd_quota;
	int result;

.............................................

	while ((skb = tcp_send_head(sk))) {
..................................................

		//可以看到只有当传输成功,我们才会走到下面的函数。
		if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp)))
			break;

		/* Advance the send_head.  This one is sent out.
		 * This call will increment packets_out.
		 */
		//最终在这个函数中启动重传定时器。
		tcp_event_new_data_sent(sk, skb);

		tcp_minshall_update(tp, mss_now, skb);
		sent_pkts++;

		if (push_one)
			break;
	}
...........................
}

现在我们来看tcp_event_new_data_sent,如何启动定时器的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void tcp_event_new_data_sent(struct sock *sk, struct sk_buff *skb)
{
	struct tcp_sock *tp = tcp_sk(sk);
	unsigned int prior_packets = tp->packets_out;

	tcp_advance_send_head(sk, skb);
	tp->snd_nxt = TCP_SKB_CB(skb)->end_seq;

	/* Don't override Nagle indefinately with F-RTO */
	if (tp->frto_counter == 2)
		tp->frto_counter = 3;
	//关键在这里.
	tp->packets_out += tcp_skb_pcount(skb);
	if (!prior_packets)
		inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
}

可以看到只有当prior_packets为0时才会重启定时器,而prior_packets则是发送未确认的段的个数,也就是说如果发送了很多段,如果前面的段没有确认,那么后面发送的时候不会重启这个定时器.
我们要知道,定时器的间隔是通过rtt来得到的,具体的算法,可以看下tcp/ip详解。
当 启动了重传定时器,我们就会等待ack的到来,如果超时还没到来,那么就调用重传定时器的回调函数,否则最终会调用tcp_rearm_rto来删除或者 重启定时器,这个函数是在tcp_ack()->tcp_clean_rtx_queue()中被调用的。tcp_ack是专门用来处理ack。
这个函数很简单,就是通过判断packets_out,这个值表示当前还未确认的段的个数。然后来进行相关操作。

1
2
3
4
5
6
7
8
9
10
11
12
static void tcp_rearm_rto(struct sock *sk)
{
	struct tcp_sock *tp = tcp_sk(sk);

	//为0说明所有的传输的段都已经acked。此时remove定时器。否则重启定时器。
	if (!tp->packets_out) {
		inet_csk_clear_xmit_timer(sk, ICSK_TIME_RETRANS);
	} else {
		inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
					  inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
	}
}

接下来来看tcp_write_timer的实现。这个函数主要是通过icsk->icsk_pending来判断是那个定时器导致超时,这里只有两 种,一种是ICSK_TIME_RETRANS,也就是重传定时器,另一种是ICSK_TIME_PROBE0也就是0窗口定时器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#define ICSK_TIME_RETRANS   1   /* Retransmit timer */
#define ICSK_TIME_PROBE0    3   /* Zero window probe timer */
static void tcp_write_timer(unsigned long data)
{
	struct sock *sk = (struct sock *)data;
	struct inet_connection_sock *icsk = inet_csk(sk);
	int event;

	//首先加锁。
	bh_lock_sock(sk);
	//如果是进程空间则什么也不做。
	if (sock_owned_by_user(sk)) {
		/* Try again later */
		sk_reset_timer(sk, &icsk->icsk_retransmit_timer, jiffies + (HZ / 20));
		goto out_unlock;
	}

	//如果状态为close或者icsk_pending为空,则什么也不做。
	if (sk->sk_state == TCP_CLOSE || !icsk->icsk_pending)
		goto out;
	//如果超时时间已经过了,则重启定时器。

	if (time_after(icsk->icsk_timeout, jiffies)) {
		sk_reset_timer(sk, &icsk->icsk_retransmit_timer, icsk->icsk_timeout);
		goto out;
	}
	//取出定时器类型。
	event = icsk->icsk_pending;
	icsk->icsk_pending = 0;

	//通过判断event来确定进入那个函数进行处理。
	switch (event) {
	case ICSK_TIME_RETRANS:
		tcp_retransmit_timer(sk);
		break;
	case ICSK_TIME_PROBE0:
		tcp_probe_timer(sk);
		break;
	}
	TCP_CHECK_TIMER(sk);

out:
	sk_mem_reclaim(sk);
out_unlock:
	bh_unlock_sock(sk);
	sock_put(sk);
}

我们这里只看重传定时器,0窗口定时器后面紧接着会介绍。
tcp_retransmit_timer,这个函数用来处理数据段的重传。
这里要注意,重传的时候为了防止确认二义性,使用karn算法,也就是定时器退避策略。下面的代码最后部分会修改定时器的值,这里是增加一倍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
static void tcp_retransmit_timer(struct sock *sk)
{
	struct tcp_sock *tp = tcp_sk(sk);
	struct inet_connection_sock *icsk = inet_csk(sk);

	//如果没有需要确认的段,则什么也不做。
	if (!tp->packets_out)
		goto out;

	WARN_ON(tcp_write_queue_empty(sk));

	/**首先进行一些合法性判断,其中:
	 * snd_wnd为窗口大小。
	 * sock_flag用来判断sock的状态。
	 * 最后一个判断是当前的连接状态不能处于syn_sent和syn_recv状态,也就是连接还未建
	 * 立状态.
	if (!tp->snd_wnd && !sock_flag(sk, SOCK_DEAD) &&
		!((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV))) {
		//tcp_time_stamp也就是jifes,而rcv_tstamp表示最后一个ack接收的时间,也就是最后一次对端确认的时间。因此这两个时间之差不能大于tcp_rto_max,因为tcp_rto_max为我们重传定时器的间隔时间的最大值。
		if (tcp_time_stamp - tp->rcv_tstamp > TCP_RTO_MAX) {
			tcp_write_err(sk);
			goto out;
		}
		//这个函数用来进入loss状态,也就是进行一些拥塞以及流量的控制。
		tcp_enter_loss(sk, 0);
		//现在开始重传skb。
		tcp_retransmit_skb(sk, tcp_write_queue_head(sk));
		__sk_dst_reset(sk);
		//然后重启定时器,继续等待ack的到来。
		goto out_reset_timer;
	}

	//程序到达这里说明上面的校验失败,因此下面这个函数用来判断我们重传需要的次数。如果超过了重传次数,直接跳转到out。
	if (tcp_write_timeout(sk))
		goto out;

	//到达这里说明我们重传的次数还没到。icsk->icsk_retransmits表示重传的次数。
	if (icsk->icsk_retransmits == 0) {
		//这里其实也就是收集一些统计信息。
		int mib_idx;

		if (icsk->icsk_ca_state == TCP_CA_Disorder) {
			if (tcp_is_sack(tp))
				mib_idx = LINUX_MIB_TCPSACKFAILURES;
			else
				mib_idx = LINUX_MIB_TCPRENOFAILURES;
		} else if (icsk->icsk_ca_state == TCP_CA_Recovery) {
			if (tcp_is_sack(tp))
				mib_idx = LINUX_MIB_TCPSACKRECOVERYFAIL;
			else
				mib_idx = LINUX_MIB_TCPRENORECOVERYFAIL;
		} else if (icsk->icsk_ca_state == TCP_CA_Loss) {
			mib_idx = LINUX_MIB_TCPLOSSFAILURES;
		} else {
			mib_idx = LINUX_MIB_TCPTIMEOUTS;
		}
		NET_INC_STATS_BH(sock_net(sk), mib_idx);
	}

	//是否使用f-rto算法。
	if (tcp_use_frto(sk)) {
		tcp_enter_frto(sk);
	} else {
		//否则处理sack.
		tcp_enter_loss(sk, 0);
	}

	// 再次尝试重传队列的第一个段。
	if (tcp_retransmit_skb(sk, tcp_write_queue_head(sk)) > 0) {
		//重传失败。
		if (!icsk->icsk_retransmits)
			icsk->icsk_retransmits = 1;
		inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
					  min(icsk->icsk_rto, TCP_RESOURCE_PROBE_INTERVAL),
					  TCP_RTO_MAX);
		goto out;
	}
	//icsk->icsk_backoff主要用在零窗口定时器。
	icsk->icsk_backoff++;
	//icsk_retransmits也就是重试次数。
	icsk->icsk_retransmits++;

out_reset_timer:
	//计算rto,并重启定时器,这里使用karn算法,也就是下次超时时间增加一倍/
	icsk->icsk_rto = min(icsk->icsk_rto << 1, TCP_RTO_MAX);
	//重启定时器,可以看到超时时间就是我们上面的icsk_rto.
	inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, icsk->icsk_rto, TCP_RTO_MAX);
	if (icsk->icsk_retransmits > sysctl_tcp_retries1)
		__sk_dst_reset(sk);

out:;
}

下面我们来看tcp_write_timeout,它用来判断重传次数是否已经到了。这里主要分为两个分支,一个是状态为syn_sent或者syn_recv状态,一个是另外的状态。而这里系统设置的重传次数一共有4种。
1 sysctl_tcp_syn_retries,它表示syn分节的重传次数。
2 sysctl_tcp_retries1 它表示的是最大的重试次数,当超过了这个值,我们就需要检测路由表了。
3 sysctl_tcp_retries2 这个值也是表示重试最大次数,只不过这个值一般要比上面的值大。和上面那个不同的是,当重试次数超过这个值,我们就必须放弃重试了。
4 sysctl_tcp_orphan_retries 主要是针对孤立的socket(也就是已经从进程上下文中删除了,可是还有一些清理工作没有完成).对于这种socket,我们重试的最大的次数就是它。
下面来看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
static int tcp_write_timeout(struct sock *sk)
{
	struct inet_connection_sock *icsk = inet_csk(sk);
	//retry_untry表示我们需要重传的最大次数。
	int retry_until;

	//判断socket状态。
	if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
		if (icsk->icsk_retransmits)
			dst_negative_advice(&sk->sk_dst_cache);
		//设置重传最大值
		retry_until = icsk->icsk_syn_retries ? : sysctl_tcp_syn_retries;
	} else {
		//是否需要检测路由表。
		if (icsk->icsk_retransmits >= sysctl_tcp_retries1) {
			/* Black hole detection */
			tcp_mtu_probing(icsk, sk);

			dst_negative_advice(&sk->sk_dst_cache);
		}
		//设置重传最大次数为sysctl_tcp_retries2
		retry_until = sysctl_tcp_retries2;
		if (sock_flag(sk, SOCK_DEAD)) {
			//表示是一个孤立的socket。
			const int alive = (icsk->icsk_rto < TCP_RTO_MAX);

			//从tcp_orphan_retries(这个函数中会通过sysctl_tcp_orphan_retries来进行计算)中取得重传最大次数。
			retry_until = tcp_orphan_retries(sk, alive);

			if (tcp_out_of_resources(sk, alive || icsk->icsk_retransmits < retry_until))
				return 1;
		}
	}

	//最终进行判断,如果重传次数已到则返回1,否则为0.
	if (icsk->icsk_retransmits >= retry_until) {
		/* Has it gone just too far? */
		tcp_write_err(sk);
		return 1;
	}
	return 0;
}

下面来介绍下tcp_enter_loss,这个函数主要用来标记丢失的段(也就是没有acked的段),然后通过执行slow start来降低传输速率.
有关slow start以及Congestion avoidance算法描述可以看rfc2001:
http://www.faqs.org/rfcs/rfc2001.html

下面4个算法主要是用来对拥塞进行控制的,这四个算法其实都是彼此相连的。slow start和Congestion avoidance使用了相同的机制,他们都涉及到了拥塞窗口的定义。其中拥塞窗口限制着传输的长度,它的大小根据拥塞程度上升或者下降。

1
2
3
4
Slow start
Congestion avoidance
Fast re-transmit
Fast recovery

然后下面主要是介绍了slow start和Congestion avoidance的一些实现细节。

1
2
3
4
5
6
7
8
9
CWND - Sender side limit
RWND - Receiver side limit
Slow start threshold ( SSTHRESH ) - Used to determine whether slow start is used or congestion avoidance
When starting, probe slowly - IW <= 2 * SMSS
Initial size of SSTHRESH can be arbitrarily high, as high as the RWND
Use slow start when SSTHRESH > CWND. Else, use Congestion avoidance
Slow start - CWND is increased by an amount less than or equal to the SMSS for every ACK
Congestion avoidance - CWND += SMSS*SMSS/CWND
When loss is detected - SSTHRESH = max( FlightSize/2, 2*SMSS )

这里要注意在slow start中,窗口的大小是指数级的增长的。并且当cwnd(拥塞窗口)小于等于ssthresh,就是slow start模式,否则就执行Congestion avoidance。

现在我们来看tcp_enter_loss的实现。

首先来介绍下下面要用到的几个关键域的含义。
1 icsk->icsk_ca_state 这个域表示拥塞控制的状态。
2 tp->snd_una 这个域表示tcp滑动窗口中的发送未确认的第一个字节的序列号。
3 tp->prior_ssthresh 这个域表示前一个snd_ssthresh得大小,也就是说每次改变snd_ssthresh前都要保存老的snd_ssthresh到这个域。
4 tp->snd_ssthresh slow start开始时的threshold大小
5 tp->snd_cwnd_cnt 这个域表示拥塞窗口的大小。
6 TCP_SKB_CB(skb)->sacked tcp数据中的sack标记。
7 tp->high_seq 拥塞开始时,snd_nxt的大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
void tcp_enter_loss(struct sock *sk, int how)
{
	const struct inet_connection_sock *icsk = inet_csk(sk);
	struct tcp_sock *tp = tcp_sk(sk);
	struct sk_buff *skb;


	/* 1 拥塞控制状态小于TCP_CA_Disorder
	 * 2 发送未确认的序列号等于拥塞开始时的下一个将要发送的序列号
	 * 3 状态为TCP_CA_Loss,并且还未重新传输过。
	 * 如果有一个满足说明有数据丢失,因此降低threshold。
	 */
	if (icsk->icsk_ca_state <= TCP_CA_Disorder || tp->snd_una == tp->high_seq ||
		(icsk->icsk_ca_state == TCP_CA_Loss && !icsk->icsk_retransmits)) {
		//保存老的snd_ssthresh。
		tp->prior_ssthresh = tcp_current_ssthresh(sk);
		//减小snd_ssthresh
		tp->snd_ssthresh = icsk->icsk_ca_ops->ssthresh(sk);
		//设置拥塞状态。
		tcp_ca_event(sk, CA_EVENT_LOSS);
	}

	//设置拥塞窗口大小
	tp->snd_cwnd    = 1;
	tp->snd_cwnd_cnt   = 0;
	//设置时间
	tp->snd_cwnd_stamp = tcp_time_stamp;

	tp->bytes_acked = 0;
	//清空所有相关的计数器。
	tcp_clear_retrans_partial(tp);

	if (tcp_is_reno(tp))
		tcp_reset_reno_sack(tp);

	if (!how) {
		/* Push undo marker, if it was plain RTO and nothing
		 * was retransmitted. */
		tp->undo_marker = tp->snd_una;
	} else {
		tp->sacked_out = 0;
		tp->fackets_out = 0;
	}
	tcp_clear_all_retrans_hints(tp);

	//遍历sock的write队列。
	tcp_for_write_queue(skb, sk) {
		if (skb == tcp_send_head(sk))
			break;
		//判断sack段。
		if (TCP_SKB_CB(skb)->sacked & TCPCB_RETRANS)
			tp->undo_marker = 0;
		TCP_SKB_CB(skb)->sacked &= (~TCPCB_TAGBITS)|TCPCB_SACKED_ACKED;

		//如果how为1,则说明不管sack段,此时标记所有的段为丢失(sack的意思去看tcp/ip详解).
		if (!(TCP_SKB_CB(skb)->sacked&TCPCB_SACKED_ACKED) || how) {
			//设置sack段。
			TCP_SKB_CB(skb)->sacked &= ~TCPCB_SACKED_ACKED;
			TCP_SKB_CB(skb)->sacked |= TCPCB_LOST;
			//update 相关的域。
			tp->lost_out += tcp_skb_pcount(skb);
			tp->retransmit_high = TCP_SKB_CB(skb)->end_seq;
		}
	}
	tcp_verify_left_out(tp);
	//设置当前的reordering的长度
	tp->reordering = min_t(unsigned int, tp->reordering,
				   sysctl_tcp_reordering);
	//设置拥塞状态。
	tcp_set_ca_state(sk, TCP_CA_Loss);
	tp->high_seq = tp->snd_nxt;
	//由于我们修改了拥塞窗口,因此设置ecn状态。
	TCP_ECN_queue_cwr(tp);
	/* Abort F-RTO algorithm if one is in progress */
	tp->frto_counter = 0;
}

接 下来来看零窗口探测定时器。至于为什么会出现零窗口,这里就不阐述了,详细的可以去看tcp/ip详解。我们知道当0窗口之后,客户机会等待服务器端的窗 口打开报文,可是由于ip是不可靠的,有可能这个报文会丢失,因此就需要客户机发送一个探测段,用来提醒服务器及时汇报当前的窗口大小。这里我们知道当对 端接收窗口关闭后,我们这边的发送窗口也会关闭,此时不能发送任何一般的数据,除了探测段。
在内核中是通过tcp_ack_probe来控制零窗口的定时器的。也就是说接收到对端的窗口报告数据后,会进入这个函数。我们来看实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static void tcp_ack_probe(struct sock *sk)
{
	const struct tcp_sock *tp = tcp_sk(sk);
	struct inet_connection_sock *icsk = inet_csk(sk);


	//首先判断是否对端的接收窗口是否已经有空间。
	if (!after(TCP_SKB_CB(tcp_send_head(sk))->end_seq, tcp_wnd_end(tp))) {
		//如果有空间则删除零窗口探测定时器。
		icsk->icsk_backoff = 0;
		inet_csk_clear_xmit_timer(sk, ICSK_TIME_PROBE0);
		/* Socket must be waked up by subsequent tcp_data_snd_check().
		 * This function is not for random using!
		 */
	} else {
		//否则启动定时器。
		inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0,
					  min(icsk->icsk_rto << icsk->icsk_backoff, TCP_RTO_MAX),
					  TCP_RTO_MAX);
	}
}

我们知道零窗口定时器和重传的定时器是一个定时器,只不过在回调函数中,进行event判断,从而进入不同的处理。而它调用的是tcp_probe_timer函数。
这个函数主要就是用来发送探测包,我们来看它的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
static void tcp_probe_timer(struct sock *sk)
{
	struct inet_connection_sock *icsk = inet_csk(sk);
	struct tcp_sock *tp = tcp_sk(sk);
	int max_probes;
	/* 1 tp->packets_out不为0说明,当定时器被安装之后,对端的接收窗口已经被打开。这* 时就不需要传输探测包。
	 * 2 tcp_send_head用来检测是否有新的段被传输。
	 * 如果上面有一个满足,则不需要发送探测包,并直接返回。
	 */
	if (tp->packets_out || !tcp_send_head(sk)) {
		icsk->icsk_probes_out = 0;
		return;
	}

	//设置最大的重试次数。
	max_probes = sysctl_tcp_retries2;

	//这里的处理和上面的tcp_write_timeout很类似。
	if (sock_flag(sk, SOCK_DEAD)) {
		const int alive = ((icsk->icsk_rto << icsk->icsk_backoff) < TCP_RTO_MAX);

		max_probes = tcp_orphan_retries(sk, alive);

		if (tcp_out_of_resources(sk, alive || icsk->icsk_probes_out <= max_probes))
			return;
	}

	//如果重试次数大于最大的重试次数,则报错。
	if (icsk->icsk_probes_out > max_probes) {
		tcp_write_err(sk);
	} else {
		/* Only send another probe if we didn't close things up. */
	//否则发送探测包。这个函数里面会发送探测包,并重启定时器。
		tcp_send_probe0(sk);
	}
}

然 后来看delay ack定时器。所谓的delay ack也就是ack不会马上发送,而是等待一段时间和数据一起发送,这样就减少了一个数据包的发送。这里一般是将ack包含在tcp option中发送的。这里的定时器就是用来控制这段时间,如果定时器到期,都没有数据要发送给对端,此时单独发送这个ack。如果在定时器时间内,有数 据要发送,此时这个ack和数据一起发送给对端。
前面我们知道delay ack定时器的回调函数是tcp_delack_timer。在分析这个函数之前,我们先来看下这个定时器是什么时候被启动的。
首先我们知道内核接收数据都是在tcp_rcv_eastablished实现的,当我们接收完数据后,此时进入是否进行delay ack.
在tcp_rcv_eastablished最终会调用__tcp_ack_snd_check进行判断。
可以看到这个函数很简单,就是判断是否需要发送delay ack,如果是则tcp_send_delayed_ack,否则直接发送ack恢复给对端。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static void __tcp_ack_snd_check(struct sock *sk, int ofo_possible)
{
	struct tcp_sock *tp = tcp_sk(sk);

/* 1 第一个判断表示多于一个的段在等待ack,并且我们的receive buf有足够的空间,
 *   这是因为这种情况,表明应用程序读取比较快,而对端的发送速度依赖于ack的到达时间,* 因此我们不希望对端减慢速度。
 * 2 这个sock处在quickack 模式
 * 3 我们有 out-of-order数据,此时必须马上给对端以确认。
 *   当上面的任意一个为真,则立即发送ack。
**/
	if (((tp->rcv_nxt - tp->rcv_wup) > inet_csk(sk)->icsk_ack.rcv_mss
		 /* ... and right edge of window advances far enough.
		  * (tcp_recvmsg() will send ACK otherwise). Or...
		  */
		 && __tcp_select_window(sk) >= tp->rcv_wnd) ||
		/* We ACK each frame or... */
		tcp_in_quickack_mode(sk) ||
		/* We have out of order data. */
		(ofo_possible && skb_peek(&tp->out_of_order_queue))) {
		/* Then ack it now */
		tcp_send_ack(sk);
	} else {
		/* Else, send delayed ack. */
		//在这里启动定时器。
		tcp_send_delayed_ack(sk);
	}
}

上面还有一个tcp_in_quickack_mode,这个函数我们说了,它是用来判断是否处在quickack 模式。
来看这个函数:

1
2
3
4
5
static inline int tcp_in_quickack_mode(const struct sock *sk)
{
	const struct inet_connection_sock *icsk = inet_csk(sk);
	return icsk->icsk_ack.quick && !icsk->icsk_ack.pingpong;
}

其中icsk->icsk_ack.pingpong域被设置的情况只有当tcp连接是交互式的,比如telnet等等。icsk->icsk_ack.quick表示能够 quickack的数量。 然后我们来看tcp_delack_timer的实现。
在看之前,我们要知道icsk->icsk_ack.pending表示的是当前的ack的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
static void tcp_delack_timer(unsigned long data)
{
	struct sock *sk = (struct sock *)data;
	struct tcp_sock *tp = tcp_sk(sk);
	struct inet_connection_sock *icsk = inet_csk(sk);

	bh_lock_sock(sk);
	//用户进程正在使用,则等会再尝试。
	if (sock_owned_by_user(sk)) {
		/* Try again later. */
		icsk->icsk_ack.blocked = 1;
		NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_DELAYEDACKLOCKED);
		sk_reset_timer(sk, &icsk->icsk_delack_timer, jiffies + TCP_DELACK_MIN);
		goto out_unlock;
	}

	sk_mem_reclaim_partial(sk);

	//判断sock状态 以及ack的状态。如果是close或者已经处在ICSK_ACK_TIMER,则直接跳出。
	if (sk->sk_state == TCP_CLOSE || !(icsk->icsk_ack.pending & ICSK_ACK_TIMER))
		goto out;

	//如果已经超时,则重启定时器,并退出。
	if (time_after(icsk->icsk_ack.timeout, jiffies)) {
		sk_reset_timer(sk, &icsk->icsk_delack_timer, icsk->icsk_ack.timeout);
		goto out;
	}
	//清除ack状态。
	icsk->icsk_ack.pending &= ~ICSK_ACK_TIMER;

	//开始遍历prequeue。此时主要的目的是为了调用tcp_rcv_eastablished.这里会调用tcp_ack_snd_check来发送ack。
	if (!skb_queue_empty(&tp->ucopy.prequeue)) {
		struct sk_buff *skb;

		NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPSCHEDULERFAILED);

		//遍历prequeue队列,发送未发送的ack。
		while ((skb = __skb_dequeue(&tp->ucopy.prequeue)) != NULL)
			sk_backlog_rcv(sk, skb);

		tp->ucopy.memory = 0;
	}

	//检测是否有ack还需要被发送。也就是处于ICSK_ACK_SCHED状态的ack
	if (inet_csk_ack_scheduled(sk)) {

		if (!icsk->icsk_ack.pingpong) {
			/* Delayed ACK missed: inflate ATO. */
			icsk->icsk_ack.ato = min(icsk->icsk_ack.ato << 1, icsk->icsk_rto);
		} else {
			//到这里说明已经长时间没有通信,并且处于交互模式。这个时候我们需要关闭pingpong模式。
			icsk->icsk_ack.pingpong = 0;
			icsk->icsk_ack.ato      = TCP_ATO_MIN;
		}
		//立即发送ack。
		tcp_send_ack(sk);
		NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_DELAYEDACKS);
	}
	TCP_CHECK_TIMER(sk);

out:
	if (tcp_memory_pressure)
		sk_mem_reclaim(sk);
out_unlock:
	bh_unlock_sock(sk);
	sock_put(sk);
}

ubuntu dota2

ERROR- You are missing the following 32-bit libraries, and Steam may not run:

1
sudo ln -s /usr/lib/i386-linux-gnu/mesa/libGL.so.1 /usr/lib

无法输入:

1
export LC_CTYPE="en_US.UTF-8" && steam

一、集显

ubuntu下,如果是intel的核心显卡,mesa低于9.2版本的话,会出现看不见树和看不见英雄的情况 这时候就要更新mesa到9.2,mesa9.2支持3.6之后的内核版本,如果内核低于3.6,就要先更新内核 ubuntu的解决办法: 查看当前mesa版本:glxinfo |grep -i opengl 查看当前内核版本:uname -a

sudo add-apt-repository ppa:xorg-edgers/ppa sudo apt-get update sudo apt-get install linux-generic-lts-raring (更新内核) sudo apt-get dist-upgrade mesa (更新mesa) 然后就是重启系统

二、独显

ubuntu 装独显 ubuntu 12.04 N卡双显卡

如果你想用独显玩dota2, 那么你需要用optirun steam来启动steam客户端,然后再启动游戏,这样游戏就是通过独显来渲染的。你也可以用普通的steam命令来启动steam,然后在dota2 游戏的属性中,加入启动方式optirun %command。 这样只有在启动游戏之后独显才会工作。

用optirun -b primus %command%(记得要装primus),效果更好。 // 用%command%在启动时画面会显示不全,但是好像用%command好像又不会用独显了

primus默认是有垂直同步的,帧数当然会低,加个vblank_mode=0绝对秒杀virtualgl

不能用vblank_mode=0 optirun -b primus programme做桥接启动程序,这样会拉低许多显卡性能, 使用vblank_mode=0 primusrun programme,性能就上来了,我这里确实比optirun提高30%左右


1打开启动选项输入框
2 输入所选命令(使用多个命令是中间用空格隔开,例如 -novid -international -console )

-novid (去除开始动画)
-console(命令面板)
-high (使dota2 的cpu和内存使用级为最高,也就是说让dota2 可以优先其他程序使用内存)
-windowed (窗口模式)

dota 2 console 命令
1首先开启命令面板
2输入常用命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
net_graph 1 ( 网络状况显示)
	再来就是改变位置,有些人不喜欢显示在左边,这个时候可以输入:
	net_graphpos 1
	这样显示的数据就会变到右边

	net_graphpos 2
	这样会变成中间

	net_graphpos 3
	这样会变成左边

dota_minimap_hero_size 650 (英雄在小地图上的大小 650 为正常值,可自行更改)
dota_force_right_click_attack 1 (英雄可以右键直接反补)
dota_hud_healthbars 1 (去掉生命条上的分隔)
dota_health_per_vertical_marker 250 (更改每一个分隔代表的血量 默认为250)
dota_disable_range_finder 0  (随时显示你的施法距离)(很有用)
dota_camera_accelerate 49 (任意调整观看视角)(没用过)

dota2 一共有数百种命令,包括血的颜色,屏蔽某种声音等等,但是比较实际的就是这几种,其他的就不列举了。

Dota2 录像下载失败

无法打开录像文件,请确保没有其他进程已打开此文件。

在XXX\Steam\SteamApps\common\dota 2 beta\dota目录下新建一个名为replays的文件夹即可


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
net_graphheight "64"
这个等于是设置高度位置 大家如果分屏率不同 可以修改数字来决定位置 数字越小 会往下移动 

net_graphinsetbottom "437"
这个等于是设置地步位置 大家如果分屏率不同 可以修改数字来决定位置 数字越小 会往上移动 

net_graphinsetleft "0"
因为已经设置右边 这个保持0就OK 但是也记得输入一次 以防万一 

net_graphinsetright "-83"
设置右边距离 记住这里是"-83" 不是83 负数越高 越往右 大家可以根据自己的需要改变数字 


net_graphproportionalfont "0"
这个是关键 字体比例问题 默认是1 设定为0以后 就会变成我图中那样的小字 

net_graphtext "1"
这个没什么大问题 字体样式

接收包的主流程

1
int tcp_v4_rcv(struct sk_buff *skb)    linux/net/ipv4/tcp_ipv4.c #1611

//tcp刚刚收到从ipv4发上来的包
(struct tcphdr: 定义在/include/net/tcp.h中,即包的tcp首部,不包括options部分)
(struct sock :定义在/include/net/sock.h中,即表示socket)
检查skb->pkt_type != PACKET_HOST 则丢弃
检查th->doff < sizeof(struct tcphdr) / 4,即首部大小不合理,则丢弃
检查checksum

(TCP_SKB_CB(skb):定义在tcp.h是获取一个实际指向skb->cb[0]的tcp_skb_cb类型指针;将到达的首部剥离后,从中拷贝一些信息到这个变量,供tcp控制功能使用;tcp_skb_cb是在tcp刚收到时填写在包中的)
注意:
1. tcp_skb_cb->end_seq = seq + th->fin + th->fin + len-doff*4
2. when 和 sacked 没有被赋值

sk = __inet_lookup(…) 从一个hash表中获取该收包对应的sock结构,根据源IP地址+端口,目的IP地址+端口,inet_iif检查sk->sk_state == TCP_TIME_WAIT,TCP在该状态下则丢弃任何接收到的包并转入后续的特殊处理(未看,和关闭连接的状态迁移有关需要后续来看$),马上准备进入CLOSED状态了;
检查sk_filter(sk,skb),则被过滤器阻拦,丢弃
检查!sock_owned_by_user(sk),不明白sock->sk_lock的意义是什么,只有检查满足才能进入接收,否则 sk_add_backlog(sk, skb)将该sk_buff记录进sk_backlog队列;(注意这部操作加锁了!)
(struct tcp_sock *tp = tcp_sk(sk):tcp_sock定义在tcp.h中,通过tcp_sk直接将sock指针转换为tcp_sock型)

ret = tcp_v4_do_rcv(sk, skb) 进入进一步接收处理!
(之后的异常操作未看)


1
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)    linux/net/ipv4/tcp_ipv4.c #1542

//在正常状态下由tcp_v4_rcv调用,进一步进行针对接收包的处理
检查sk->sk_state == TCP_ESTABLISHED
则tcp_rcv_established(sk, skb, skb->h.th, skb->len),连接已经建立,则进入进一步接收处理!
检查sk->sk_state == TCP_LISTEN,
则struct sock *nsk = tcp_v4_hnd_req(sk, skb); //该函数中判断能否找到已有的连接请求,如果有则说明接收到的是一个ack并在其中创建一个新的sock即nsk;如果没有则说明接收到的是 syn,nsk即为sk;
if(nsk!=sk) tcp_child_process(sk,nsk,skb) //当nsk==sk时,接收的是SYN,不进行此步直接进入tcp_rcv_state_process;否则是ack说明已经创建好了的nsk,在 tcp_child_process对nsk进行tcp_rcv_state_process状态转移处理;
tcp_rcv_state_process(sk, skb, skb->h.th, skb->len); 非常重要函数!处理tcp的状态转移
reset: tcp_v4_send_reset(rsk, skb); reset,未看$
discard: kfree_skb(skb);


1
int tcp_rcv_established(struct sock *sk, struct sk_buff *skb,struct tcphdr *th, unsigned len)    linux/net/ipv4/tcp_input.c #3881

Header Prediction:基于效率的考虑,将包的处理后续阶段分为fast path和slow path两种,前者用于普通的包,后者用于特殊的包;该header prediction即用于区分两种包的流向。
1.(tcp_flag_word(th) & TCP_HP_BITS) == tp->pred_flags 判断标志位是不是正常情况;tcp_flag_word返回指向tcphdr的第三个32位基址(即length前面),而TCP_HP_BITS是把 PSH标志位给屏蔽掉即该位值不影响流向;所以总的来说pred_flag应该等于0xS?10 << 16 + snd_wnd(那么pred_flag是在tcp_fast_path_check或tcp_fast_path_on中更新值的)
2.TCP_SKB_CB(skb)->seq == tp->rcv_nxt 判断所收包是否为我们正想要接收的,非乱序包
3.*ptr != htonl((TCPOPT_NOP << 24) | (TCPOPT_NOP << 16) | (TCPOPT_TIMESTAMP << 8) | TCPOLEN_TIMESTAMP) 若包中没有正常的timestamp选项则转入slow path timestamp选项处理: 从包中的ts选项中获取数据,以此刷新tp->rx_opt的saw_tstamp,rcv_tsval,rcv_tsecr域;ts选项含三个 32bit,其中后两个分别记录着tsval和tsecr;(注意,ts_recent并不在此处更新,在后面的tcp_store_ts_recent 中更新)
struct tcp_options_received: 定义在tcp.h中,其中saw_tstamp表明timestamp选项是否有效,ts_recent_stamp是我们最近一次更新 ts_recent的时间,ts_recent是下一次回显的时戳一般等于下次发包中的rcv_tsecr;rcv_tsval是该data从发端发出时的时戳值,rcv_tsecr是回显时间戳(即该ack对应的data或者该data对应的上次ack中的ts_tsval值),(注意两端时钟无需同步;当ack被收端推迟时,所回复的ack中的timestamp指向所回复包群中的第一个确认包 “When an incoming segment belongs to the current window, but arrives out of order (which implies that an earlier segment was lost), the timestamp of the earlier segment is returned as soon as it arrives, rather than the timestamp of the segment that arrived out of order.”这条细节未看明白$)从包中的时间戳选项中记录这两个值

4.PAWS check:(s32)(tp->rx_opt.rcv_tsval - tp->rx_opt.ts_recent) < 0,则转入slow path
(PAWS:Protection Against Wrapped Sequence Numbers, SeqNo有可能会有回环交叠(因为它最大只有32bit),两个相同序号的包实际上是不同的两个包,此时判断tsval是否小于ts_recent即判断该包是否是一个过去时间的一个多余的包,然后将其作为一个重复包丢弃)

Fast Path:

1.当len == tcp_header_len,即这是一个纯ack(区别于piggyback),注意这是个纯ack,所以它通过长度来进行判断而不是标识!
tcp_store_ts_recent(tp): tp->rx_opt.ts_recent = tp->rx_opt.rcv_tsval;
tcp_ack(sk, skb, 0) 处理ack,进一步处理,未看!
__kfree_skb(skb) 释放该包
tcp_data_snd_check(sk,tp) 检查有无更进一步的data包处理
2.当len < tcp_header_len,说明该包的首部太小,清除之;
3.当len > tcp_header_len,它是一个data包,tcp_copy_to_iovec函数未看,它决定该payload是否可以直接拷贝给用户空间:
可,tcp_store_ts_recent(tp);
tcp_rcv_rtt_measure_ts(sk,skb); //计算RTT
__skb_pull(skb, tcp_header_len); //剥tcp首部
tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq; //更新rcv_next
($ 那么将data拷贝到用户空间的操作在何处体现?难道是在tcp_copy_to_iovec中?)
不可,除了以上的操作之外,还要
__skb_queue_tail(&sk->sk_receive_queue, skb); //将该包加入到接收sk_buff队列尾部
tcp_event_data_recv():management tasks处理
若TCP_SKB_CB(skb)->ack_seq != tp->snd_una,说明这是一个有效的ack包
tcp_ack(sk, skb, FLAG_DATA); //FLAG_DATA说明这是一个背在data上的ack
tcp_data_snd_check(sk, tp); //该函数调用tcp_push_pending_frames函数,如果sk->sk_send_head存在则最终调用 tcp_write_xmit函数发包
__tcp_ack_snd_check(sk, 0); //检查基于该收包事件,有无进一步的ack包处理(Delayed ACK,Quick ACK)

Slow Path:

tcp_checksum_complete_user(sk, skb):checksum检查
tcp_fast_parse_options(skb, th, tp):timestamp选项检查;tcp_paws_discard(sk, skb):PAWS检查
tcp_sequence(tp, TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq):检查是否乱序,并在其中激活QuickACK模式
上面两行中,都会再检查RST标志,若没激活则tcp_send_dupack,作用不明,貌似是针对该错包回复一个冗余的ack
检查RST标志,tcp_reset(sk) 该函数没什么操作,填写一些错误信息后进入tcp_done函数(该函数进行一些关闭tcp连接的收尾操作)
tcp_replace_ts_recent(tp, TCP_SKB_CB(skb)->seq):更新timestamp信息
检查SYN标志,在连接已建立的状态下,收到SYN是错误的,因此tcp_reset(sk)
检查ACK标志,tcp_ack(sk, skb, FLAG_SLOWPATH)
tcp_rcv_rtt_measure_ts(sk, skb):更新RTT
tcp_urg(sk, skb, th):处理URG标志
tcp_data_queue(sk, skb):处理接收包所含数据,未看
tcp_data_snd_check(sk, tp) & tcp_ack_snd_check(sk):检查有无进一步的data或ack发送


1
static void tcp_event_data_recv(struct sock *sk, struct tcp_sock *tp, struct sk_buff *skb)    linux/net/ipv4/tcp_input.c #502

// inet_csk_schedule_ack(sk):将icsk_pending置为ICSK_ACK_SCHED,但具体意义不明
(struct inet_connection_sock:/linux/include/net/inet_connection_sock,面向INET连接的 socket结构,记录着和tcp连接有关的很多变量,比如本函数要处理的ATO(Acknowledgement timeout)信息;tcp_sock是其上的拓展,它的具体意义尚待发掘)
tcp_measure_rcv_mss(sk, skb):更新rcv_mss,说是与delayed ACK有关,但是具体是怎么运作的?
tcp_rcv_rtt_measure(tp):更新RTT,为什么又更新一遍$
接下来的一些列操作是更新inet_connection_sock中的ATO信息,具体操作代码中有注释,但这些信息的运作方式还不明


1
static int tcp_ack(struct sock *sk, struct sk_buff *skb, int flag)    /linux/net/ipv4/tcp_input.c #2491

//处理接受到的ack,内容非常复杂 首先介绍一下ack可以携带的各个FLAG:

1
2
3
4
5
6
7
8
9
10
11
12
13
FLAG_DATA:              Incoming frame contained data.
FLAG_WIN_UPDATE:        Incoming ACK was a window update
FLAG_DATA_ACKED:        This ACK acknowledged new data.
FLAG_RETRANS_DATA_ACKED:Some of which was retransmitted.
FLAG_SYN_ACKED:         This ACK acknowledged SYN.
FLAG_DATA_SACKED:       New SACK.
FLAG_ECE:               ECE in this ACK.
FLAG_DATA_LOST:         SACK detected data lossage.
FLAG_SLOWPATH:          Do not skip RFC checks for window update.
FLAG_ACKED:             (FLAG_DATA_ACKED|FLAG_SYN_ACKED)
FLAG_NOT_DUP:           (FLAG_DATA|FLAG_WIN_UPDATE|FLAG_ACKED)
FLAG_CA_ALERT:          (FLAG_DATA_SACKED|FLAG_ECE)
FLAG_FORWARD_PROGRESS: (FLAG_ACKED|FLAG_DATA_SACKED)

prior_snd_una = tp->snd_una;ack_seq = TCP_SKB_CB(skb)->seq; ack = TCP_SKB_CB(skb)->ack_seq;
//1记录着上一次被确认的data序号;2记录着所收ack包的序号;3记录着所收ack包确认对象的data序号;
首先判断若ack在tp->snd_nxt之后或者在prio_snd_una之前,则说明该ack非法或者过时(在过时的情况下,若sacked打开则还需tcp_sacktag_write_queue处理) 24

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
if (!(flag&FLAG_SLOWPATH) && after(ack, prior_snd_una))
	tcp_update_wl(即tp->snd_wl1 = ack_seq); tp->snd_una=ack; //为什么此种情况下并不更新窗口?
else
	flag |= tcp_ack_update_window(sk, tp, skb, ack, ack_seq);
	//nwin = ntohs(skb->h.th->window)从ack中记录通告窗口
	如果检查需要更新发送窗口,则tp->snd_wl1 = ack_seq; tp->snd_wnd = nwin;
	tp->snd_una = ack;
	if (TCP_SKB_CB(skb)->sacked) flag |= tcp_sacktag_write_queue(sk, skb, prior_snd_una); //该函数未看

	tp->rcv_tstamp = tcp_time_stamp; //rcv_tstamp记录着最近一次收到ack的时戳
	prior_in_flight = tcp_packets_in_flight(tp);
	if(!tp->packets_out) icsk->icsk_prbes_out = 0;
	if (sk->sk_send_head) tcp_ack_probe(sk);    //若此时网络中没有data,直接进入zero-window probe的ack处理;通告窗口的数据已经得到处理,所以tcp_ack_probe中仅仅是重置probe计时器,即 icsk->icsk_retransmit_timer

	flag |= tcp_clean_rtx_queue(sk, &seq_rtt);   //从重传队列中移除被确认的data包

	if (tcp_ack_is_dubious(sk, flag)) { //该函数判断此ack是否可疑,判真情况下具体是flag不为FLAG_NOT_DUP,或flag是FLAG_CA_ALERT,或 icsk_ca_state不为TCP_CA_OPEN状态
	if ((flag & FLAG_DATA_ACKED) && tcp_may_raise_cwnd(sk, flag))
	//如果这个包是一个对新数据包的ack,那么通过tcp_may_raise_cwnd函数来判断是否要进行窗口操作,判真情况下具体是flag不是 FLAG_ECE或snd_cwnd<snd_ssthresh(慢启动?)且icsk_ca_state不为TCP_CA_RECOVERY和 TCP_CA_CWR状态(所以,为什么TCP_CA_LOSS状态可以增窗呢?)
		tcp_cong_avoid(sk, ack, seq_rtt, prior_in_flight, 0);  
	//该函数会调用icsk->icsk_ca_ops->cong_avoid(sk, ack, rtt, in_flight, good), 这是个函数指针;另外会更新snd_cwnd_stamp
	tcp_fastretrans_alert(sk, prior_snd_una, prior_packets, flag); //未看,极其重要的函数
}else{
	if ((flag & FLAG_DATA_ACKED)) tcp_cong_avoid(sk, ack, seq_rtt, prior_in_flight, 1);
}

tcp_ack中有很多新的内容,都还未涉及,要注意!!!!!!


1
static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)    /linux/net/ipv4/tcp_input.c #3139

//将数据拷贝至用户空间
若TCP_SKB_CB(skb)->seq == TCP_SKB_CB(skb)->end_seq 则空包丢弃
__skb_pull(skb, th->doff*4) //剥离tcp首部

1.若TCP_SKB_CB(skb)->seq == tp->rcv_nxt且tcp_receive_window(tp)!=0,非乱序且处于接受窗口中,正常的情况

若tp->ucopy.task == current, tp->copied_seq == tp->rcv_nxt, tp->ucopy.len等条件满足,则可以拷贝至用户空间
//current是什么不明?ucopy.len貌似是用户最先设定的数据包的量,每次收包之后减小直至零
skb_copy_datagram_iovec(skb, 0, tp->ucopy.iov, chunk) //向ucopy.iov拷贝数据
tcp_rcv_space_adjust(sk) //计算TCP接受buffer空间大小,拷贝完
tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq;
if(th->fin) tcp_fin(skb, sk, th); //原来fin的处理在这里!
若!skb_queue_empty(&tp->out_of_order_queue)
tcp_ofo_queue(sk); //看out_of_order_queue中有没有可以移到receive_queue中
tcp_sack_remove(tp) //RCV.NXT advances, some SACKs should be eaten
tcp_fast_path_check(sk,tp) //tp->pred_flag值的更新
清除skb并return

2.若!after(TCP_SKB_CB(skb)->end_seq, tp->rcv_nxt) 说明这是一个重传的包

tcp_dsack_set(tp, TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq); //在其中打开并填写dsack信息,在dyokucate_sack[0]中从seq到end_seq,修改dsack和eff_sacks值
tcp_enter_quickack_mode(sk); //进入quick ack模式
清除skb并return
若!before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt + tcp_receive_window(tp))
清除skb并return
若before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt) 说明这是一个Partial包,即seq<rcv_next<end_seq
tcp_dsack_set(tp, TCP_SKB_CB(skb)->seq, tp->rcv_nxt); //填写dsack信息,从seq到rcv_nxt

3. 其他情况,说明收到了一个乱序包

若out_of_order_queue为空,则
(注:out_of_order_queue是一个sk_buff_head结构,它的prev/next指针分别指向最后一个和第一个sk_buff结构,块的排放顺序对应其序号的大小顺序)
初始化sack相关域,num_sacks/eff_sacks为1,dsack为0,selective_acks[0]从seq到end_seq;
__skb_queue_head(&tp->out_of_order_queue,skb); //将收包加入out_of_order_queue的头部

若out_of_order_queue不为空,则首先获取skb1 = tp->out_of_order_queue.prev即最新的一个乱序块
若seq == TCP_SKB_CB(skb1)->end_seq,说明收包能够接在最新乱序块的右边
__skb_append(skb1, skb, &tp->out_of_order_queue);
tp->selective_acks[0].end_seq = end_seq; //将新收包接在skb1的右边,看来第一个selective_acks块对应的是最新的乱序序列
循环执行skb1=skb1->prev,直到找到!after(TCP_SKB_CB(skb1)->seq, seq)表明需要将收包插在此块之后,或skb1=(struct sk_buff)&tp->out_of_order_queue表明收包比队列中的所与块的序列都要小
循环内需要找到收包与队列已有包中的重复部分,然后tcp_dsack_set设置该部分为dsack内容
__skb_insert(skb, skb1, skb1->next, &tp->out_of_order_queue); //将收包对应的块插入到队列中
再次循环执行skb1=skb1->next,直到找到!after(end_seq, TCP_SKB_CB(skb1)->seq)表明需要将从收包到该包之间的所有包全部从队列中移除,或者skb1=(struct sk_buff
)&tp->out_of_order_queue表明需要将收包之后的所有包都移出
循环内需要将当前的队列包与收包的交叠部分设置为dsack值(当然随着循环的推进,dsack处于不断更新的状况),还要通过 __skb_unlink(skb1, &tp->out_of_order_queue),__kfree_skb(skb1);将当前的队列包移除
(该处的两部循环,旨在通过比较队列中块的序号和所收包的序号范围,将队列中的包连续化,即消除孔洞)