linux内核tcp的定时器管理(一)

 

linux内核tcp的定时器管理(一)

在内核中tcp协议栈有6种类型的定时器:

1 重传定时器。

2 delayed ack定时器

3 零窗口探测定时器

上面三种定时器都是作为tcp状态机的一部分来实现的。

4 keep-alive 定时器

主要是管理established状态的连接。

5 time_wait定时器

主要是用来客户端关闭时的time_wait状态用到。

6 syn-ack定时器(主要是用在listening socket)

管理新的连接请求时所用到。


而在内核中,tcp协议栈管理定时器主要有下面4个函数:

inet_csk_reset_xmit_timer

这个函数是用来重启定时器

inet_csk_clear_xmit_timer

这个函数用来删除定时器。

上面两个函数都是针对状态机里面的定时器。

tcp_set_keepalive

这个函数是用来管理keepalive 定时器的接口。

tcp_synack_timer

这个函数是用来管理syn_ack定时器的接口。


ok,我们现在先来看定时器的初始化。

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

Java代码 复制代码  收藏代码
  1. void tcp_init_xmit_timers(struct sock *sk)   
  2. {   
  3.     inet_csk_init_xmit_timers(sk, &tcp_write_timer, &tcp_delack_timer,   
  4.                   &tcp_keepalive_timer);   
  5. }  
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。

Java代码 复制代码  收藏代码
  1. void inet_csk_init_xmit_timers(struct sock *sk,   
  2.                    void (*retransmit_handler)(unsigned long),   
  3.                    void (*delack_handler)(unsigned long),   
  4.                    void (*keepalive_handler)(unsigned long))   
  5. {   
  6.     struct inet_connection_sock *icsk = inet_csk(sk);   
  7.   
  8. ///安装定时器,设置定时器的回掉函数。   
  9.     setup_timer(&icsk->icsk_retransmit_timer, retransmit_handler,   
  10.             (unsigned long)sk);   
  11.     setup_timer(&icsk->icsk_delack_timer, delack_handler,   
  12.             (unsigned long)sk);   
  13.     setup_timer(&sk->sk_timer, keepalive_handler, (unsigned long)sk);   
  14.     icsk->icsk_pending = icsk->icsk_ack.pending = 0;   
  15. }  
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层,而每次当它发送完毕(返回正确),则它就会启动重传定时器,我们来看代码:


Java代码 复制代码  收藏代码
  1. static int tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,   
  2.               int push_one, gfp_t gfp)   
  3. {   
  4.     struct tcp_sock *tp = tcp_sk(sk);   
  5.     struct sk_buff *skb;   
  6.     unsigned int tso_segs, sent_pkts;   
  7.     int cwnd_quota;   
  8.     int result;   
  9.   
  10. .............................................   
  11.   
  12.     while ((skb = tcp_send_head(sk))) {   
  13. ..................................................   
  14.   
  15. ///可以看到只有当传输成功,我们才会走到下面的函数。   
  16.         if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp)))   
  17.             break;   
  18.   
  19.         /* Advance the send_head.  This one is sent out.  
  20.          * This call will increment packets_out.  
  21.          */  
  22. ///最终在这个函数中启动重传定时器。   
  23.         tcp_event_new_data_sent(sk, skb);   
  24.   
  25.         tcp_minshall_update(tp, mss_now, skb);   
  26.         sent_pkts++;   
  27.   
  28.         if (push_one)   
  29.             break;   
  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,如何启动定时器的.

Java代码 复制代码  收藏代码
  1. static void tcp_event_new_data_sent(struct sock *sk, struct sk_buff *skb)   
  2. {   
  3.     struct tcp_sock *tp = tcp_sk(sk);   
  4.     unsigned int prior_packets = tp->packets_out;   
  5.   
  6.     tcp_advance_send_head(sk, skb);   
  7.     tp->snd_nxt = TCP_SKB_CB(skb)->end_seq;   
  8.   
  9.     /* Don't override Nagle indefinately with F-RTO */  
  10.     if (tp->frto_counter == 2)   
  11.         tp->frto_counter = 3;   
  12. ///关键在这里.   
  13.     tp->packets_out += tcp_skb_pcount(skb);   
  14.     if (!prior_packets)   
  15.         inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,   
  16.                       inet_csk(sk)->icsk_rto, TCP_RTO_MAX);   
  17. }  
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,这个值表示当前还未确认的段的个数。然后来进行相关操作。

