kk Blog —— 通用基础

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

内核tcp的ack的处理tcp_ack

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

我们来看tcp输入对于ack,段的处理。

  • 先是ack的处理,在内核中,处理ack段是通过tcp_ack来进行的。
    这个函数主要功能是:
  • update重传队列,并基于sack来设置skb的相关buf。
  • update发送窗口。
  • 基于sack的信息或者重复ack来决定是否进入拥塞模式。
    在看之前我们要知道tcp是累积确认的。为了解决带来的缺点,我们才需要sack的。

然后我们来看几个很重要的数据结构,先是tcp_skb_cb,它其实就是表示skb中所保存的tcp的控制信息。而他是保存在skb的cb中的(这个域可以看我前面的blog)。所以这里我们经常会用TCP_SKB_CB来存取这个结构。

1
#define TCP_SKB_CB(__skb)   ((struct tcp_skb_cb *)&((__skb)->cb[0]))

这里还有一个inet_skb_parm,这个结构保存了ipoption的一些信息。

1
2
3
4
5
6
7
8
9
10
11
struct inet_skb_parm
{
	struct ip_options   opt;        /* Compiled IP options      */
	unsigned char       flags;

	#define IPSKB_FORWARDED           1
	#define IPSKB_XFRM_TUNNEL_SIZE    2
	#define IPSKB_XFRM_TRANSFORMED    4
	#define IPSKB_FRAG_COMPLETE       8
	#define IPSKB_REROUTED            16
};

然后来看tcp_skb_cb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct tcp_skb_cb {
	union {
		struct inet_skb_parm    h4;
#if defined(CONFIG_IPV6) || defined (CONFIG_IPV6_MODULE)
		struct inet6_skb_parm   h6;
#endif
	} header;   /* For incoming frames      */
//这个表示当前tcp包的序列号
	__u32       seq;
//这个表示结束序列号,也就是SEQ + FIN + SYN + datalen。
	__u32       end_seq;
//主要用来计算rtt
	__u32       when;
//tcp头的flag(比如syn,fin等),它能取的值,我们下面会介绍。
	__u8        flags;

//SACK/FACK的状态flag或者是sack option的偏移(相对于tcp头的)。我们下面会介绍
	__u8        sacked;
//ack的序列号。
	__u32       ack_seq;
};

下面就是flags所能取的值,可以看到也就是tcp头的控制位。

1
2
3
4
5
6
7
8
#define TCPCB_FLAG_FIN      0x01
#define TCPCB_FLAG_SYN      0x02
#define TCPCB_FLAG_RST      0x04
#define TCPCB_FLAG_PSH      0x08
#define TCPCB_FLAG_ACK      0x10
#define TCPCB_FLAG_URG      0x20
#define TCPCB_FLAG_ECE      0x40
#define TCPCB_FLAG_CWR      0x80

然后是sack/fack的状态标记:

1
2
3
4
5
6
7
8
9
10
//有这个域说明当前的tcpcb是被sack块确认的。
#define TCPCB_SACKED_ACKED  0x01
//表示重传的帧
#define TCPCB_SACKED_RETRANS    0x02
//丢失
#define TCPCB_LOST      0x04
#define TCPCB_TAGBITS       0x07
//重传的帧。
#define TCPCB_EVER_RETRANS  0x80
#define TCPCB_RETRANS       (TCPCB_SACKED_RETRANS|TCPCB_EVER_RETRANS)

这里要注意,当我们接收到正确的SACK后,这个域就会被初始化为sack所在的相对偏移(也就是相对于tcp头的偏移值,这样我们就能很容易得到sack option的位置). 然后是tcp_sock,这个结构保存了我们整个tcp层所需要得所有必要的信息(也就是从sock中提取出来).我们分两个部分来看这个结构,这里只看我们关注的两部分,第一部分是窗口相关的一些域。第二部分是拥塞控制的一些相关域。 先来看窗口相关的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//我们期待从另一台设备接收的下一个数据字节的序列号。
u32 rcv_nxt;
//还没有被读取的数据的序列号。
u32 copied_seq;
//当最后一次窗口update被发送之前我们的rcv_nxt.
u32 rcv_wup;
//将要发送给另一台设备的下一个数据字节的序列号。
u32 snd_nxt;
//已经发送但尚未被确认的第一个数据字节的序列号。
u32 snd_una;
//
u32 snd_sml;
//最后一次接收到ack的时间戳,主要用于keepalive
u32 rcv_tstamp;
//最后一次发送数据包的时间戳。
u32 lsndtime;
//发送窗口长度
u32 snd_wnd;
//接收窗口长度。
u32 rcv_wnd
//发送未确认的数据包的个数(或者字节数?)
u32 packets_out;
//重传的数据包的个数
u32 retrans_out;

然后是拥塞部分,看这里之前还是需要取熟悉一下tcp拥塞控制的相关概念。

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
//慢开始的阀值,也就是超过这个我们就要进入拥塞避免的阶段
u32  snd_ssthresh;
//发送的拥塞窗口
u32 snd_cwnd;
//这个应该是拥塞状态下所发松的数据字节数
u32 snd_cwnd_cnt;
//这里也就是cwnd的最大值
u32 snd_cwnd_clamp;
//这两个值不太理解什么意思。
u32 snd_cwnd_used;
u32 snd_cwnd_stamp;

//接收窗口打消
u32 rcv_wnd;
//tcp的发送buf数据的尾部序列号。
u32 write_seq;
//最后一次push的数据的序列号
u32 pushed_seq;
//丢失的数据包字节数
u32 lost_out;
//sack的数据包的字节数
u32 sacked_out;
//fack处理的数据包的字节数
u32 fackets_out;
u32 tso_deferred;
//计数
u32 bytes_acked;

分析完相关的数据结构我们来看函数的实现。
来看tcp_ack的代码,函数比较大,因此我们分段来看,先来看一开始的一些校验部分。
这里有一个tcp_abc也就是proc下面的可以设置的东西,这个主要是看要不要每个ack都要进行拥塞控制。

