kk Blog —— 通用基础

date [-d @int|str] [+%s|"+%F %T"]

通过blktrace, debugfs分析磁盘IO

如何通过blktrace+debugfs找到发生IO的文件,然后再结合自己的应用程序,分析出这些IO到底是 谁产生的,最终目的当然是尽量减少不必要的IO干扰,提高程序的性能。

blktrace是Jens Axobe写的一个跟踪IO请求的工具,Linux系统发起的IO请求都可以通过blktrace捕获并分析,关于这个工具的介绍请自行google之,这里推荐我们部门的褚霸同学的blog,里面有好几篇文章分别介绍了blktrace, blkparse以及blkiomon等工具的使用。

debugfs是ext2, ext3, ext4文件系统提供的文件系统访问工具,通过它我们可以不通过mount文件系统而直接访问文件系统的内容,它是e2fsprogs的一部分,默认应该都是安装的,详细的说明可以通过man debugfs得到。

下面我来演示一下如何通过这两个工具的配合来找到磁盘IO的源头。

先看一个简单的例子:
在一个终端会输入如下命令:

1
while [ 1 ];do dd if=/dev/zero of=test_file bs=4k count=20 seek=$RANDOM oflag=sync;done

随机的在test_file里面写数据造成较大的IO压力,现在看看如何通过blktrace和debugfs抓到它。

1、通过iostat观察到有很大的磁盘压力

1
2
Device:         rrqm/s   wrqm/s     r/s     w/s   rsec/s   wsec/s avgrq-sz avgqu-sz   await  svctm  %util
sdb               0.00  2759.00    0.00 3515.50     0.00 50196.00    14.28     0.90    0.26   0.24  85.70

2、我们看到sdb压力很大,这时候就需要通过blktrace抓取对应盘的数据
blktrace /dev/sdb 有IO压力的时候一会儿就可以了,通过ctrl+c停止抓取。
blktrace是需要debugfs支持的,如果系统提示debugfs没有mount,需要先mount上
mount -t debugfs none /sys/kernel/debug 再执行blktrace命令

3、将blktrace抓出来的二进制文件转成文本格式。
blkparse sdb.blktrace.* > 1.log
或blktrace -d /dev/sda -o - |blkparse -i - > 1.log

4、开始分析日志

1
2
3
4
5
6
7
  grep ‘ A ‘ 1.log|head -n 5

8,16   0       39     0.001242727  2872  A  WS 420143 + 8 <- (8,17) 420080
8,16   0       52     0.001361766  2872  A  WS 420151 + 8 <- (8,17) 420088
8,16   0       65     0.001440210  2872  A  WS 420159 + 8 <- (8,17) 420096
8,16   0       78     0.001518207  2872  A  WS 420167 + 8 <- (8,17) 420104
8,16   0       91     0.001596083  2872  A  WS 420175 + 8 <- (8,17) 420112

为啥要grep ‘ A ‘呢?因为这条信息是上层一个读写请求进入到Linux IO协议栈的第一步,只有在这里我们可以看到清晰的请求原始信息。比如

1
8,16   0       39     0.001242727  2872  A  WS 420143 + 8 <- (8,17) 420080

这条说明是设备(8,17)也就是sdb1上产生的扇区为420080的写请求(读请求的话会在WS对应的位置出现‘R’),长度是8,它被映射到(8,16)上位置为420143。这个IO请求的完整生命周期是这样的:

1
2
3
4
5
 8,16   0       39     0.001242727  2872  A  WS 420143 + 8 <- (8,17) 420080
 8,16   0       41     0.001244984  2872  G  WS 420143 + 8 [dd]
 8,16   0       43     0.001246609  2872  I  WS 420143 + 8 [dd]
 8,16   0       45     0.001255064  2872  D  WS 420143 + 8 [dd]
 8,16   0       46     0.001325168     0  C  WS 420143 + 8 [0]

可以看到从’ A ‘的下一条开始都是以整个设备的扇区号为标识的,不方便我们找到对应的分区。