Java代码 复制代码  收藏代码
  1. static void tcp_rearm_rto(struct sock *sk)   
  2. {   
  3.     struct tcp_sock *tp = tcp_sk(sk);   
  4.   
  5. ///为0说明所有的传输的段都已经acked。此时remove定时器。否则重启定时器。   
  6.     if (!tp->packets_out) {   
  7.         inet_csk_clear_xmit_timer(sk, ICSK_TIME_RETRANS);   
  8.     } else {   
  9.         inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,   
  10.                       inet_csk(sk)->icsk_rto, TCP_RTO_MAX);   
  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窗口定时器。

Java代码 复制代码  收藏代码
  1. #define ICSK_TIME_RETRANS   1   /* Retransmit timer */  
  2. #define ICSK_TIME_PROBE0    3   /* Zero window probe timer */  
  3.   
  4.   
  5. static void tcp_write_timer(unsigned long data)   
  6. {   
  7.     struct sock *sk = (struct sock *)data;   
  8.     struct inet_connection_sock *icsk = inet_csk(sk);   
  9.     int event;   
  10.   
  11. ///首先加锁。   
  12.     bh_lock_sock(sk);   
  13. ///如果是进程空间则什么也不做。   
  14.     if (sock_owned_by_user(sk)) {   
  15.         /* Try again later */  
  16.         sk_reset_timer(sk, &icsk->icsk_retransmit_timer, jiffies + (HZ / 20));   
  17.         goto out_unlock;   
  18.     }   
  19.   
  20. ///如果状态为close或者icsk_pending为空,则什么也不做。   
  21.     if (sk->sk_state == TCP_CLOSE || !icsk->icsk_pending)   
  22.         goto out;   
  23. ///如果超时时间已经过了,则重启定时器。   
  24.   
  25.     if (time_after(icsk->icsk_timeout, jiffies)) {   
  26.         sk_reset_timer(sk, &icsk->icsk_retransmit_timer, icsk->icsk_timeout);   
  27.         goto out;   
  28.     }   
  29. ///取出定时器类型。   
  30.     event = icsk->icsk_pending;   
  31.     icsk->icsk_pending = 0;   
  32.   
  33. ///通过判断event来确定进入那个函数进行处理。   
  34.     switch (event) {   
  35.     case ICSK_TIME_RETRANS:   
  36.         tcp_retransmit_timer(sk);   
  37.         break;   
  38.     case ICSK_TIME_PROBE0:   
  39.         tcp_probe_timer(sk);   
  40.         break;   
  41.     }   
  42.     TCP_CHECK_TIMER(sk);   
  43.   
  44. out:   
  45.     sk_mem_reclaim(sk);   
  46. out_unlock:   
  47.     bh_unlock_sock(sk);   
  48.     sock_put(sk);   
  49. }  
#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算法,也就是定时器退避策略。下面的代码最后部分会修改定时器的值,这里是增加一倍。

Java代码 复制代码  收藏代码
  1. static void tcp_retransmit_timer(struct sock *sk)   
  2. {   
  3.     struct tcp_sock *tp = tcp_sk(sk);   
  4.     struct inet_connection_sock *icsk = inet_csk(sk);   
  5.   
  6. ///如果没有需要确认的段,则什么也不做。   
  7.     if (!tp->packets_out)   
  8.         goto out;   
  9.   
  10.     WARN_ON(tcp_write_queue_empty(sk));   
  11.   
  12. /**首先进行一些合法性判断,其中:   
  13.  * snd_wnd为窗口大小。   
  14.  * sock_flag用来判断sock的状态。   
  15.  * 最后一个判断是当前的连接状态不能处于syn_sent和syn_recv状态,也就是连接还未建                * 立状态.   
  16.     if (!tp->snd_wnd && !sock_flag(sk, SOCK_DEAD) &&   
  17.         !((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV))) {   
  18. ///tcp_time_stamp也就是jifes,而rcv_tstamp表示最后一个ack接收的时间,也就是最后一次对端确认的时间。因此这两个时间之差不能大于tcp_rto_max,因为tcp_rto_max为我们重传定时器的间隔时间的最大值。   
  19.         if (tcp_time_stamp - tp->rcv_tstamp > TCP_RTO_MAX) {   
  20.             tcp_write_err(sk);   
  21.             goto out;   
  22.         }   
  23. ///这个函数用来进入loss状态,也就是进行一些拥塞以及流量的控制。   
  24.         tcp_enter_loss(sk, 0);   
  25. ///现在开始重传skb。   
  26.         tcp_retransmit_skb(sk, tcp_write_queue_head(sk));   
  27.         __sk_dst_reset(sk);   
  28. ///然后重启定时器,继续等待ack的到来。   
  29.         goto out_reset_timer;   
  30.     }   
  31.   
  32. ///程序到达这里说明上面的校验失败,因此下面这个函数用来判断我们重传需要的次数。如果超过了重传次数,直接跳转到out。   
  33.     if (tcp_write_timeout(sk))   
  34.         goto out;   
  35.   
  36. ///到达这里说明我们重传的次数还没到。icsk->icsk_retransmits表示重传的次数。   
  37.     if (icsk->icsk_retransmits == 0) {   
  38. ///这里其实也就是收集一些统计信息。   
  39.         int mib_idx;   
  40.   
  41.         if (icsk->icsk_ca_state == TCP_CA_Disorder) {   
  42.             if (tcp_is_sack(tp))   
  43.                 mib_idx = LINUX_MIB_TCPSACKFAILURES;   
  44.             else  
  45.                 mib_idx = LINUX_MIB_TCPRENOFAILURES;   
  46.         } else if (icsk->icsk_ca_state == TCP_CA_Recovery) {   
  47.             if (tcp_is_sack(tp))   
  48.                 mib_idx = LINUX_MIB_TCPSACKRECOVERYFAIL;   
  49.             else  
  50.                 mib_idx = LINUX_MIB_TCPRENORECOVERYFAIL;   
  51.         } else if (icsk->icsk_ca_state == TCP_CA_Loss) {   
  52.             mib_idx = LINUX_MIB_TCPLOSSFAILURES;   
  53.         } else {   
  54.             mib_idx = LINUX_MIB_TCPTIMEOUTS;   
  55.         }   
  56.         NET_INC_STATS_BH(sock_net(sk), mib_idx);   
  57.     }   
  58.   
  59. ///是否使用f-rto算法。   
  60.     if (tcp_use_frto(sk)) {   
  61.         tcp_enter_frto(sk);   
  62.     } else {   
  63. ///否则处理sack.   
  64.         tcp_enter_loss(sk, 0);   
  65.     }   
  66.   
  67. /// 再次尝试重传队列的第一个段。   
  68.     if (tcp_retransmit_skb(sk, tcp_write_queue_head(sk)) > 0) {   
  69. ///重传失败。   
  70.         if (!icsk->icsk_retransmits)   
  71.             icsk->icsk_retransmits = 1;   
  72.         inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,   
  73.                       min(icsk->icsk_rto, TCP_RESOURCE_PROBE_INTERVAL),   
  74.                       TCP_RTO_MAX);   
  75.         goto out;   
  76.     }   
  77. ///icsk->icsk_backoff主要用在零窗口定时器。   
  78.     icsk->icsk_backoff++;   
  79. ///icsk_retransmits也就是重试次数。   
  80.     icsk->icsk_retransmits++;   
  81.   
  82. out_reset_timer:   
  83. ///计算rto,并重启定时器,这里使用karn算法,也就是下次超时时间增加一倍/   
  84.     icsk->icsk_rto = min(icsk->icsk_rto << 1, TCP_RTO_MAX);   
  85. ///重启定时器,可以看到超时时间就是我们上面的icsk_rto.   
  86.     inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, icsk->icsk_rto, TCP_RTO_MAX);   
  87.     if (icsk->icsk_retransmits > sysctl_tcp_retries1)   
  88.         __sk_dst_reset(sk);   
  89.   
  90. out:;   
  91. }  
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,我们重试的最大的次数就是它。

下面来看代码:


Java代码 复制代码  收藏代码
  1. static int tcp_write_timeout(struct sock *sk)   
  2. {   
  3.     struct inet_connection_sock *icsk = inet_csk(sk);   
  4. ///retry_untry表示我们需要重传的最大次数。   
  5.     int retry_until;   
  6.   
  7. ///判断socket状态。   
  8.     if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {   
  9.         if (icsk->icsk_retransmits)   
  10.             dst_negative_advice(&sk->sk_dst_cache);   
  11. ///设置重传最大值   
  12.         retry_until = icsk->icsk_syn_retries ? : sysctl_tcp_syn_retries;   
  13.     } else {   
  14. ///是否需要检测路由表。   
  15.         if (icsk->icsk_retransmits >= sysctl_tcp_retries1) {   
  16.             /* Black hole detection */  
  17.             tcp_mtu_probing(icsk, sk);   
  18.   
  19.             dst_negative_advice(&sk->sk_dst_cache);   
  20.         }   
  21. ///设置重传最大次数为sysctl_tcp_retries2   
  22.         retry_until = sysctl_tcp_retries2;   
  23.         if (sock_flag(sk, SOCK_DEAD)) {   
  24. ///表示是一个孤立的socket。   
  25.             const int alive = (icsk->icsk_rto < TCP_RTO_MAX);   
  26.   
  27. ///从tcp_orphan_retries(这个函数中会通过sysctl_tcp_orphan_retries来进行计算)中取得重传最大次数。   
  28.             retry_until = tcp_orphan_retries(sk, alive);   
  29.   
  30.             if (tcp_out_of_resources(sk, alive || icsk->icsk_retransmits < retry_until))   
  31.                 return 1;   
  32.         }   
  33.     }   
  34.   
  35. ///最终进行判断,如果重传次数已到则返回1,否则为0.   
  36.     if (icsk->icsk_retransmits >= retry_until) {   
  37.         /* Has it gone just too far? */  
  38.         tcp_write_err(sk);   
  39.         return 1;   
  40.     }   
  41.     return 0;   
  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使用了相同的机制,他们都涉及到了拥塞窗口的定义。其中拥塞窗口限制着传输的长度,它的大小根据拥塞程度上升或者下降。