Controls Appropriate Byte Count defined in RFC3465. If set to 0 then does congestion avoid once per ACK. 1 is conservative value, and 2 is more aggressive. The default value is 1.

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
struct inet_connection_sock *icsk = inet_csk(sk);
struct tcp_sock *tp = tcp_sk(sk);
//等待ack,也就是发送未确认的序列号。
u32 prior_snd_una = tp->snd_una;
u32 ack_seq = TCP_SKB_CB(skb)->seq;
//得到ack的序列号。
u32 ack = TCP_SKB_CB(skb)->ack_seq;
u32 prior_in_flight;
u32 prior_fackets;
int prior_packets;
int frto_cwnd = 0;

//如果ack的序列号小于发送未确认的,也就是说可能这个ack只是重传老的ack,因此我们忽略它。
if (before(ack, prior_snd_una))
	goto old_ack;

//如果ack大于snd_nxt,也就是它确认了我们还没发送的数据段,因此我们discard这个段。
if (after(ack, tp->snd_nxt))
	goto invalid_ack;
//如果ack大于发送未确认,则设置flag
if (after(ack, prior_snd_una))
	flag |= FLAG_SND_UNA_ADVANCED;

//是否设置tcp_abc,有设置的话,说明我们不需要每个ack都要拥塞避免,因此我们需要计算已经ack的字节数。
if (sysctl_tcp_abc) {
	if (icsk->icsk_ca_state < TCP_CA_CWR)
		tp->bytes_acked += ack - prior_snd_una;
	else if (icsk->icsk_ca_state == TCP_CA_Loss)
		 tp->bytes_acked += min(ack - prior_snd_una,qtp->mss_cache);
}

//得到fack的数据包的字节数
prior_fackets = tp->fackets_out;
//计算还在传输的数据段的字节数,下面会详细分析这个函数。
prior_in_flight = tcp_packets_in_flight(tp);

packets_out这个表示已经发送还没有ack的数据段的字节数(这个值不会重复加的,比如重传的话不会增加这个值)。
sakced_out :sack了的字节数。
lost_out:丢失了的字节数。
retrans_out:重传的字节数。
现在我们就对这个函数的返回值很清楚了,它也就是包含了还没有到达对方的数据段的字节数。

1
2
3
4
5
6
7
8
9
static inline unsigned int tcp_left_out(const struct tcp_sock *tp)
{
	return tp->sacked_out + tp->lost_out;
}

static inline unsigned int tcp_packets_in_flight(const struct tcp_sock *tp)
{
	return tp->packets_out - tcp_left_out(tp) + tp->retrans_out;
}

接下来这一段主要是通过判断flag(slow还是fast)来进行一些窗口的操作。有关slow_path和fast_path的区别,可以看我前面的blog。
fast_path的话很简单,我们就更新相关的域以及snd_wl1(这个域主要是用于update窗口的时候).它这里会被赋值为我们这次的数据包的序列号。然后进行拥塞控制的操作。
snd_wl1是只要我们需要更新发送窗口的话,这个值是都会被更新的。
slow_path的话,我们就需要判断要不要update窗口的大小了。以及是否要处理sack等。
在看下面的代码之前,我们先来看传递进tcp_ack这个函数中的第三个参数flag,这里我们在函数中也还会修改这个值,这个flag也就是当前的skb的类型信息。看了注释后就清楚了。可疑看到好几个都是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
//这个说明当前的输入帧包含有数据。
#define FLAG_DATA       0x01
//这个说明当前的ack是一个窗口更新的ack
#define FLAG_WIN_UPDATE     0x02
//这个ack确认了一些数据
#define FLAG_DATA_ACKED     0x04
//这个表示ack确认了一些我们重传的段。
#define FLAG_RETRANS_DATA_ACKED 0x08
//这个表示这个ack是对syn的回复。
#define FLAG_SYN_ACKED      0x10
//新的sack
#define FLAG_DATA_SACKED    0x20
//ack中包含ECE
#define FLAG_ECE        0x40
//sack检测到了数据丢失。
#define FLAG_DATA_LOST      0x80
//当更新窗口的时候不跳过RFC的检测。
#define FLAG_SLOWPATH       0x100

#define FLAG_ONLY_ORIG_SACKED   0x200
//snd_una被改变了。也就是更新了。
#define FLAG_SND_UNA_ADVANCED   0x400
//包含D-sack
#define FLAG_DSACKING_ACK   0x800
//这个不太理解什么意思。
#define FLAG_NONHEAD_RETRANS_ACKED  0x1000
//
#define FLAG_SACK_RENEGING  0x2000

//下面也就是一些组合。
#define FLAG_ACKED  (FLAG_DATA_ACKED|FLAG_SYN_ACKED)
#define FLAG_NOT_DUP (FLAG_DATA|FLAG_WIN_UPDATE|FLAG_ACKED)
#define FLAG_CA_ALERT       (FLAG_DATA_SACKED|FLAG_ECE)
#define FLAG_FORWARD_PROGRESS   (FLAG_ACKED|FLAG_DATA_SACKED)
#define FLAG_ANY_PROGRESS   (FLAG_FORWARD_PROGRESS|FLAG_SND_UNA_ADVANCED)

然后我们来看代码,下面的代码会设置flag,也就是用上面的宏。
这里有一个很大的不同就是slow_path中,我们需要update窗口的大小,而在fast模式中,我们不需要,这个详细去看我前面的blog介绍的fast和slow的区别。fast就是最理想的情况,因此我们不需要update窗口。

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
//如果不是slowpath并且ack确实是正确的序列号(必须大于snd_una).
	if (!(flag & FLAG_SLOWPATH) && after(ack, prior_snd_una)) {
//更新snd_wl1域为ack_seq;
		tcp_update_wl(tp, ack_seq);
//snd_una更新为ack也就是确认的序列号
		tp->snd_una = ack;
//更新flag域。
		flag |= FLAG_WIN_UPDATE;
//进入拥塞的操作。
		tcp_ca_event(sk, CA_EVENT_FAST_ACK);
................................
	} else {
//这个判断主要是为了判断是否输入帧包含数据。也就是ack还包含了数据,如果有的话,我们设置标记然后后面会处理。
		if (ack_seq != TCP_SKB_CB(skb)->end_seq)
			flag |= FLAG_DATA;
		else
.....................................

//然后进入更新窗口的操作。
		flag |= tcp_ack_update_window(sk, skb, ack, ack_seq);
//然后判断是否有sack段,有的话,我们进入sack段的处理。
		if (TCP_SKB_CB(skb)->sacked)
			flag |= tcp_sacktag_write_queue(sk, skb, prior_snd_una);
//判断是否有ecn标记,如果有的话,设置ecn标记。
		if (TCP_ECN_rcv_ecn_echo(tp, tcp_hdr(skb)))
			flag |= FLAG_ECE;
//进入拥塞的处理。
		tcp_ca_event(sk, CA_EVENT_SLOW_ACK);
	}