5、下面就开始通过debugfs来分析这条读写请求的来源了。 (8, 17)是sdb1, 420080是扇区号(IO层的基本单位是扇区sector,大小是512bytes),而文件系统层是通过块block来管理的,一般的 ext3,ext4的块block大小是4096[1],由此可得这个请求对应到文件系统的块block号是420080/8=52510, debugfs提供了命令icheck可以通过block号找到它对应的文件的inode。

1
2
3
4
#debugfs -R ‘icheck 52510′ /dev/sdb1
debugfs 1.43-WIP (1-Aug-2012)
Block Inode number
52510 12

6、通过inode number找到对应的文件名,很幸运,debugfs又提供了另外一条命令ncheck可以找到inode对应的文件名[2]。

1
2
3
4
#debugfs -R ‘ncheck 12′ /dev/sdb1
debugfs 1.43-WIP (1-Aug-2012)
Inode Pathname
12 //test_file

大家可以看到,我们现在已经成功找到了test_file这个文件啦,至此块设备层和文件系统层的分析工作已经结束了,下面就可以结合应用看看为啥会对这个文件有如此频繁的读写操作了!

[1] 块大小实际上也能够通过debugfs来得到。

1
debugfs -R ‘stats’ /dev/sdb1|grep “Block size”

[2] 有的同学找到的inode number是8,然后却无法找到对应的文件名。这是因为8是ext3/4文件系统中的日志文件的inode,它是隐藏文件,所以无法找到,可以再试试其他的block号哦!

根据inode你就可以找到对应的文件是什么了
find / -inum your_inode

内核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);将当前的队列包移除
(该处的两部循环,旨在通过比较队列中块的序号和所收包的序号范围,将队列中的包连续化,即消除孔洞)


内核tcp协议栈SACK的处理tcp_sacktag_write_queue

http://simohayha.iteye.com/blog/578744

上一篇处理ack的blog中我们知道当我们接收到ack的时候,我们会判断sack段,如果包含sack段的话,我们就要进行处理。这篇blog就主要来介绍内核如何处理sack段。

SACK是包含在tcp的option中的,由于tcp的头的长度的限制,因此SACK也就是最多包含4个段,也就是32个字节。我们先来看tcp中的SACK段的表示:

1
2
3
4
struct tcp_sack_block {
	u32   start_seq; //起始序列号
	u32   end_seq;   //结束序列号
};

可以看到很简单,就是一个段的起始序列号和一个结束序列号。

前一篇blog我们知道tcp_skb_cb的sacked域也就是sack option的偏移值,而在tcp的option它的组成是由3部分组成的,第一部分为option类型,第二部分为当前option的长度,第三部分才是数据段,因此我们如果要取得SACK的段,就必须这样计算。

这里ack_skb也就是我们要处理的skbuffer。

1
2
3
4
5
//首先得到sack option的起始指针。
unsigned char *ptr = (skb_transport_header(ack_skb) +
			  TCP_SKB_CB(ack_skb)->sacked);
//加2的意思也就是加上类型和长度,这里刚好是2个字节。最终结果也就是sack option的数据段。
struct tcp_sack_block_wire *sp_wire = (struct tcp_sack_block_wire *)(ptr+2);

这里很奇怪,内核还有一个tcp_sack_block_wire类型的结构,它和tcp_sack_block是完全一样的。

而我们如果要得到当前的SACK段的个数我们要这样做:

1
2
#define TCPOLEN_SACK_BASE        2
int num_sacks = min(TCP_NUM_SACKS, (ptr[1] - TCPOLEN_SACK_BASE) >> 3);

这里ptr1也就是sack option的长度(字节数),而TCPOLEN_SACK_BASE为类型和长度字段的长度,因此这两个值的差也就是sack段的总长度,而这里每个段都是8个字节,因此我们右移3位就得到了它的个数,最后sack的段的长度不能大于4,因此我们要取一个最小值。

上面的结构下面这张图非常清晰的展示了,这几个域的关系:

然后我们来看SACK的处理,在内核中SACK的处理是通过tcp_sacktag_write_queue来实现的,这个函数比较长,因此这里我们分段来看。

先来看函数的原型

1
2
3
static int
tcp_sacktag_write_queue(struct sock *sk, struct sk_buff *ack_skb,
			u32 prior_snd_una)

第一个参数是当前的sock,第二个参数是要处理的skb,第三个参数是接受ack的时候的snd_una.

在看之前这里有几个重要的域要再要说明下。
1 tcp socket的sacked_out域,这个域保存了所有被sack的段的个数。
2 还有一个就是tcp_sacktag_state结构,这个结构保存了当前skb的一些信息。

1
2
3
4
5
struct tcp_sacktag_state {
	int reord;
	int fack_count;
	int flag;
};

3 tcp socket的highest_sack域,这个域也就是被sack确认的最大序列号的skb。

先来看第一部分,这部分的代码主要功能是初始化一些用到的值,比如sack的指针,当前有多少sack段等等,以及一些合法性校验。

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
//sack段的最大个数
#define TCP_NUM_SACKS 4
......
......
	const struct inet_connection_sock *icsk = inet_csk(sk);
	struct tcp_sock *tp = tcp_sk(sk);
	//下面两句代码,前面已经分析过了,也就是取得sack的指针以及sack 数据段的指针。
	unsigned char *ptr = (skb_transport_header(ack_skb) +
				  TCP_SKB_CB(ack_skb)->sacked);
	struct tcp_sack_block_wire *sp_wire = (struct tcp_sack_block_wire *)(ptr+2);
	//这个数组最终会用来保存所有的SACK段。
	struct tcp_sack_block sp[TCP_NUM_SACKS];
	struct tcp_sack_block *cache;
	struct tcp_sacktag_state state;
	struct sk_buff *skb;
	//这里得到当前的sack段的个数,这段代码前面也介绍过了。
	int num_sacks = min(TCP_NUM_SACKS, (ptr[1] - TCPOLEN_SACK_BASE) >> 3);
	int used_sacks;
	//重复的sack的个数。
	int found_dup_sack = 0;
	int i, j;
	int first_sack_index;

	state.flag = 0;
	state.reord = tp->packets_out;
	//如果sack的个数为0,则我们要更新相关的域。
	if (!tp->sacked_out) {
		if (WARN_ON(tp->fackets_out))
			tp->fackets_out = 0;
	//这个函数主要更新highest_sack域。
		tcp_highest_sack_reset(sk);
	}

	//开始检测是否有重复的sack。这个函数紧接着会详细分析。
	found_dup_sack = tcp_check_dsack(sk, ack_skb, sp_wire,
					 num_sacks, prior_snd_una);
	//如果有发现,则设置flag。
	if (found_dup_sack)
		state.flag |= FLAG_DSACKING_ACK;

	//再次判断ack的序列号是否太老。
	if (before(TCP_SKB_CB(ack_skb)->ack_seq, prior_snd_una - tp->max_window))
		return 0;
	//如果packets_out为0,则说明我们没有发送还没有确认的段,此时进入out,也就是错误处理。
	if (!tp->packets_out)
		goto out;

在看接下来的部分之前我们先来看tcp_highest_sack_reset和tcp_check_dsack函数,先是tcp_highest_sack_reset函数。

1
2
3
4
5
static inline void tcp_highest_sack_reset(struct sock *sk)
{
	//设置highest_sack为写队列的头。
	tcp_sk(sk)->highest_sack = tcp_write_queue_head(sk);
}

这里原因很简单,因为当sacked_out为0,则说明没有通过sack确认的段,此时highest_sack自然就指向写队列的头。
第二个是tcp_check_dsack函数,这个函数比较复杂,他主要是为了检测D-SACK,也就是重复的sack。
有关dsack的概念可以去看RFC 2883和3708.
我这里简要的提一下dsack的功能,D-SACK的功能主要是使接受者能够通过sack的块来报道接收到的重复的段,从而使发送者更好的进行拥塞控制。
这里D-SACK的判断是通过RFC2883中所描述的进行的。如果是下面两种情况,则说明收到了一个D-SACK。
1 如果SACK的第一个段所ack的区域被当前skb的ack所确认的段覆盖了一部分,则说明我们收到了一个d-sack,而代码中也就是sack第一个段的起始序列号小于snd_una。下面的图描述了这种情况:

2 如果sack的第二个段完全包含了第二个段,则说明我们收到了重复的sack,下面这张图描述了这种关系。

最后要注意的是,这里收到D-SACK后,我们需要打开当前sock d-sack的option。并设置dsack的flag。

然后我们还需要判断dsack的数据是否已经被ack完全确认过了,如果确认过了,我们就需要更新undo_retrans域,这个域表示重传的数据段的个数。

来看代码:

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
static int tcp_check_dsack(struct sock *sk, struct sk_buff *ack_skb,
			   struct tcp_sack_block_wire *sp, int num_sacks,
			   u32 prior_snd_una)
{
	struct tcp_sock *tp = tcp_sk(sk);
	//首先取得sack的第一个段的起始和结束序列号
	u32 start_seq_0 = get_unaligned_be32(&sp[0].start_seq);
	u32 end_seq_0 = get_unaligned_be32(&sp[0].end_seq);
	int dup_sack = 0;

	//判断D-sack,首先判断第一个条件,也就是起始序列号小于ack的序列号
	if (before(start_seq_0, TCP_SKB_CB(ack_skb)->ack_seq)) {
		//设置dsack标记。
		dup_sack = 1;
		//这里更新tcp的option的sack_ok域。
		tcp_dsack_seen(tp);
		NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPDSACKRECV);
	} else if (num_sacks > 1) {
		//然后执行第二个判断,取得第二个段的起始和结束序列号。
		u32 end_seq_1 = get_unaligned_be32(&sp[1].end_seq);
		u32 start_seq_1 = get_unaligned_be32(&sp[1].start_seq);
		//执行第二个判断,也就是第二个段完全包含第一个段。
		if (!after(end_seq_0, end_seq_1) &&
			!before(start_seq_0, start_seq_1)) {
			dup_sack = 1;
			tcp_dsack_seen(tp);
			NET_INC_STATS_BH(sock_net(sk),
					LINUX_MIB_TCPDSACKOFORECV);
		}
	}

	//判断是否dsack的数据段完全被ack所确认。
	if (dup_sack &&
		!after(end_seq_0, prior_snd_una) &&
		after(end_seq_0, tp->undo_marker))
		//更新重传段的个数。
		tp->undo_retrans--;

	return dup_sack;
}

然后回到tcp_sacktag_write_queue,接下来这部分很简单,主要是提取sack的段到sp中,并校验每个段的合法性,然后统计一些信息。

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
//开始遍历,这里num_sacks也就是我们前面计算的sack段的个数
for (i = 0; i < num_sacks; i++) {
	int dup_sack = !i && found_dup_sack;

	//赋值。
	sp[used_sacks].start_seq = get_unaligned_be32(&sp_wire[i].start_seq);
	sp[used_sacks].end_seq = get_unaligned_be32(&sp_wire[i].end_seq);

	//检测段的合法性。
	if (!tcp_is_sackblock_valid(tp, dup_sack,
					sp[used_sacks].start_seq,
					sp[used_sacks].end_seq)) {
		int mib_idx;

		if (dup_sack) {
			if (!tp->undo_marker)
				mib_idx = LINUX_MIB_TCPDSACKIGNOREDNOUNDO;
			else
				mib_idx = LINUX_MIB_TCPDSACKIGNOREDOLD;
		} else {
			/* Don't count olds caused by ACK reordering */
			if ((TCP_SKB_CB(ack_skb)->ack_seq != tp->snd_una) &&
				!after(sp[used_sacks].end_seq, tp->snd_una))
				continue;
			mib_idx = LINUX_MIB_TCPSACKDISCARD;
		}
		//更新统计信息。
		NET_INC_STATS_BH(sock_net(sk), mib_idx);
		if (i == 0)
			first_sack_index = -1;
		continue;
	}

	//忽略已经确认过的段。
	if (!after(sp[used_sacks].end_seq, prior_snd_una))
		continue;
	//这个值表示我们要使用的sack的段的个数。
	used_sacks++;
}

