kk Blog —— 通用基础

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

Thunderbird

Mozilla Thunderbird 最小化附加组建

在工具->附加组建中
1、搜索minimize,安装firetray
2、firetray 首选项->邮件->邮件通知类型 选第二个:显示新邮件图示
不想最小化到托盘: firetray 首选项->视窗 去掉第二个勾选

Mozilla Thunderbird 导入导出邮件

一种Mozilla Thunderbird的扩展ImportExportTools。
https://addons.mozilla.org/zh-CN/thunderbird/addon/importexporttools/

安装流程 (本地安装)
1
2
3
4
5
1 进入Mozilla Thunderbird
2 点击菜单栏的“工具”->“附加软件”
3 从附加组件搜索框旁边的选项按钮中,选择“从文件安装附加组件...”选项
4 选择本地文件(ImportExportTools-X.X.xpi)位置 安装
5 安装完成 重启Mozilla Thunderbird
如何用呢
1
2
3
4
5
1 进入Mozilla Thunderbird
2 右键“收件箱”
3 “导入/导出”
4 “汇出此资料夹所以的邮件” 进行选择格式
5 汇出完成 查看本地文件夹

如何知道文件被那个进程写

一个文件正在被进程写 我想查看这个进程 文件一直在增大 找不到谁在写 使用lsof也没找到

这个问题挺有普遍性的,解决方法应该很多,这里我给大家提个比较直观的方法。

linux下每个文件都会在某个块设备上存放,当然也都有相应的inode, 那么透过vfs.write我们就可以知道谁在不停的写入特定的设备上的inode。

幸运的是systemtap的安装包里带了inodewatch.stp,位于/usr/local/share/doc/systemtap/examples/io目录下,就是用来这个用途的。

我们来看下代码:

$ cat inodewatch.stp

1
2
3
4
5
6
7
8
9
10
#! /usr/bin/env stap
 
probe vfs.write, vfs.read
{
	# dev and ino are defined by vfs.write and vfs.read
	if (dev == MKDEV($1,$2) # major/minor device
		&& ino == $3)
	printf ("%s(%d) %s 0x%x/%u\n",
		execname(), pid(), probefunc(), dev, ino)
}

这个脚本的使用方法如下: stap inodewatch.stp major minor ino

下面我们构造个场景: dd不停的写入一个文件,查出这个文件的ino, 以及它所在设备的major, minor, 运行stap脚本就可以得到答案。

场景交代好了,我们来演示下:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ pwd
/home/chuba
$ df
Filesystem           1K-blocks      Used Available Use% Mounted on
...
/dev/sdb1            1621245336 825209568 713681236  54% /home
...
$ ls -al /dev/sdb1
brw-rw---- 1 root disk 8, 17 Oct 24 11:22 /dev/sdb1 
$ rm -f test.dat && dd if=/dev/zero of=test.dat
^C9912890+0 records in
9912890+0 records out
5075399680 bytes (5.1 GB) copied, 26.8189 s, 189 MB/s

这个终端模拟文件的不停写入,同时在另外一个终端查验谁干的。这里我们已经知道设备的major/minor为8/17

1
2
3
4
5
6
7
8
9
10
$ stat -c '%i' test.dat
25337884
$ sudo stap /usr/local/share/doc/systemtap/examples/io/inodewatch.stp 8 17 25337884
dd(740) vfs_write 0x800011/25337884
dd(740) vfs_write 0x800011/25337884
dd(740) vfs_write 0x800011/25337884
dd(740) vfs_write 0x800011/25337884
dd(740) vfs_write 0x800011/25337884
dd(740) vfs_write 0x800011/25337884
...

看到了吧,dd是罪魁祸首,pid是740

通过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"
这个没什么大问题 字体样式