接下来这段主要工作是:
1 清理重传队列中的已经ack的段。
2 处理F-RTO。
3 判断是否是零窗口探测的回复ack。
4 检测是否要进入拥塞处理。

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
sk->sk_err_soft = 0;
icsk->icsk_probes_out = 0;
tp->rcv_tstamp = tcp_time_stamp;
//如果发送并且没有ack的数据段的值为0,则说明这个有可能是0窗口探测的回复,因此我们进入no_queue的处理,这个我们紧接着会详细介绍。
prior_packets = tp->packets_out;
if (!prior_packets)
	goto no_queue;
//清理重传队列中的已经ack的数据段。
flag |= tcp_clean_rtx_queue(sk, prior_fackets, prior_snd_una);

//处理F-RTO
if (tp->frto_counter)
	frto_cwnd = tcp_process_frto(sk, flag);

if (before(tp->frto_highmark, tp->snd_una))
	tp->frto_highmark = 0;
//判断ack是否是可疑的。它主要是检测我们是否进入拥塞状态,或者已经处于拥塞状态。
if (tcp_ack_is_dubious(sk, flag)) {
//检测flag以及是否需要update拥塞窗口的大小。
if ((flag & FLAG_DATA_ACKED) && !frto_cwnd &&
	tcp_may_raise_cwnd(sk, flag))
//如果都为真,则更新拥塞窗口。
	tcp_cong_avoid(sk, ack, prior_in_flight);
//这里进入拥塞状态的处理(这个函数是一个非常关键的函数,等到后面详细分析拥塞的时候,会分析到)。
	tcp_fastretrans_alert(sk, prior_packets - tp->packets_out,flag);
} else {
//这里可以看到和上面相比,没有tcp_may_raise_cwnd这个,我们紧接着就会分析到。
	if ((flag & FLAG_DATA_ACKED) && !frto_cwnd)
		tcp_cong_avoid(sk, ack, prior_in_flight);
}
//是否更新neigh子系统。
if ((flag & FLAG_FORWARD_PROGRESS) || !(flag & FLAG_NOT_DUP))
	dst_confirm(sk->sk_dst_cache);

return 1;

no_queue:
//这里判断发送缓冲区是否为空,如果不为空,则我们进入判断需要重启keepalive定时器还是关闭定时器
	if (tcp_send_head(sk))
		tcp_ack_probe(sk);
	return 1;

ok,,接着来看上面略过的几个函数,先来看tcp_ack_is_dubious,这里的条件我们一个个来看
1 说明flag不能是 FLAG_NOT_DUP的, FLAG_NOT_DUP表示我们的ack不是重复的。
2 是flag是CA_ALERT,它的意思是我们是否在我们进入拥塞状态时被alert。
3 拥塞状态不能为TCP_CA_OPEN不为这个,就说明我们已经进入了拥塞状态。
可以看下面这几个宏的定义,就比较清楚了。

1
2
3
4
5
#define FLAG_ACKED  (FLAG_DATA_ACKED|FLAG_SYN_ACKED)
#define FLAG_NOT_DUP (FLAG_DATA|FLAG_WIN_UPDATE|FLAG_ACKED)

//收到sack则说明可能有的段丢失了。而ECE则是路由器提示我们有拥塞了。我们必须处理。
#define FLAG_CA_ALERT       (FLAG_DATA_SACKED|FLAG_ECE)

上面的任意一个为真。就说明ack是可疑的。这里起始也可以说我们就必须进入拥塞的处理了(tcp_fastretrans_alert)

1
2
3
4
static inline int tcp_ack_is_dubious(const struct sock *sk, const int flag)
{
	return (!(flag & FLAG_NOT_DUP) || (flag & FLAG_CA_ALERT) ||inet_csk(sk)->icsk_ca_state != TCP_CA_Open);
}

然后是 tcp_may_raise_cwnd,这个函数用来判断是否需要增大拥塞窗口。
1 不能有ECE flag或者发送的拥塞窗口不能大于slow start的阀值。
3 拥塞状态为RECO或者CWR.

1
2
3
4
5
static inline int tcp_may_raise_cwnd(const struct sock *sk, const int flag)
{
	const struct tcp_sock *tp = tcp_sk(sk);
	return (!(flag & FLAG_ECE) || tp->snd_cwnd < tp->snd_ssthresh) &&!((1 << inet_csk(sk)->icsk_ca_state) & (TCPF_CA_Recovery | TCPF_CA_CWR));
}

在看tcp_ack_update_window函数之前,我们先来看tcp_may_update_window,这个函数用来判断是否需要更新发送窗口。
1 新的数据已经被ack了。
2 当前的数据包的序列号大于当窗口更新的时候那个数据包的序列号。
3 当前的数据包的序列号等于窗口更新时的序列号并且新的窗口大小大于当前的发送窗口大小。这个说明对端可能已经增加了窗口的大小

1
2
3
4
5
6
static inline int tcp_may_update_window(const struct tcp_sock *tp,const u32 ack, const u32 ack_seq,const u32 nwin)
{
	return (after(ack, tp->snd_una) ||
		after(ack_seq, tp->snd_wl1) ||
		(ack_seq == tp->snd_wl1 && nwin > tp->snd_wnd));
}