然后接下来的代码就是排序sack的段,也就是按照序列号的大小来排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
for (i = used_sacks - 1; i > 0; i--) {
	for (j = 0; j < i; j++) {
		//可以看到这里通过比较起始序列号来排序。
		if (after(sp[j].start_seq, sp[j + 1].start_seq)) {
			//交换对应的值。
			swap(sp[j], sp[j + 1]);

			/* Track where the first SACK block goes to */
			if (j == first_sack_index)
				first_sack_index = j + 1;
		}
	}
}

然后就是cache的初始化,这里的tcp socket的recv_sack_cache域要注意,这个域保存了上一次处理的sack的段的序列号。可以看到这个域类型也是tcp_sack_block,而且大小也是4,

1
2
3
4
5
6
7
8
9
10
11
12
13
//如果sack的数据段的个数为0,则说明我们要忽略调cache,此时可以看到cache指向recv_sack_cache的末尾。
if (!tp->sacked_out) {
	/* It's already past, so skip checking against it */
	cache = tp->recv_sack_cache + ARRAY_SIZE(tp->recv_sack_cache);
} else {
	//否则取出cache,然后跳过空的块。
	cache = tp->recv_sack_cache;
	/* Skip empty blocks in at head of the cache */
	while (tcp_sack_cache_ok(tp, cache) && !cache->start_seq &&
		   !cache->end_seq)
		//跳过空的块。
		cache++;
}

然后就是开始真正处理重传队列中的skb了。

我们要知道重传队列中的skb有三种类型,分别是SACKED(S), RETRANS® 和LOST(L),而每种类型所处理的数据包的个数分别保存在sacked_out, retrans_out 和lost_out中。

而处于重传队列的skb也就是会处于下面6中状态:

1
2
3
4
5
6
7
 * Tag  InFlight    Description
 * 0       1        - orig segment is in flight.
 * S       0        - nothing flies, orig reached receiver.
 * L       0        - nothing flies, orig lost by net.
 * R       2        - both orig and retransmit are in flight.
 * L|R     1        - orig is lost, retransmit is in flight.
 * S|R     1        - orig reached receiver, retrans is still in flight.

这里Tag也就是上面所说的三种类型,而InFlight也就是表示还在网络中的段的个数。

然后重传队列中的skb的状态变迁是通过下面这几种事件来触发的:

1
2
3
4
5
6
7
8
9
10
11
 1. New ACK (+SACK) arrives. (tcp_sacktag_write_queue())
 * 2. Retransmission. (tcp_retransmit_skb(), tcp_xmit_retransmit_queue())
 * 3. Loss detection event of one of three flavors:
 *    A. Scoreboard estimator decided the packet is lost.
 *       A'. Reno "three dupacks" marks head of queue lost.
 *       A''. Its FACK modfication, head until snd.fack is lost.
 *    B. SACK arrives sacking data transmitted after never retransmitted
 *       hole was sent out.
 *    C. SACK arrives sacking SND.NXT at the moment, when the
 *       segment was retransmitted.
 * 4. D-SACK added new rule: D-SACK changes any tag to S.

在进入这段代码分析之前,我们先来看几个重要的域。

tcp socket的high_seq域,这个域是我们进入拥塞控制的时候最大的发送序列号,也就是snd_nxt.