引用

Slow start
Congestion avoidance
Fast re-transmit
Fast recovery




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

引用

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。

ok,现在我们来看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的大小。

Java代码 复制代码  收藏代码
  1.   
  2. void tcp_enter_loss(struct sock *sk, int how)   
  3. {   
  4.     const struct inet_connection_sock *icsk = inet_csk(sk);   
  5.     struct tcp_sock *tp = tcp_sk(sk);   
  6.     struct sk_buff *skb;   
  7.   
  8.   
  9. /* 1 拥塞控制状态小于TCP_CA_Disorder   
  10. *  2 发送未确认的序列号等于拥塞开始时的下一个将要发送的序列号  
  11. * 3 状态为TCP_CA_Loss,并且还未重新传输过。  
  12. * 如果有一个满足说明有数据丢失,因此降低threshold。  
  13. */  
  14.     if (icsk->icsk_ca_state <= TCP_CA_Disorder || tp->snd_una == tp->high_seq ||   
  15.         (icsk->icsk_ca_state == TCP_CA_Loss && !icsk->icsk_retransmits)) {   
  16. ///保存老的snd_ssthresh。   
  17.         tp->prior_ssthresh = tcp_current_ssthresh(sk);   
  18. ///减小snd_ssthresh   
  19.         tp->snd_ssthresh = icsk->icsk_ca_ops->ssthresh(sk);   
  20. ///设置拥塞状态。   
  21.         tcp_ca_event(sk, CA_EVENT_LOSS);   
  22.     }   
  23.   
  24. ///设置拥塞窗口大小   
  25.     tp->snd_cwnd    = 1;   
  26.     tp->snd_cwnd_cnt   = 0;   
  27. ///设置时间   
  28.     tp->snd_cwnd_stamp = tcp_time_stamp;   
  29.   
  30.     tp->bytes_acked = 0;   
  31. ///清空所有相关的计数器。   
  32.     tcp_clear_retrans_partial(tp);   
  33.   
  34.     if (tcp_is_reno(tp))   
  35.         tcp_reset_reno_sack(tp);   
  36.   
  37.     if (!how) {   
  38.         /* Push undo marker, if it was plain RTO and nothing  
  39.          * was retransmitted. */  
  40.         tp->undo_marker = tp->snd_una;   
  41.     } else {   
  42.         tp->sacked_out = 0;   
  43.         tp->fackets_out = 0;   
  44.     }   
  45.     tcp_clear_all_retrans_hints(tp);   
  46.   
  47. ///遍历sock的write队列。   
  48.     tcp_for_write_queue(skb, sk) {   
  49.         if (skb == tcp_send_head(sk))   
  50.             break;   
  51. ///判断sack段。   
  52.         if (TCP_SKB_CB(skb)->sacked & TCPCB_RETRANS)   
  53.             tp->undo_marker = 0;   
  54.         TCP_SKB_CB(skb)->sacked &= (~TCPCB_TAGBITS)|TCPCB_SACKED_ACKED;   
  55.   
  56. ///如果how为1,则说明不管sack段,此时标记所有的段为丢失(sack的意思去看tcp/ip详解).   
  57.         if (!(TCP_SKB_CB(skb)->sacked&TCPCB_SACKED_ACKED) || how) {   
  58. ///设置sack段。   
  59.             TCP_SKB_CB(skb)->sacked &= ~TCPCB_SACKED_ACKED;   
  60.             TCP_SKB_CB(skb)->sacked |= TCPCB_LOST;   
  61. ///update 相关的域。   
  62.             tp->lost_out += tcp_skb_pcount(skb);   
  63.             tp->retransmit_high = TCP_SKB_CB(skb)->end_seq;   
  64.         }   
  65.     }   
  66.     tcp_verify_left_out(tp);   
  67. ///设置当前的reordering的长度   
  68.     tp->reordering = min_t(unsigned int, tp->reordering,   
  69.                    sysctl_tcp_reordering);   
  70. ///设置拥塞状态。   
  71.     tcp_set_ca_state(sk, TCP_CA_Loss);   
  72.     tp->high_seq = tp->snd_nxt;   
  73. ///由于我们修改了拥塞窗口,因此设置ecn状态。   
  74.     TCP_ECN_queue_cwr(tp);   
  75.     /* Abort F-RTO algorithm if one is in progress */  
  76.     tp->frto_counter = 0;   
  77. }  
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来控制零窗口的定时器的。也就是说接收到对端的窗口报告数据后,会进入这个函数。我们来看实现:

Java代码 复制代码  收藏代码
  1. static void tcp_ack_probe(struct sock *sk)   
  2. {   
  3.     const struct tcp_sock *tp = tcp_sk(sk);   
  4.     struct inet_connection_sock *icsk = inet_csk(sk);   
  5.   
  6.   
  7. ///首先判断是否对端的接收窗口是否已经有空间。   
  8.     if (!after(TCP_SKB_CB(tcp_send_head(sk))->end_seq, tcp_wnd_end(tp))) {   
  9. ///如果有空间则删除零窗口探测定时器。   
  10.         icsk->icsk_backoff = 0;   
  11.         inet_csk_clear_xmit_timer(sk, ICSK_TIME_PROBE0);   
  12.         /* Socket must be waked up by subsequent tcp_data_snd_check().  
  13.          * This function is not for random using!  
  14.          */  
  15.     } else {   
  16. ///否则启动定时器。   
  17.         inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0,   
  18.                       min(icsk->icsk_rto << icsk->icsk_backoff, TCP_RTO_MAX),   
  19.                       TCP_RTO_MAX);   
  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函数。


这个函数主要就是用来发送探测包,我们来看它的实现:

Java代码 复制代码  收藏代码
  1. static void tcp_probe_timer(struct sock *sk)   
  2. {   
  3.     struct inet_connection_sock *icsk = inet_csk(sk);   
  4.     struct tcp_sock *tp = tcp_sk(sk);   
  5.     int max_probes;   
  6. /* 1 tp->packets_out不为0说明,当定时器被安装之后,对端的接收窗口已经被打开。这* 时就不需要传输探测包。  
  7. * 2 tcp_send_head用来检测是否有新的段被传输。  
  8. * 如果上面有一个满足,则不需要发送探测包,并直接返回。  
  9. */  
  10.     if (tp->packets_out || !tcp_send_head(sk)) {   
  11.         icsk->icsk_probes_out = 0;   
  12.         return;   
  13.     }   
  14.   
  15. ///设置最大的重试次数。   
  16.     max_probes = sysctl_tcp_retries2;   
  17.   
  18. ///这里的处理和上面的tcp_write_timeout很类似。   
  19.     if (sock_flag(sk, SOCK_DEAD)) {   
  20.         const int alive = ((icsk->icsk_rto << icsk->icsk_backoff) < TCP_RTO_MAX);   
  21.   
  22.         max_probes = tcp_orphan_retries(sk, alive);   
  23.   
  24.         if (tcp_out_of_resources(sk, alive || icsk->icsk_probes_out <= max_probes))   
  25.             return;   
  26.     }   
  27.   
  28. ///如果重试次数大于最大的重试次数,则报错。   
  29.     if (icsk->icsk_probes_out > max_probes) {   
  30.         tcp_write_err(sk);   
  31.     } else {   
  32.         /* Only send another probe if we didn't close things up. */  
  33. ///否则发送探测包。这个函数里面会发送探测包,并重启定时器。   
  34.         tcp_send_probe0(sk);   
  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恢复给对端。

Java代码 复制代码  收藏代码
  1. static void __tcp_ack_snd_check(struct sock *sk, int ofo_possible)   
  2. {   
  3.     struct tcp_sock *tp = tcp_sk(sk);   
  4.   
  5. /** 1 第一个判断表示多于一个的段在等待ack,并且我们的receive buf有足够的空间,  
  6. * 这是因为这种情况,表明应用程序读取比较快,而对端的发送速度依赖于ack的到达时间,* 因此我们不希望对端减慢速度。  
  7. *   2 这个sock处在quickack 模式  
  8. *   3  我们有 out-of-order数据,此时必须马上给对端以确认。  
  9. *    当上面的任意一个为真,则立即发送ack。  
  10. **/  
  11.     if (((tp->rcv_nxt - tp->rcv_wup) > inet_csk(sk)->icsk_ack.rcv_mss   
  12.          /* ... and right edge of window advances far enough.  
  13.           * (tcp_recvmsg() will send ACK otherwise). Or...  
  14.           */  
  15.          && __tcp_select_window(sk) >= tp->rcv_wnd) ||   
  16.         /* We ACK each frame or... */  
  17.         tcp_in_quickack_mode(sk) ||   
  18.         /* We have out of order data. */  
  19.         (ofo_possible && skb_peek(&tp->out_of_order_queue))) {   
  20.         /* Then ack it now */  
  21.         tcp_send_ack(sk);   
  22.     } else {   
  23.         /* Else, send delayed ack. */  
  24. ///在这里启动定时器。   
  25.         tcp_send_delayed_ack(sk);   
  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 模式。

来看这个函数:
Java代码 复制代码  收藏代码
  1. static inline int tcp_in_quickack_mode(const struct sock *sk)   
  2. {   
  3.     const struct inet_connection_sock *icsk = inet_csk(sk);   
  4.     return icsk->icsk_ack.quick && !icsk->icsk_ack.pingpong;   
  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的状态。

Java代码 复制代码  收藏代码
  1. static void tcp_delack_timer(unsigned long data)   
  2. {   
  3.     struct sock *sk = (struct sock *)data;   
  4.     struct tcp_sock *tp = tcp_sk(sk);   
  5.     struct inet_connection_sock *icsk = inet_csk(sk);   
  6.   
  7.     bh_lock_sock(sk);   
  8. ///用户进程正在使用,则等会再尝试。   
  9.     if (sock_owned_by_user(sk)) {   
  10.         /* Try again later. */  
  11.         icsk->icsk_ack.blocked = 1;   
  12.         NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_DELAYEDACKLOCKED);   
  13.         sk_reset_timer(sk, &icsk->icsk_delack_timer, jiffies + TCP_DELACK_MIN);   
  14.         goto out_unlock;   
  15.     }   
  16.   
  17.     sk_mem_reclaim_partial(sk);   
  18.   
  19. ///判断sock状态 以及ack的状态。如果是close或者已经处在ICSK_ACK_TIMER,则直接跳出。   
  20.     if (sk->sk_state == TCP_CLOSE || !(icsk->icsk_ack.pending & ICSK_ACK_TIMER))   
  21.         goto out;   
  22.   
  23. ///如果已经超时,则重启定时器,并退出。   
  24.     if (time_after(icsk->icsk_ack.timeout, jiffies)) {   
  25.         sk_reset_timer(sk, &icsk->icsk_delack_timer, icsk->icsk_ack.timeout);   
  26.         goto out;   
  27.     }   
  28. ///清除ack状态。   
  29.     icsk->icsk_ack.pending &= ~ICSK_ACK_TIMER;   
  30.   
  31. ///开始遍历prequeue。此时主要的目的是为了调用tcp_rcv_eastablished.这里会调用tcp_ack_snd_check来发送ack。   
  32.     if (!skb_queue_empty(&tp->ucopy.prequeue)) {   
  33.         struct sk_buff *skb;   
  34.   
  35.         NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPSCHEDULERFAILED);   
  36.   
  37. ///遍历prequeue队列,发送未发送的ack。   
  38.         while ((skb = __skb_dequeue(&tp->ucopy.prequeue)) != NULL)   
  39.             sk_backlog_rcv(sk, skb);   
  40.   
  41.         tp->ucopy.memory = 0;   
  42.     }   
  43.   
  44. ///检测是否有ack还需要被发送。也就是处于ICSK_ACK_SCHED状态的ack   
  45.     if (inet_csk_ack_scheduled(sk)) {   
  46.   
  47.         if (!icsk->icsk_ack.pingpong) {   
  48.             /* Delayed ACK missed: inflate ATO. */  
  49.             icsk->icsk_ack.ato = min(icsk->icsk_ack.ato << 1, icsk->icsk_rto);   
  50.         } else {   
  51.   
  52. ///到这里说明已经长时间没有通信,并且处于交互模式。这个时候我们需要关闭pingpong模式。   
  53.             icsk->icsk_ack.pingpong = 0;   
  54.             icsk->icsk_ack.ato      = TCP_ATO_MIN;   
  55.         }   
  56. ///立即发送ack。   
  57.         tcp_send_ack(sk);   
  58.         NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_DELAYEDACKS);   
  59.     }   
  60.     TCP_CHECK_TIMER(sk);   
  61.   
  62. out:   
  63.     if (tcp_memory_pressure)   
  64.         sk_mem_reclaim(sk);   
  65. out_unlock:   
  66.     bh_unlock_sock(sk);   
  67.     sock_put(sk);   
  68. }  
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
第1章 硬件基础与软件基础 61.1 硬件基础 61.1.1 CPU 71.1.2 存储器 81.1.3 总线 81.1.4 控制器和外设 81.1.5 地址空间 91.1.6 时钟 91.2 软件基础 91.2.1 计算机语言 91.2.2 什么是操作系统 111.2.3 内核数据结构 13第2章 内存管理 152.1 虚拟内存抽象模型 152.1.1 请求调页 172.1.2 交换 172.1.3 共享虚拟内存 182.1.4 物理寻址模式和虚拟寻址模式 182.1.5 访问控制 182.2 高速缓存 192.3 Linux页表 202.4 页分配和回收 212.4.1 页分配 222.4.2 页回收 222.5 内存映射 222.6 请求调页 232.7 Linux页缓存 242.8 页换出和淘汰 252.8.1 减少缓冲区和页缓存大小 252.8.2 换出System V共享内存页 262.8.3 换出和淘汰页 272.9 交换缓存 272.10 页换入 28第3章 进程 293.1 Linux进程 293.2 标识符 313.3 调度 323.4 文件 343.5 虚拟内存 353.6 创建进程 363.7 时间和定时器 373.8 执行程序 383.8.1 ELF 393.8.2 脚本文件 40第4章 进程间通信机制 414.1 信号机制 414.2 管道 424.3 套接字 444.3.1 System V的进程间通信机制 444.3.2 消息队列 444.3.3 信号量 454.3.4 共享存储区 47第5章 PCI 495.1 PCI的地址空间 495.2 PCI配置头 505.3 PCI的I/O和存储地址空间 515.4 PCI-ISA桥 515.5 PCI-PCI 桥 515.5.1 PCI-PCI桥:PCI I/O和存储地址 空间的窗口 515.5.2 PCI-PCI桥:PCI配置周期和PCI 总线编号 525.6 Linux PCI初始化 535.6.1 Linux内核PCI数据结构 535.6.2 PCI设备驱动程序 535.6.3 PCI的BIOS函数 565.6.4 PCI修正过程 57第6章 中断处理与设备驱动程序 606.1 中断与中断处理 606.1.1 可编程中断控制器 616.1.2 初始化中断处理数据结构 616.1.3 中断处理 626.2 设备驱动程序 636.2.1 测试与中断 646.2.2 直接存储器访问(DMA) 656.2.3 存储器 666.2.4 设备驱动程序与内核的接口 666.2.5 硬盘 696.2.6 网络设备 74第7章 文件系统 777.1 第二个扩展文件系统EXT2 787.1.1 EXT2系统的inode节点 797.1.2 EXT2系统的超级块 807.1.3 EXT2系统的组描述符 807.1.4 EXT2系统的目录 817.1.5 在EXT2文件系统中查找文件 817.1.6 在EXT2文件系统中改变文件 的大小 827.2 虚拟文件系统 837.2.1 VFS文件系统的超级块 847.2.2 VFS文件系统的inode节点 847.2.3 注册文件系统 857.2.4 装配文件系统 857.2.5 在虚拟文件系统中查找文件 877.2.6 卸载文件系统 877.2.7 VFS文件系统的inode缓存 877.2.8 目录缓存 887.3 缓冲区缓存 887.3.1 bdflush内核守护进程 907.3.2 update进程 907.4 /proc文件系统 917.5 特殊设备文件 91第8章 网络 928.1 TCP/IP网络概述 928.2 Linux中的TCP/IP网络层次结构 958.3 BSD套接字接口 968.4 INET的套接字层 978.4.1 创建BSD套接字 988.4.2 为INET BSD Socket绑定地址 998.4.3 建立INET BSD Socket连接 998.4.4 INET BSD Socket侦听 1008.4.5 接受连接请求 1008.5 IP层 1008.5.1 套接字缓冲区 1008.5.2 接收IP报文 1018.5.3 发送IP报文 1028.5.4 数据分片 1028.6 地址解析协议 1038.7 IP路由 104第9章 内核机制与模块 1079.1 内核机制 1079.1.1 Bottom Half控制 1079.1.2 任务队列 1089.1.3 定时器 1099.1.4 等待队列 1109.1.5 自旋锁 1109.1.6 信号量 1109.2 模块 1119.2.1 模块载入 1129.2.2 模块卸载 113第10章 处理器 11510.1 X86 11510.2 ARM 11510.3 Alpha AXP处理器 115第11章 Linux内核源代码 11711.1 怎样得到Linux内核源码 11711.2 内核源码的编排 11711.3 从何处看起 118第12章 Linux数据结构 120附录A 有用的Web和FTP站点 138附录B 词汇表 139
设计shell脚本程序,运行结果如下: 当用户输入相应的数字执行相应的功能。 2、设计shell脚本程序,在屏幕上输出操作系统的信息,包括计算机名、Linux分发版本名称、Linux内核版本和当前的IP地址。 3、设计shell脚本程序,要求用户对/home目录下的文件进行备份,压缩为Linux系统中常用的tar.gz格式。 4、设计shell脚本程序,假设用户建立了目录A和目录B,目录中不包含子目录,要求用户编写一个脚本程序,比较两个目录内文件的差异。 5、设计一个shell程序,添加一个新组为class1,然后添加属于这个组的30个用户,用户名的形式为stdxx,其中xx从01到30。 6、设计一个shell程序计算n的阶乘。要求: (1)从命令行接收参数n; (2)在程序开始后立即判断n的合法性,即是否有参数,若有是否为正整数,若非法请给错误提示; (3)最后输出计算的结果。 7、设计一个shell程序,在每月第一天备份并压缩/etc目录的所有内容,存放在/root/bak目录里,且文件名为如下形式yymmdd_etc,yy为年,mm为月,dd为日。 8、判断当前工作目录下所有的文件类型,如果是目录显示目录名,如果是文件查看文件内容,如果都不是,显示提示信息。 9、打印1-99之间的奇数到文件。 10、根据从键盘输入的学生成绩,显示相应的成绩等级,其中60分以下为“Failed!”,60~70分为“Passed!”,70~80分为“Medium!”,80~90分为“Good!”,90~100分为“Excellent!”。如果输入超过100分或低于0分,则显示错误分数提示。 文件和目录部分 1、编写程序,打开一个文本文件 (1)读取其中内容,将其复制到一个新建文件中; (2)将文件中的小写字母转换成为大写字母 ,其他字符不变。 2、编写程序,读取当前目录下的内容,并将其打印输出到终端。 3、编写程序,在/tmp目录下面建立一个test目录,然后在test目录下建立一个空文件hello.txt,注意函数出错处理。 4、编写程序实现一个简单的员工档案管理系统,具备简单的员工资料增加、删除和查询等功能,并采用二进制文件保存员工的资料信息。 5、为了便于文件的管理和传输,某些时候需要将特别大的文件切割为多个指定长度小的文件。现有一个文件管理程序需要实现大文件切割功能,要求用户编写一个函数实现该功能。 6、编写程序,根据输入的参数创建一个目录文件。 7、编写程序,首先输出当前的工作目录,然后更改工作目录,输出更改后的工作目录。 8、将存放学生各种信息的文件中的学生信息读出,重新组成一个存放所有学生的前3门成绩的文件。 9、创建一个新目录,然后删除此目录。 10、编写程序,编写shell命令中的ls命令。 11、编写程序,编写shell命令中的pwd命令。 getcwd()会将当前工作目录的绝对路径复制到参数buffer所指的内存空间中,参数size为buf的空间大小。 12、编写程序,编写shell命令中的cp命令。 13、编写Shell命令中的cat命令。 14、编写Shell命令中的tail命令。(用缓冲技术来提高效率) 15、编写Shell命令中的ls -i命令。 进程和信号部分 1、编写程序,获取并输出子进程和父进程的相关信息。 2、编写程序,要求用户设计多进程应用程序,改程序作为父进程执行,在执行过程中能够通过调用自身创建一个子进程。父进程和子进程均在终端中输出一条信息,标识自己的身份。 3、守护进程是运行于系统后台的进程,常用于提供各种系统服务和系统日志管理功能。现要求用户编写一个守护进程应用程序,定时向日志文件写入字符串。 4、编写程序,在不同的进程间实现信号发送和接收,同时在传达过程中附加其他信息。 5、编写程序,使用alarm函数在系统中设置一个定时器,期间对整数进行递减操作,并输出到屏幕。 6、现有一个应用程序在死循环中执行,要求用户让程序能够带捕捉用户按下的组合键Ctrl+C,终止应用程序的执行。 7、Linux系统提供了alarm( )和setitimer( )系统调用作为定时器的功能,要求用户使用这两个函数设计程序,让程序每隔一秒发出一个SIGALRM信号,每隔0.5秒发出一个SIGVTALRM信号。 8、编写程序,在主进程中创建一个子进程,子进程进行空循环,不停地输出“hello world!”字符串,主进程休眠一段时间后,在主进程中结束子进程,随后主进程也退出。 9、编写程序,在程序中使用命令行形式显示程序所在当前文件夹下的内容。 10、编写程序,得到当前进程的标识号,并将它打印输出,随后写入一个文件中。 11、在Linux系统下使用execl( )函数代替一个hello.c文件,在hello.c文件中实现从1到100的累加计算。 网络编程部分 1、在Linux系统下,通过TCP协议的套接字编程,在服务器端的计算机上实现累加求和的计算,数据全部从客户端传送,然后在服务器端计算的和输出到终端,并传送回客户端。 2、在Linux系统下,实现IP地址转换,将名字地址转换为数字地址。 3、利用read函数编写读取客户端数据(提示在程序中,首先监听一个端口,如果有客户端连接这个端口则接受这个连接,然后用read函数读取远程主机发送的数据,输出这些数据以后结束这个程序)。 4、编程实现一个面向连接的套接字服务程序和客户端程序。客户端打开一个文件,把文件内容传送给服务器端,服务器端接受到文件内容后,保存在/tmp目录下。 ...... ......

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值