然后是tcp_ack_update_window函数,这个主要用来更新发送窗口的大小。

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
static int tcp_ack_update_window(struct sock *sk, struct sk_buff *skb, u32 ack, u32 ack_seq)
{
	struct tcp_sock *tp = tcp_sk(sk);
	int flag = 0;
	//得到窗口的大小。
	u32 nwin = ntohs(tcp_hdr(skb)->window);

	if (likely(!tcp_hdr(skb)->syn))
		nwin <<= tp->rx_opt.snd_wscale;

	//判断是否需要update窗口。
	if (tcp_may_update_window(tp, ack, ack_seq, nwin)) {
		flag |= FLAG_WIN_UPDATE;
	//更新snd_wl1
		tcp_update_wl(tp, ack_seq);
	//如果不等于,则说明我们需要更新窗口。
		if (tp->snd_wnd != nwin) {
			tp->snd_wnd = nwin;
	...................................
		}
	}

	tp->snd_una = ack;
	return flag;
}

然后是tcp_cong_avoid函数,这个函数用来实现慢开始和快重传的拥塞算法。

1
2
3
4
5
6
static void tcp_cong_avoid(struct sock *sk, u32 ack, u32 in_flight)
{
	const struct inet_connection_sock *icsk = inet_csk(sk);
	icsk->icsk_ca_ops->cong_avoid(sk, ack, in_flight);
	tcp_sk(sk)->snd_cwnd_stamp = tcp_time_stamp;
}

可以看到它主要是调用cong_avoid回调函数,而这个函数被初始化为tcp_reno_cong_avoid,我们来看这个函数,在看这个函数之前我们要知道一些慢开始和快回复的概念。这些东西随便介绍tcp的书上都有介绍的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void tcp_reno_cong_avoid(struct sock *sk, u32 ack, u32 in_flight)
{
	struct tcp_sock *tp = tcp_sk(sk);
	//是否已经到达拥塞窗口的限制。
	if (!tcp_is_cwnd_limited(sk, in_flight))
		return;
	//如果拥塞窗口还没有到达慢开始的阈值,我们就进入慢开始处理。
	if (tp->snd_cwnd <= tp->snd_ssthresh)
		tcp_slow_start(tp);

	//否则我们就要进入拥塞避免阶段。
	else if (sysctl_tcp_abc) {
	//RFC3465,只有当当前的拥塞窗口的所有段都被ack了,窗口才被允许增加。
	if (tp->bytes_acked >= tp->snd_cwnd*tp->mss_cache) {
		tp->bytes_acked -= tp->snd_cwnd*tp->mss_cache;
			if (tp->snd_cwnd < tp->snd_cwnd_clamp)
				tp->snd_cwnd++;
		}
	} else {
	//和上面处理方式类似。
		tcp_cong_avoid_ai(tp, tp->snd_cwnd);
	}
}

最后我们来看tcp_clean_rtx_queue函数,这个函数主要用于清理发送队列中已经被ack的数据段。函数比较大,我们来分段看。
这里有使用karn算法,也就是如果重传的段,则计算rto的话,不采样这次的值。
还有就是要判断是syn的ack回复,还是数据的ack回复。以及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
57
58
59
60
61
62
63
64
65
66
67
68
while ((skb = tcp_write_queue_head(sk)) && skb != tcp_send_head(sk)) {
	struct tcp_skb_cb *scb = TCP_SKB_CB(skb);
	u32 acked_pcount;
	u8 sacked = scb->sacked;
	//这个说明当前的数据已经在发送未确认的段里面了。
	if (after(scb->end_seq, tp->snd_una)) {
		//这边不是很懂。
		if (tcp_skb_pcount(skb) == 1 ||
			!after(tp->snd_una, scb->seq))
			break;
		acked_pcount = tcp_tso_acked(sk, skb);
		if (!acked_pcount)
			break;
		fully_acked = 0;
	} else {
		acked_pcount = tcp_skb_pcount(skb);
	}
	//如果sack的状态有被设置重传,则我们会使用karn算法。
	if (sacked & TCPCB_RETRANS) {
		//如果标记为sack了重传段,则更新重传的计数。
		if (sacked & TCPCB_SACKED_RETRANS)
			tp->retrans_out -= acked_pcount;
		flag |= FLAG_RETRANS_DATA_ACKED;

		//都为-1,也就是后面计算rtt,不会采样这次值。
		ca_seq_rtt = -1;
		seq_rtt = -1;
			if ((flag & FLAG_DATA_ACKED) || (acked_pcount > 1))
		flag |= FLAG_NONHEAD_RETRANS_ACKED;
	} else {
		//否则根据时间戳得到对应的rtt
		ca_seq_rtt = now - scb->when;
		last_ackt = skb->tstamp;
		if (seq_rtt < 0) {
			seq_rtt = ca_seq_rtt;
		}
		if (!(sacked & TCPCB_SACKED_ACKED))
			reord = min(pkts_acked, reord);
	}
	//如果有sack的数据包被ack确认了,则我们需要减小sack的计数
	if (sacked & TCPCB_SACKED_ACKED)
		tp->sacked_out -= acked_pcount;
	if (sacked & TCPCB_LOST)
		tp->lost_out -= acked_pcount;
	//总得发送为ack的数据字节计数更新。
	tp->packets_out -= acked_pcount;
	pkts_acked += acked_pcount;
	//判断是否为syn的ack。
	if (!(scb->flags & TCPCB_FLAG_SYN)) {
		flag |= FLAG_DATA_ACKED;
	} else {
		//如果是设置标记
		flag |= FLAG_SYN_ACKED;
		tp->retrans_stamp = 0;
	}

	if (!fully_acked)
		break;
	//从写buf,unlink掉。
	tcp_unlink_write_queue(skb, sk);
	//释放内存。
	sk_wmem_free_skb(sk, skb);
	tp->scoreboard_skb_hint = NULL;
	if (skb == tp->retransmit_skb_hint)
		tp->retransmit_skb_hint = NULL;
	if (skb == tp->lost_skb_hint)
		tp->lost_skb_hint = NULL;
}

剩下的部分就是计算rtt的部分,这里就不介绍了。

git-svn

常用

1
2
3
4
1、git-svn clone your_svn_repository
2、git add/commit
3、git-svn rebase    获取中心svn repository的更新;
4、git-svn dcommit   将本地git库的修改同步到中心svn库。