然后这里还有FACK的概念,FACK算法也就是收到的不同的SACK块之间的hole,他就认为是这些段丢失掉了。因此这里tcp socket有一个fackets_out域,这个域表示了

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
	//首先取得写队列的头,以便与下面的遍历。
	skb = tcp_write_queue_head(sk);
	state.fack_count = 0;
	i = 0;

	//这里used_sacks表示我们需要处理的sack段的个数。
	while (i < used_sacks) {
		u32 start_seq = sp[i].start_seq;
		u32 end_seq = sp[i].end_seq;
		//得到是否是重复的sack
		int dup_sack = (found_dup_sack && (i == first_sack_index));
		struct tcp_sack_block *next_dup = NULL;

		if (found_dup_sack && ((i + 1) == first_sack_index))
			next_dup = &sp[i + 1];

		//如果sack段的结束序列号大于将要发送的最大序列号,这个情况说明我们可能有数据丢失。因此设置丢失标记。这里可以看到也就是上面所说的事件B到达。
		if (after(end_seq, tp->high_seq))
			state.flag |= FLAG_DATA_LOST;

		//跳过一些太老的cache
		while (tcp_sack_cache_ok(tp, cache) &&
			   !before(start_seq, cache->end_seq))
			cache++;

		//如果有cache,就先处理cache的sack块。
		if (tcp_sack_cache_ok(tp, cache) && !dup_sack &&
			after(end_seq, cache->start_seq)) {

			//如果当前的段的起始序列号小于cache的起始序列号(这个说明他们之间有交叉),则我们处理他们之间的段。
			if (before(start_seq, cache->start_seq)) {
				skb = tcp_sacktag_skip(skb, sk, &state,
							   start_seq);
				skb = tcp_sacktag_walk(skb, sk, next_dup,
							   &state,
							   start_seq,
							   cache->start_seq,
							   dup_sack);
			}

			//处理剩下的块,也就是cache->end_seq和ned_seq之间的段。
			if (!after(end_seq, cache->end_seq))
				goto advance_sp;
			//是否有需要跳过处理的skb
			skb = tcp_maybe_skipping_dsack(skb, sk, next_dup,
							   &state,
							   cache->end_seq);

			/* ...tail remains todo... */
			//如果刚好等于sack处理的最大序列号,则我们需要处理这个段。
			if (tcp_highest_sack_seq(tp) == cache->end_seq) {
				/* ...but better entrypoint exists! */
				skb = tcp_highest_sack(sk);
				if (skb == NULL)
					break;
				state.fack_count = tp->fackets_out;
				cache++;
				goto walk;
			}

			//再次检测是否有需要skip的段。
			skb = tcp_sacktag_skip(skb, sk, &state, cache->end_seq);

			//紧接着处理下一个cache。
			cache++;
			continue;
		}

		//然后处理这次新的sack段。
		if (!before(start_seq, tcp_highest_sack_seq(tp))) {
			skb = tcp_highest_sack(sk);
			if (skb == NULL)
				break;
			state.fack_count = tp->fackets_out;
		}
		skb = tcp_sacktag_skip(skb, sk, &state, start_seq);

walk:
		//处理sack的段,主要是tag赋值。
		skb = tcp_sacktag_walk(skb, sk, next_dup, &state,
					   start_seq, end_seq, dup_sack);

advance_sp:
		/* SACK enhanced FRTO (RFC4138, Appendix B): Clearing correct
		 * due to in-order walk
		 */
		if (after(end_seq, tp->frto_highmark))
			state.flag &= ~FLAG_ONLY_ORIG_SACKED;

		i++;
	}

上面的代码并不复杂,这里主要有两个函数,我们需要详细的来分析,一个是tcp_sacktag_skip,一个是tcp_sacktag_walk。

先来看tcp_sacktag_skip,我们给重传队列的skb的tag赋值时,我们需要遍历整个队列,可是由于我们有序列号,因此我们可以先确认起始的skb,然后从这个skb开始遍历,这里这个函数就是用来确认起始skb的,这里确认的步骤主要是通过start_seq来确认的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static struct sk_buff *tcp_sacktag_skip(struct sk_buff *skb, struct sock *sk,
					struct tcp_sacktag_state *state,
					u32 skip_to_seq)
{
	//开始遍历重传队列。
	tcp_for_write_queue_from(skb, sk) {
		//如果当前的skb刚好等于发送队列的头,则说明我们这个是第一个数据包,则我们直接跳出循环。
		if (skb == tcp_send_head(sk))
			break;

		//如果skb的结束序列号大于我们传递进来的序列号,则说明这个skb包含了我们sack确认的段,因此我们退出循环。
		if (after(TCP_SKB_CB(skb)->end_seq, skip_to_seq))
			break;
		//更新fack的计数。
		state->fack_count += tcp_skb_pcount(skb);
	}
	//返回skb
	return skb;
}