git-svn默认包含在Git的安装包中,不过在Ubuntu中,git-svn是作为一个独立的Package需要额外安装的(sudo apt-get install git-svn)。安装后你就可以使用git svn xxx命令来操作中心SVN代码库了。当然如果你要使用与git svn等价的git-svn命令的话,你还需要将/usr/lib/git-core配置到你的PATH环境变量中,否则Shell会提示你无法找到 git-svn这个命令。

  • 检出一个已存在svn repository(类似于svn checkout)
    我们可以通过git-svn clone命令完成这个操作: git-svn clone your_svn_repository_url

  • 从中心服务器的svn repository获取最新更新
    这个操作可以通过"git-svn rebase"完成。注意这里用的是rebase,而不是update。update命令对于通过git-svn检出的svn repostory的git版本库是不可用的。

  • 查看提交历史日志
    这个简单,使用"git-svn log",加上-v选项,还可以提供每次commit操作涉及的相关文件的详细信息。

  • 将本地代码同步到Svn服务器
    完成这一操作需要通过"git-svn dcommit"命令。这个命令会将你在本地使用git commit提交到本地代码库的所有更改逐一提交到svn库中。加上-n选项,则该命令不会真正执行commit到svn的操作,而是会显示会有哪些本地 变动将被commit到svn服务器。git-svn dcommit似乎不能单独提交某个本地版本的修改,而是一次批量提交所有与svn中心版本库的差异。

下面是一个git-svn的一般使用流程:

1、git-svn clone your_svn_repository;
2、修改本地代码,使用git add/commit将修改提交到本地git库;
3、定期使用git-svn rebase获取中心svn repository的更新;
4、使用git-svn dcommit命令将本地git库的修改同步到中心svn库。

冲突

使用git-svn处理代码冲突的步骤有些繁琐,不过瑕不掩瑜吧。这里用一个小例子来说明一下。

假设某svn中心库上的某个项目foo中只有一个源码文件foo.c:
* 我在使用git-svn clone检出版本时,foo.c当时只有一个commit版本信息:"svn v1";
* clone出来后,我在本地git库中修改foo.c,并通过git commit提交到本地git库中,版本为"git v1";
* 不过与此同时另外一个同事也在修改foo.c这个文件,并已经将他的修改提交到了svn库中,版本为"svn v2";
* 此时我使用git-svn dcommit尝试提交我的改动,git-svn提示我:
Committing to svn://10.10.1.1:80/foo …
M foo.c
事务过时: 过期: ”foo/foo.c“在事务“260-1” at /usr/lib/git-core/git-svn line 570
* 使用git-svn rebase获取svn服务器上的最新foo.c,导致与foo.c冲突,不过此时svn版本信息已经添加到本地git库中(通过git log可以查看),git-svn rebase提示你在解决foo.c的冲突后,运行git rebase –continue完成rebase操作;
* 打开foo.c,修改代码,解决冲突;
* 执行git rebase –continue,git提示我:
You must edit all merge conflicts and then
mark them as resolved using git add
* 执行git add foo.c,告知git已完成冲突解决;
* 再次执行git rebase –continue,提示"Applying: git v1",此时"git v1"版本又一次成功加入本地版本库,你可通过git log查看;
* 执行git-svn dcommit将foo.c的改动同步到svn中心库,到此算是完成一次冲突解决。

  • 设置忽略文件
    要忽略某些文件, 需要首先执行如下命令:
    git config –global core.excludesfile ~/.gitignore
    然后编辑 vi ~/.gitignore.
    例如: 需要忽略vim的临时文件,就写:
    .*.swp

解析pcap数据包格式(code)

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <linux/types.h>

typedef unsigned int  bpf_u_int32;
typedef unsigned short  u_short;
typedef int bpf_int32;
typedef struct pcap_file_header {
	bpf_u_int32 magic;
	u_short version_major;
	u_short version_minor;
	bpf_int32 thiszone;
	bpf_u_int32 sigfigs;   
	bpf_u_int32 snaplen;   
	bpf_u_int32 linktype;  
}pcap_file_header;
 
typedef struct  timestamp{
	bpf_u_int32 timestamp_s;
	bpf_u_int32 timestamp_ms;
}timestamp;
 
typedef struct pcap_header{
	timestamp ts;
	bpf_u_int32 capture_len;
	bpf_u_int32 len;
 
}pcap_header;


#define ETH_ALEN 6
#define __LITTLE_ENDIAN_BITFIELD 1
#define NIPQUAD(addr) \
		((unsigned char *)&addr)[0], \
		((unsigned char *)&addr)[1], \
		((unsigned char *)&addr)[2], \
		((unsigned char *)&addr)[3]


struct ethhdr {
	unsigned char h_dest[ETH_ALEN];       /* destination eth addr */
	unsigned char h_source[ETH_ALEN];     /* source ether addr    */
	unsigned short    h_proto;                /* packet type ID field */
};

struct iphdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
	__u8  ihl:4,
			version:4;
#elif defined (__BIG_ENDIAN_BITFIELD)
	__u8  version:4,
			ihl:4;
#endif
	__u8    tos;
	__be16  tot_len;
	__be16  id;
	__be16  frag_off;
	__u8    ttl;
	__u8    protocol;
	__u16   check;
	__be32  saddr;
	__be32  daddr;
	/*The options start here. */
};

struct tcphdr {
	__u16   source;
	__u16   dest;
	__u32   seq;
	__u32   ack_seq;
#if defined(__LITTLE_ENDIAN_BITFIELD)
	__u16   res1:4,
			doff:4,
			fin:1,
			syn:1,
			rst:1,
			psh:1,
			ack:1,
			urg:1,
			ece:1,
			cwr:1;
#elif defined(__BIG_ENDIAN_BITFIELD)
	__u16   doff:4,
			res1:4,
			cwr:1,
			ece:1,
			urg:1,
			ack:1,
			psh:1,
			rst:1,
			syn:1,
			fin:1;
#endif  
	__u16   window;
	__u16   check;
	__u16   urg_ptr;
};

struct udphdr {
	__u16   source;
	__u16   dest;
	__u16   len;
	__u16   check;
};

struct icmphdr {
	__u8  type;
	__u8  code;
	__u16 checksum;
	union {
		struct {
			__u16   id;
			__u16   sequence;
		} echo;
		__u32   gateway;
		struct {
			__u16   __unused;
			__u16   mtu;
		} frag;
	} un;
};

FILE *fp1, *fp2;
__u32 flag1, flag2, seq1, seq2, ip1, ip2;
long long stime;

void printPcap(int count, void * data, struct pcap_header *ph)
{
	size_t size = ph->capture_len;
	unsigned  short iPos = 0;
	struct ethhdr *eth;
	struct iphdr *iph;
	struct tcphdr *tcph;
	struct udphdr *udph;
	struct icmphdr *icmph;
	__u8 op1, op2, type, len;
	void * data1;
	int i;
	long long dt;

	if (data==NULL) {
		return;
	}
	eth = (struct ethhdr*)(data);
	eth->h_proto = ntohs(eth->h_proto);
	//printf("Ether:\tproto = 0x%x\n", eth->h_proto);
	if (eth->h_proto == 0x0800) { // IP
		iph = (struct iphdr*)(data+sizeof(struct ethhdr));
		if (iph->protocol == IPPROTO_TCP) { // tcp
			tcph = (struct tcphdr*)(data+sizeof(struct ethhdr)+sizeof(struct iphdr));
			if (tcph->ack == 0 && tcph->syn == 1) {
				seq1 = ntohl(tcph->seq);
				ip1 = iph->saddr;
				flag1 = 1;
				stime = 1000000LL*ph->ts.timestamp_s + ph->ts.timestamp_ms;
			} else if (tcph->ack == 1 && tcph->syn == 1) {
				seq2 = ntohl(tcph->seq);
				ip2 = iph->saddr;
				flag2 = 1;
			}

			if (flag1 == 0) {
				flag1 = 1;
				seq1 = ntohl(tcph->seq)-1;
				ip1 = iph->saddr;
				stime = 1000000LL*ph->ts.timestamp_s + ph->ts.timestamp_ms;
			}
			if (flag2 == 0 && iph->saddr != ip1) {
				flag2 = 1;
				seq2 = ntohl(tcph->seq)-1;
				ip2 = iph->saddr;
			}

			dt = (1000000LL*ph->ts.timestamp_s+ph->ts.timestamp_ms) - stime;
			fprintf(fp1, "%d\t%llu\t", count, dt/1000);
			fprintf(fp1, "%d.%d.%d.%d\t%d.%d.%d.%d\t%u\t%u\t%d\t",
						NIPQUAD(iph->saddr), NIPQUAD(iph->daddr),
						(iph->saddr==ip1?ntohl(tcph->seq)-seq1:ntohl(tcph->seq)-seq2),
						(iph->saddr==ip1?ntohl(tcph->ack_seq)-seq2:ntohl(tcph->ack_seq)-seq1),
						ntohs(iph->tot_len)-iph->ihl*4-tcph->doff*4 + tcph->syn + tcph->fin);
			fprintf(fp1, "%d\t%d\t%d\t%d\t", ntohs(iph->tot_len), iph->ihl*4, tcph->doff*4, iph->ttl);
			fprintf(fp1, "%u\t%u\t%d\t%d\t%d\t",
						ntohl(tcph->seq), ntohl(tcph->ack_seq),
						tcph->ack, tcph->syn, ntohs(tcph->window));

			if (tcph->doff > 5) {
				data1 = data + sizeof(struct ethhdr)+sizeof(struct iphdr)+sizeof(struct tcphdr);
				op1 = *(__u8*)(data1);
				op2 = *(__u8*)(data1+1);
				type = *(__u8*)(data1+2);
				len = *(__u8*)(data1+3);
				//fprintf(fp1, "%u\t%u\t%u\t%u\n", op1, op2, type, len);
				if (op1 == 1 && op2 == 1 && type == 5) { // sack
					i = 4;
					while (i < len+2) {
						if (i > 4) fprintf(fp1, " ");
						fprintf(fp1, "%u-%u", ntohl(*(__u32*)(data1+i))-seq2, ntohl(*(__u32*)(data1+i+4))-seq2);
						i += 8;
					}
				}
			}
			fprintf(fp1, "\n");

		} else if (iph->protocol == IPPROTO_UDP) { // udp
			udph = (struct udphdr*)(data+sizeof(struct ethhdr)+sizeof(struct iphdr));
			//printf("UDP:\tsource=%u\tdest=%u\tlen=%d\n", ntohs(udph->source), ntohs(udph->dest), ntohs(udph->len));
		} else if (iph->protocol == IPPROTO_ICMP) { // ICMP
			icmph = (struct icmphdr*)(data+sizeof(struct ethhdr)+sizeof(struct iphdr));
			//printf("ICMP:\ttype=%u\tcode=%u\n", icmph->type, icmph->code);
		}
	}
}

#define MAX_ETH_FRAME 1514000
int main (int argc, const char * argv[])
{
	pcap_file_header  pfh;
	pcap_header  ph;
	int count=0;
	void * buff = NULL;
	int readSize=0;
	int ret = 0;
 
	 if (argc != 2) {
		 printf("uage: ./a.out pcap_filename\n");
		 return -1;
	 }
	FILE *fp = fopen(argv[1], "r");
	if (fp==NULL) {
		fprintf(stderr, "Open file %s error.", argv[1]);
		return -1;
	}
	fread(&pfh, sizeof(pcap_file_header), 1, fp);
 
	fp1 = fopen("out", "w");
	fprintf(fp1, "#ID\tTIME\tsaddr\tdaddr\tseq\tack_seq\tpayload\ttot_len\tihl\tdoff\tttl\tseq\tack_seq\tack\tsyn\twin\tSACK\n");
	buff = (void *)malloc(MAX_ETH_FRAME);
	flag1 = flag2 = 0;

	for (count=1; ; count++) {
		memset(buff,0,MAX_ETH_FRAME);
		readSize=fread(&ph, sizeof(pcap_header), 1, fp);
		if (readSize<=0) {
			break;
		}
		if (buff==NULL) {
			fprintf(stderr, "malloc memory failed.\n");
			return -1;
		}
 
		readSize=fread(buff,1,ph.capture_len, fp);
		if (readSize != ph.capture_len) {
			free(buff);
			fprintf(stderr, "pcap file parse error.\n");
			return -1;
		}
		printPcap(count, buff, &ph);

		if (feof(fp) || readSize <=0 ) {
			break;
		}
	}
	fclose(fp);
	fclose(fp1);
	return ret;
}

解析pcap数据包格式