然后是最关键的一个函数tcp_sacktag_walk,这个函数主要是遍历重传队列,找到对应需要设置的段,然后设置tcp_cb的sacked域为TCPCB_SACKED_ACKED,这里要注意,还有一种情况就是sack确认了多个skb,这个时候我们就需要合并这些skb,然后再处理。

然后来看代码。

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
static struct sk_buff *tcp_sacktag_walk(struct sk_buff *skb, struct sock *sk,
					struct tcp_sack_block *next_dup,
					struct tcp_sacktag_state *state,
					u32 start_seq, u32 end_seq,
					int dup_sack_in)
{
	struct tcp_sock *tp = tcp_sk(sk);
	struct sk_buff *tmp;

	//开始遍历skb队列。
	tcp_for_write_queue_from(skb, sk) {
		//in_sack不为0的话表示当前的skb就是我们要设置标记的skb。
		int in_sack = 0;
		int dup_sack = dup_sack_in;

		if (skb == tcp_send_head(sk))
			break;

		//由于skb是有序的,因此如果某个skb的序列号大于sack段的结束序列号,我们就退出循环。
		if (!before(TCP_SKB_CB(skb)->seq, end_seq))
			break;
		//如果存在next_dup,则判断是否需要进入处理。这里就是skb的序列号小于dup的结束序列号
		if ((next_dup != NULL) &&
			before(TCP_SKB_CB(skb)->seq, next_dup->end_seq)) {
			//返回值付给in_sack,也就是这个函数会返回当前skb是否能够被sack的段确认。
			in_sack = tcp_match_skb_to_sack(sk, skb,
							next_dup->start_seq,
							next_dup->end_seq);
			if (in_sack > 0)
				dup_sack = 1;
		}

		//如果小于等于0,则尝试着合并多个skb段(主要是由于可能一个sack段确认了多个skb,这样我们尝试着合并他们)
		if (in_sack <= 0) {
			tmp = tcp_shift_skb_data(sk, skb, state,
						 start_seq, end_seq, dup_sack);
			//这里tmp就为我们合并成功的skb。
			if (tmp != NULL) {
				//如果不等,则我们从合并成功的skb重新开始处理。
				if (tmp != skb) {
					skb = tmp;
					continue;
				}

				in_sack = 0;
			} else {
				//否则我们单独处理这个skb
				in_sack = tcp_match_skb_to_sack(sk, skb,
								start_seq,
								end_seq);
			}
		}

		if (unlikely(in_sack < 0))
			break;
		//如果in_sack大于0,则说明我们需要处理这个skb了。
		if (in_sack) {
			//开始处理skb,紧接着我们会分析这个函数。
			TCP_SKB_CB(skb)->sacked = tcp_sacktag_one(skb, sk,
								  state,
								  dup_sack,
								  tcp_skb_pcount(skb));
			//是否需要更新sack处理的那个最大的skb。
			if (!before(TCP_SKB_CB(skb)->seq,
					tcp_highest_sack_seq(tp)))
				tcp_advance_highest_sack(sk, skb);
		}

		state->fack_count += tcp_skb_pcount(skb);
	}
	return skb;
}

然后我们来看tcp_sacktag_one函数,这个函数用来设置对应的tag,这里所要设置的也就是tcp_cb的sacked域。我们再来回顾一下它的值:

1
2
3
4
5
6
#define TCPCB_SACKED_ACKED      0x01 /* SKB ACK'd by a SACK block    */
#define TCPCB_SACKED_RETRANS    0x02  /* SKB retransmitted        */
#define TCPCB_LOST              0x04  /* SKB is lost          */
#define TCPCB_TAGBITS           0x07  /* All tag bits         */
#define TCPCB_EVER_RETRANS      0x80  /* Ever retransmitted frame */
#define TCPCB_RETRANS     (TCPCB_SACKED_RETRANS|TCPCB_EVER_RETRANS)

如果一切都正常的话,我们最终就会设置skb的这个域为TCPCB_SACKED_ACKED,也就是已经被sack过了。

这个函数处理比较简单,主要就是通过序列号以及sacked本身的值最终来确认sacked要被设置的值。

这里我们还记得,一开始sacked是被初始化为sack option的偏移(如果是正确的sack)的.

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
static u8 tcp_sacktag_one(struct sk_buff *skb, struct sock *sk,
			  struct tcp_sacktag_state *state,
			  int dup_sack, int pcount)
{
	struct tcp_sock *tp = tcp_sk(sk);
	u8 sacked = TCP_SKB_CB(skb)->sacked;
	int fack_count = state->fack_count;

	......

	//如果skb的结束序列号小于发送未确认的,则说明这个帧应当被丢弃。
	if (!after(TCP_SKB_CB(skb)->end_seq, tp->snd_una))
		return sacked;
	//如果当前的skb还未被sack确认过,则我们才会进入处理。
	if (!(sacked & TCPCB_SACKED_ACKED)) {
		//如果是重传被sack确认的。
		if (sacked & TCPCB_SACKED_RETRANS) {
			//如果设置了lost,则我们需要修改它的tag。
			if (sacked & TCPCB_LOST) {
				sacked &= ~(TCPCB_LOST|TCPCB_SACKED_RETRANS);
				//更新lost的数据包
				tp->lost_out -= pcount;
				tp->retrans_out -= pcount;
			}
		} else {
			.......
		}
		//开始修改sacked,设置flag。
		sacked |= TCPCB_SACKED_ACKED;
		state->flag |= FLAG_DATA_SACKED;
		//增加sack确认的包的个数/
		tp->sacked_out += pcount;

		fack_count += pcount;

		//处理fack
		if (!tcp_is_fack(tp) && (tp->lost_skb_hint != NULL) &&
			before(TCP_SKB_CB(skb)->seq,
			   TCP_SKB_CB(tp->lost_skb_hint)->seq))
			tp->lost_cnt_hint += pcount;

		if (fack_count > tp->fackets_out)
			tp->fackets_out = fack_count;
	}

	/* D-SACK. We can detect redundant retransmission in S|R and plain R
	 * frames and clear it. undo_retrans is decreased above, L|R frames
	 * are accounted above as well.
	 */
	if (dup_sack && (sacked & TCPCB_SACKED_RETRANS)) {
		sacked &= ~TCPCB_SACKED_RETRANS;
		tp->retrans_out -= pcount;
	}

	return sacked;
}

最后我们来看tcp_sacktag_write_queue的最后一部分,也就是更新cache的部分。

它也就是将处理过的sack清0,没处理过的保存到cache中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//开始遍历,可以看到这里将将我们未处理的sack段的序列号清0.
for (i = 0; i < ARRAY_SIZE(tp->recv_sack_cache) - used_sacks; i++) {
		tp->recv_sack_cache[i].start_seq = 0;
		tp->recv_sack_cache[i].end_seq = 0;
	}
	//然后保存这次处理了的段。
	for (j = 0; j < used_sacks; j++)
		tp->recv_sack_cache[i++] = sp[j];

	//标记丢失的段。
	tcp_mark_lost_retrans(sk);

	tcp_verify_left_out(tp);

	if ((state.reord < tp->fackets_out) &&
		((icsk->icsk_ca_state != TCP_CA_Loss) || tp->undo_marker) &&
		(!tp->frto_highmark || after(tp->snd_una, tp->frto_highmark)))
		tcp_update_reordering(sk, tp->fackets_out - state.reord, 0);