协议是一个比较复杂的协议集,有很多专业书籍介绍。在此,我仅介绍其与编程密切相关的部分:以太网上TCP/IP协议的分层结构及其报文格式。
我们知道TCP/IP协议采用分层结构,其分层模型及协议如下表:
应 用 层 (Application) HTTP、Telnet、FTP、SMTP、SNMP
传 输 层 (Transport) TCP、UDP
网 间 网层 (Internet) IP【ARP、RARP、ICMP】
网络接口层 (Network) Ethernet、X.25、SLIP、PPP

协议采用分层结构,因此,数据报文也采用分层封装的方法。下面以应用最广泛的以太网为例说明其数据报文分层封装,如下图所示:

任何通讯协议都有独特的报文格式,TCP/IP协议也不例外。对于通讯协议编程,我们首先要清楚其报文格式。由于TCP/IP协议采用分层模型,各层都有专用的报头,以下就简单介绍以太网下TCP/IP各层报文格式。

8字节的前导用于帧同步,CRC域用于帧校验。这些用户不必关心其由网卡芯片自动添加。目的地址和源地址是指网卡的物理地址,即MAC地址,具有唯一性。帧类型或协议类型是指数据包的高级协议,如 0x0806表示ARP协议,0x0800表示IP协议等。

  ARP/RARP(地址解析/反向地址解析)报文格式如下图:

“硬件类型”域指发送者本机网络接口类型(值“1”代表以太网)。“协议类型”域指发送者所提供/请求的高级协议地址类型(“0x0800”代表 IP协议)。“操作”域指出本报文的类型(“1”为ARP请求,“2”为ARP响应,“3”为RARP请求,“4”为RARP响应)。

  IP数据报头格式如下图:

  我们用单片机实现TCP/IP协议要作一些简化,不考虑数据分片和优先权。因此,在此我们不讨论服务类型和标志偏移域,只需填“0” 即可。协议“版本”为4,“头长度”单位为32Bit,“总长度”以字节为单位,表示整个IP数据报长度。“标识”是数据包的ID号,用于识别不同的IP 数据包。“生存时间” TTL是个数量及的概念,防止无用数据包一直存在网络中。一般每经过路由器时减一,因此通过TTL 可以算出数据包到达目的地所经过的路由器个数。“协议”域表示创建该数据包的高级协议类型。如 1表示ICMP协议,6表示TCP协议,17表示 UDP协议等。IP数据包为简化数据转发时间,仅采用头校验的方法,数据正确性由高层协议保证。

  ICMP(网间网控制报文协议)协议 应用广泛。在此仅给出最常见的回应请求与应答报文格式,用户命令ping便是利用此报文来测试信宿机的可到达性。报文格式如下图所示:

  类型0 为回应应答报文,8 为回应请求报文。整个数据包均参与检验。注意ICMP封装在IP数据包里传送。

  UDP报文格式如下图:

  TCP报文格式如下图:


WireShark捕获的数据

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
		以下为物理层的数据帧概况

Frame 1 (62 bytes on wire, 62 bytes captured)           1号帧,线路62字节,实际捕获62字节
Arrival Time: Jan 21, 2008 15:17:33.910261000           捕获日期和时间
[Time delta from previous packet:0.00000 seconds]       此包与前一包的时间间隔
[Time since reference or first frame: 0.00 seconds]     此包与第1帧的间隔时间
Frame Number: 1                                         帧序号
Packet Length: 62 bytes                                 帧长度
Capture Length: 62 bytes                                捕获长度
[Frame is marked: False]                                此帧是否做了标记:否
[Protocols in frame: eth:ip:tcp]                        帧内封装的协议层次结构
[Coloring Rule Name: HTTP]                              用不同颜色染色标记的协议名称:HTTP
[Coloring Rule String: http || tcp.port == 80]          染色显示规则的字符串:


		以下为数据链路层以太网帧头部信息
Ethernet II, Src: AcerTech_5b:d4:61 (00:00:e2:5b:d4:61), Dst: Jetcell_e5:1d:0a (00:d0:2b:e5:1d:0a)
以太网协议版本II,源地址:厂名_序号(网卡地址),目的:厂名_序号(网卡地址)
 Destination: Jetcell_e5:1d:0a (00:d0:2b:e5:1d:0a)       目的:厂名_序号(网卡地址)
 Source: AcerTech_5b:d4:61 (00:00:e2:5b:d4:61)           源:厂名_序号(网卡地址)
 Type: IP (0x0800)                                       帧内封装的上层协议类型为IP(十六进制码0800)看教材70页图3.2

		以下为互联网层IP包头部信息
Internet Protocol, Src: 202.203.44.225 (202.203.44.225), Dst: 202.203.208.32 (202.203.208.32)
互联网协议,源IP地址,目的IP地址
Version: 4                                                       互联网协议IPv4
Header length: 20 bytes                                          IP包头部长度
Differentiated Services Field:0x00(DSCP 0x00:Default;ECN:0x00)   差分服务字段
Total Length: 48                                                 IP包的总长度
Identification:0x8360 (33632)                                    标志字段
Flags:                                                           标记字段(在路由传输时,是否允许将此IP包分段)
Fragment offset: 0                                               分段偏移量(将一个IP包分段后传输时,本段的标识)
Time to live: 128                                                生存期TTL
Protocol: TCP (0x06)                                             此包内封装的上层协议为TCP
Header checksum: 0xe4ce [correct]                                头部数据的校验和
Source: 202.203.44.225 (202.203.44.225)                          源IP地址
Destination: 202.203.208.32 (202.203.208.32)                     目的IP地址

		以下为传输层TCP数据段头部信息
Transmission Control Protocol, Src Port: 2764 (2764), Dst Port: http (80), Seq: 0, Len: 0   传输控制协议TCP的内容
Source port: 2764 (2764)                              源端口名称(端口号)
Destination port: http (80)                            目的端口名http(端口号80)
Sequence number: 0    (relative sequence number)       序列号(相对序列号)
Header length: 28 bytes                                头部长度
Flags: 0x02 (SYN)                                      TCP标记字段(本字段是SYN,是请求建立TCP连接)
Window size: 65535                                     流量控制的窗口大小
Checksum: 0xf73b [correct]                             TCP数据段的校验和
Options: (8 bytes)                                     可选项

linux内核调试转储工具kdump crash

http://www.ibm.com/developerworks/cn/linux/l-cn-kdump4/index.html

1
2
3
4
5
$ crash vmlinux vmcore 
crash> bt
crash> dis -l ffffffff80081000
crash> gdb x/8ub ffffffff90091000
......

如果是未完成文件可以尝试以最小方式调试

1
2
$ crash --minimal vmlinux vmcore
crash> log
1
2
3
4
5
6
7
8
9
10
11
crash_H_args_xbt> mod -S
 MODULE   NAME         SIZE  OBJECT FILE
c8019000  soundcore   2788  /lib/modules/2.2.5-15/misc/soundcore.o
。。。
crash_H_args_xbt> mod -s soundcore
 MODULE   NAME         SIZE  OBJECT FILE
c8019000  soundcore   2788  /lib/modules/2.2.5-15/misc/soundcore.o
crash_H_args_xbt> mod -d soundcore
crash_H_args_xbt> mod -s soundcore /tmp/soundcore.o
 MODULE   NAME         SIZE  OBJECT FILE
c8019000  soundcore   2788  /tmp/soundcore.o

1、kdump介绍与设置

1)介绍:

Kdump 是一种基于 kexec 的内存转储工具,目前它已经被内核主线接收,成为了内核的一部分,它也由此获得了绝大多数 Linux 发行版的支持。与传统的内存转储机制不同不同,基于 Kdump 的系统工作的时候需要两个内核,一个称为系统内核,即系统正常工作时运行的内核;另外一个称为捕获内核,即正常内核崩溃时,用来进行内存转储的内核。

安装crash,kexec-tools

2)设置

查看/boot/grub/grub.conf文件中kernel一行最后是否有crashkernel=128M@64M,如果没有,添加上去,重启
如何设定 crashkernel 参数
在 kdump 的配置中,往往困惑于 crashkernel 的设置。“crashkernel=X@Y”,X 应该多大? Y 又应该设在哪里呢?实际我们 可以完全省略“@Y”这一部分,这样,kernel 会为我们自动选择一个起始地址。而对于 X 的大小,般对 i386/x86_64 的系统, 设为 128M 即可;对于 powerpc 的系统,则要设为 256M。rhel6 引入的“auto”已经要被放弃了,代之以原来就有的如下语法:

1
2
3
4
5
6
crashkernel=<range1>:<size1>[,<range2>:<size2>,...][@offset] 
		  range=start-[end] 
		  'start' is inclusive and 'end' is exclusive. 

		  For example: 
		  crashkernel=512M-2G:64M,2G-:128M

如何判断捕获内核是否加载
可通过查看 /sys/kernel/kexec_crash_loaded 的值。“1”为已经加载,“0”为还未加载。
缩小 crashkernel
可以通过向 /sys/kernel/kexec_crash_size 中输入一个比其原值小的数来缩小甚至完全释放 crashkernel。

3)测试kdump是否可用

执行

1
2
echo 1 > /proc/sys/kernel/sysrq
echo c > /proc/sysrq-trigger

经过两次自动重启后,查看/var/crash/目录下是否有vmcore文件生成,如果有表示kdump可用

2、生成带调试信息的vmlinux文件

1)

centos: debuginfo.centos.org

2)按顺序安装

kernel-debuginfo-common-2.6.18-194.3.1.el5.i686.rpm和kernel-debuginfo-2.6.18-194.3.1.el5.i686.rpm, 之后会在/usr/lib/debug/lib/modules/2.6.18-194.3.1.el5/下生产vmlinux文件
或在源码里make binrpm-pkg -j8,然后该目录下会生成一个vmlinux
在编译内核之前,需要确认.config中,以下编译选项是否打开:

(1)CONFIG_DEBUG_INFO ,必须打开该选项,否则crash会出现以下错误:
crash no debugging data available
(2)CONFIG_STRICT_DEVMEM,必须打开该选项,否则crash会出现以下错误:
crash: read error: kernel virtual address: c0670680 type: “kernel_config_data”
WARNING: cannot read kernel_config_data
crash: read error: kernel virtual address: c066bb68 type: “cpu_possible_mask”

3、进入vmlinux所在目录,

执行crash /var/crash/2012-03-13-21:05/vmcore vmlinux
mod -S XXX –导入XXX目录下所有符号
log –查看日志文件,找到最后一条,如EIP: [] bshtej_interrupt+0x103f/0x11cb [tej21] SS:ESP 0068:c0768f38
l* bshtej_interrupt+0x103f 出现如下内容

1
2
3
4
5
6
7
8
9
10
11
0xf8ee53f5 is in bshtej_interrupt (/opt/dahdi-linux-complete-2.2.1+2.2.1/linux/drivers/dahdi/tej21/tej21.c:2910).
2904          int c, x;
2905
2906
2907          for(c = 0; c < MAX_CARDS; c++)
2908          {
2909              if (!cards[c]) break;
2910              for (x=0;x<cards[c]->numspans;x++) {
2911                  if (cards[c]->tspans[x]->sync)
2912                  {
2913

到此可确定死机问题出现在2910行。

4、设置过滤等级:

vmcore文件一般会收集内核崩溃时的各种信息,所以生成时间会较长,文件比较大,如果不需要某些信息的话,可对kdump.conf文件进行配置

1
vim  /etc/kdump.conf

将core_collector makedumpfile -c 这行打开,并加上-d 31,参数说明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-c: Compress dump data by each page.
-d: Specify the type of unnecessary page for analysis.
  Dump  | zero    cache   cache   user    free
  Level | page    page    private data    page
  -------+---------------------------------------
  0  |
  1  |    X
  2  |        X
  4  |        X   X
  8  |                X
  16  |                   X
  31  |   X   X   X   X   X

##### 5、根据Oops值大致判断错误:
Oops的错误代码根据错误的原因会有不同的定义如果发现自己遇到的Oops和下面无法对应的话,最好去内核代码里查找:
  • error_code:
  • bit 0 == 0 means no page found, 1 means protection fault
  • bit 1 == 0 means read, 1 means write
  • bit 2 == 0 means kernel, 1 means user-mode
  • bit 3 == 0 means data, 1 means instruction ```