`
simohayha
  • 浏览: 1386520 次
  • 性别: Icon_minigender_1
  • 来自: 火星
社区版块
存档分类
最新评论

tcp的输入段的处理

阅读更多
tcp是全双工的协议,因此每一端都会有流控。一个tcp段有可能是一个数据段,也有可能只是一个ack,异或者即包含数据,也包含ack。如果是数据段,那么有可能是in-sequence的段,也有可能是out-of-order的段。如果是in-sequence的段,则马上加入到socket的receive队列中,如果是out-of-order的段,则会加入到socket的ofo队列。一旦当我们接收到数据,要么立即发送ack到对端,要么延迟等待和后面的数据一起将ack发送出去。

当发送ack之前,我们需要检测一些我们已经从对端得到的信息。也就是说我们需要通过对端的信息来执行ack的生成。详细的去看tcp 协议的相关部分。一般来说就是tcp option和tcp flag的一些东西。

这里就不介绍协议相关的东西了。随便一本tcp协议的书上都将的很详细。

这次我们主要就来看内核协议栈的核心的数据交互是如何进行的。

由于发送段的执行比较简单,因此我们主要来看接收端的处理。

我们知道tcp的处理输入段的函数是tcp_rcv_established。在linux内核中有两种方法来执行输入段,分别是slow 和fast path。在fast path中,我们要做的事情非常少,只是处理输入数据(一般都是放到socket的receive队列),发送ack,存储时间戳等。而在slow path中,我们需要处理out-of-order段,PAWS,urgent数据等等。而在内核中通过实现一个伪的flag来区分是slow 还是fast path,这个伪flag是tcp头中的第12个字节组成的。分别是头长度,flag以及advertised windows。


然后来看这个flag的相关结构以及tcp头的结构。.其中pred-flag都是保存在tcp_sock的pred_flag域中的:


struct tcphdr {
	__be16	source;
	__be16	dest;
	__be32	seq;
	__be32	ack_seq;
///下面就是flag以及头的长度。
#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;
#else
#error	"Adjust your <asm/byteorder.h> defines"
#endif	
	__be16	window;
	__sum16	check;
	__be16	urg_ptr;
};



struct tcp_sock {
.........................

/*
 *	Header prediction flags
 *	0x5?10 << 16 + snd_wnd in net byte order
 */
	__be32	pred_flags;
.......................
}

union tcp_word_hdr { 
	struct tcphdr hdr;
	__be32 		  words[5];
}; 

#define tcp_flag_word(tp) ( ((union tcp_word_hdr *)(tp))->words [3]) 


可以看到我们如果要取pre-flag的话,直接取得就是tcphdr的第12个字节,也就是从长度开始。然后flag是32位。

然后就是对应的tcp flag在pre flag中的值,这个是为了方便我们存取对应的tcp flag。tcp 控制位刚好是高2个字节。

这里有一个要注意的就是,psh不影响我们判断slow还是fast path,因此这里我们忽略调psh,所以这里又构造了一个TCP_HP_BITS.
enum { 
	TCP_FLAG_CWR = __cpu_to_be32(0x00800000),
	TCP_FLAG_ECE = __cpu_to_be32(0x00400000),
	TCP_FLAG_URG = __cpu_to_be32(0x00200000),
	TCP_FLAG_ACK = __cpu_to_be32(0x00100000),
	TCP_FLAG_PSH = __cpu_to_be32(0x00080000),
	TCP_FLAG_RST = __cpu_to_be32(0x00040000),
	TCP_FLAG_SYN = __cpu_to_be32(0x00020000),
	TCP_FLAG_FIN = __cpu_to_be32(0x00010000),
	TCP_RESERVED_BITS = __cpu_to_be32(0x0F000000),
	TCP_DATA_OFFSET = __cpu_to_be32(0xF0000000)
}; 
#define TCP_HP_BITS (~(TCP_RESERVED_BITS|TCP_FLAG_PSH))



接下来我们来看如何构造pred-flag.
一旦进入fast path,prediction flag将会马上被赋值到tcp_sock的pred_flags上,而在内核中是通过__tcp_fast_path_on来做得。

static inline void __tcp_fast_path_on(struct tcp_sock *tp, u32 snd_wnd)
{
///计算pred flags。
	tp->pred_flags = htonl((tp->tcp_header_len << 26) |
			       ntohl(TCP_FLAG_ACK) |
			       snd_wnd);
}


这个计算很简单,就是直接按照pred-flag的定义进行计算。最高位是tcp_header_len,所以它需要左移26位.然后由于我们进入了fast path,因此我们这里flag就是ack。最后是对端传递过来的窗口大小。


当fast path打开了tcp_socket的pred_flag肯定是非0,否则就是0,而每次当我们需要打开fast path,之前,我们需要先进行检测是否能够进入fast path,在内核中,是通过tcp_fast_path_check实现的。

它的检测分为4个条件:

1 是否ofo队列为空。

2 当前的接收窗口是否大于0.

3 当前的已经提交的数据包大小是否小于接收缓冲区的大小。

4 是否含有urgent 数据

如果上面4个条件都为真则打开fast path.
static inline void tcp_fast_path_check(struct sock *sk)
{
	struct tcp_sock *tp = tcp_sk(sk);

	if (skb_queue_empty(&tp->out_of_order_queue) &&
	    tp->rcv_wnd &&
	    atomic_read(&sk->sk_rmem_alloc) < sk->sk_rcvbuf &&
	    !tp->urg_data)
		tcp_fast_path_on(tp);
}


ok,接下来来看什么时候才会打开slow path或者fast path。

先来看slow path。

1 我们在tcp_data_queue中接收到了一个ofo的段。

2 当我们调用tcp_prune_queue中协议栈的内存不够用了并且开始丢包。

3 我们通过调用tcp_urg_check发现是一个urgent段。而处理erg段是在tcp_urg函数中。

4 我们的发送窗口已经为0了。然后在tcp_select_window判断,然后打开slow path。

5 每一个新的连接默认都是slow path的。

然后是fast path。

fast path的打开是调用tcp_fast_path_check来实现的,因此我们来看什么时候这个函数会被调用:

1 在tcp_recvmsg中我们已经读取了urgent数据。urgent数据是在tcp_rcv_established中被handle的,而tcp_recvmsg是拷贝数据到用户空间的,这里我们得到urgent数据(tcp_rcv_established),然后就会在slow path中,直到我们接收到了urgent 数据(tcp_recvmsg),然后我们就进入fast path。

2 当在tcp_data_queue填充一些gap的时候。

3 当调用tcp_ack_update_window来修改窗口的时候.

这里只是先文字简要的介绍下,后面我们分析代码的时候会更好的理解这些。

其实简而言之,fast path就是tcp协议的最理想的状态下才会进入这个,比如:数据段都是按顺序到达,窗口都是固定大小,没有urgent数据,缓存够用等等。

而slow path则是比较恶劣的情况。情况正好和上面相反。

接下来我们就来详细分析slow 和fast path。

先来看fast path的详细实现。

我们就从函数tcp_rcv_established开始:

先来看第一个判断,下面这个判断如果为true,则我们进入fast path处理。

1 tcp_flag_word(th) & TCP_HP_BITS) == tp->pred_flags  这里TCP_HP_BITS是pred_flags的掩码,如果tp为slow path,则pred_flags为 0,自然就不会相等了。

2 TCP_SKB_CB(skb)->seq == tp->rcv_nxt 这里seq为对端发送过来的序列起始号,而rcv-nxt则是我们期望接受的序列号,如果不等,说明是ofo数据。

3 !after(TCP_SKB_CB(skb)->ack_seq, tp->snd_nxt) ack_seq是当前的发送缓冲区中,已经ack了的最后一个字节号,snd_nxt是我们将要发送的下一个段的起始序列号。一般来说ack_seq都是比snd_nxt小,也就是这个值为true。

而当ack_seq比snd_nxt大的情况我不太明白,不知道谁能解释下,什么情况下ack_seq比snd_nxt大。

if ((tcp_flag_word(th) & TCP_HP_BITS) == tp->pred_flags &&
	    TCP_SKB_CB(skb)->seq == tp->rcv_nxt &&
	    !after(TCP_SKB_CB(skb)->ack_seq, tp->snd_nxt)) 


只要上面的三个表达式都是true,则我们进入fast path处理。

接下来来的代码片断就是处理当tcp timestamp option打开时的情况。

这里有个概念是paws,简而言之就是一种依靠时间戳防止重复报文的机制。详细的东西可以看下这里:

http://www.linuxforum.net/forum/printthread.php?Cat=&Board=linuxK&main=139290&type=thread


int tcp_header_len = tp->tcp_header_len;

///相等说明tcp timestamp option被打开。
if (tcp_header_len == sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) {

///这里主要是parse timestamp选项,如果返回0则表明pase出错,此时我们进入slow_path
		if (!tcp_parse_aligned_timestamp(tp, th))
				goto slow_path;

///如果上面pase成功,则tp对应的rx_opt域已经被正确赋值,此时如果rcv_tsval(新的接收的数据段的时间戳)比ts_recent(对端发送过来的数据(也就是上一次)的最新的一个时间戳)小,则我们要进入slow path 处理paws。
if ((s32)(tp->rx_opt.rcv_tsval - tp->rx_opt.ts_recent) < 0)
				goto slow_path;

		}


fast path的最后我们看一下ack的发送处理部分。这里可以看到最终会调用__tcp_ack_snd_check来进行ack的处理,下面我们会看这个函数。这个函数前面的blog已经分析过了,不过这里再来看下。

这里主要是通过几个条件判断来决定到底是立即发送ack还是说,等会等有数据了和数据一起将ack发送。这里delay ack的话会有一个定时器,我们前面分析定时器的时候已经分析过了,这里就不分析了。我们着重来看这几个条件:

1 (tp->rcv_nxt - tp->rcv_wup) > inet_csk(sk)->icsk_ack.rcv_mss

rcv_nxt我们知道是接收方期待接收的序列号,而rcv_wup则是窗口update之前的最后一次的rcv_nxt。rcv_mss表示delay 使用的mss。

这里如果为真说明我们已经至少接收了一个完整的段(mss).

2  __tcp_select_window(sk) >= tp->rcv_wnd

第一个是计算当前的接收窗口,而rcv_wnd则是当前的接收窗口。

如果大于等于,则说明我们可能需要改变窗口,此时就必须把ack立即发送。

3 tcp_in_quickack_mode(sk)

这个主要是看有没有设置立即发送的标记。

4 (ofo_possible && skb_peek(&tp->out_of_order_queue)

这个是测试有没有ofo数据,也就是乱序的段。


static void __tcp_ack_snd_check(struct sock *sk, int ofo_possible)
{
	struct tcp_sock *tp = tcp_sk(sk);

	if (((tp->rcv_nxt - tp->rcv_wup) > inet_csk(sk)->icsk_ack.rcv_mss
	     && __tcp_select_window(sk) >= tp->rcv_wnd) ||
	    tcp_in_quickack_mode(sk) ||
	    (ofo_possible && skb_peek(&tp->out_of_order_queue))) {

///立即发送ack
		tcp_send_ack(sk);
	} else {
///否则进入delay ack的处理。
		tcp_send_delayed_ack(sk);
	}
}



剩下的代码就不介绍了,都是一些拷贝数据到用户进程,更新相关的sock域的工作,我们前面已经基本分析过了(详见我前面的blog).

然后我们来看slow path。

下面的代码就是slow path开始的地方。
这里首先是校验,然后调用tcp_validate_incoming处理paws以及段的序列号的完整性。

slow_path:

/*len是当前的段的长度,而doff<<2则是当前段的头的长度。数据段肯定要比头段要大。而第二个是数据的校验。如果有一个是true,我们则丢掉这个段。*/

if (len < (th->doff << 2) || tcp_checksum_complete_user(sk, skb))
		goto csum_error;
///接下来开始进入paws以及序列号的处理。
	res = tcp_validate_incoming(sk, skb, th, 1);
	if (res <= 0)
		return -res;




接下来我们就来看tcp_validate_incoming的实现,它的代码很简单,就是一些校验。


1 首先是处理paws。


///处理paws。
if (tcp_fast_parse_options(skb, th, tp) && tp->rx_opt.saw_tstamp &&
	    tcp_paws_discard(sk, skb)) {
		if (!th->rst) {
			tcp_send_dupack(sk, skb);
			goto discard;
		}
		/* Reset is accepted even if it did not pass PAWS. */
	}

2 然后是判断序列号的合法性。

首先end_seq(也就是当前的段的结束序列号)不能小于rcv_wup(这个的序列号表示最后一次窗口改变时我们的rcv_nxt,也就是说这个序列号之前的段已经被确认过了).

第二个检测就比较容易理解了,就是当前的序列号不能超过当前的窗口大小。

不过这里有一个要注意的就是RFC793,这里我们虽然不接受这个段,可是还是会通过tcp_send_dupack来发送一个ack。这个的详细描述救在rfc793中。

引用
RFC793, page 37: "In all states except SYN-SENT, all reset
(RST) segments are validated by checking their SEQ-fields."
And page 69: "If an incoming segment is not acceptable,
an acknowledgment should be sent in reply (unless the RST
bit is set, if so drop the segment and return)".


	
///这个函数其实包装了两个检测。

if (!tcp_sequence(tp, TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq)) {
		if (!th->rst)
///发送ack
			tcp_send_dupack(sk, skb);
		goto discard;
	}


3 检测是否是rst段。

4 最后一个是检测syn段。也就是握手时的序列号交换校验。


///当前的数据段的序列号不能小于期待接收的序列号。
if (th->syn && !before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt)) {
		if (syn_inerr)
			TCP_INC_STATS_BH(sock_net(sk), TCP_MIB_INERRS);
		NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPABORTONSYN);
		tcp_reset(sk);
		return -1;
	}



接下来继续看slow path的处理。


step5:
///首先处理ack,如果是ack段,则需要更新相关域,并且进行sack,等等的处理.
	if (th->ack && tcp_ack(sk, skb, FLAG_SLOWPATH) < 0)
		goto discard;
///更新rtt
	tcp_rcv_rtt_measure_ts(sk, skb);

	/* Process urgent data. */
	tcp_urg(sk, skb, th);

///数据段的处理都在这里。
	tcp_data_queue(sk, skb);

///如果有数据需要发送,则会发送数据到对端。
	tcp_data_snd_check(sk);
///检测是否需要发送ack到对端。如果要则发送ack。
	tcp_ack_snd_check(sk);
	return 0;



接下来一个个的看,先来看tcp_ack,这里我就不详细分析这个函数了,这个函数主要是用来处理接受到ack后,我们的接收缓冲区中所需要做得一些工作。

校验ack序列号,update 滑动窗口,清除重传队列中的已经ack了的段,执行sack信息。管理拥塞窗口,以及处理 0窗口定时器。


tcp_data_queue这个我前面的blog已经分析过一些了。这里也不详细分析了,就简要介绍下它的功能。

处理ofo段,处理内存超过限制,重传,重复的段的处理,如果我们在sack打开的情况下收到重复的段我们也会设置dsack。(这个函数很复杂)


然后来看tcp_data_snd_check这个函数,这个函数主要用来将一些暂时pending住的数据(比如打开了nagle)发送出去。并且调用tcp_check_space来唤醒等待内存的写队列。这是因为我们有可能已经ack了一些数据,从而一些skb会被释放。


而这里为什么要将pending的数据发送出去呢,主要是因为我们有可能已经akced了一些段,从而增加了拥塞窗口的大小,也就是cwnd,此时我们就需要将一些pending的数据迅速发送出去。


static inline void tcp_data_snd_check(struct sock *sk)
{
///发送pending的数据
	tcp_push_pending_frames(sk);
///如果内存有释放则唤醒等待内存的队列
	tcp_check_space(sk);
}

static inline void tcp_push_pending_frames(struct sock *sk)
{
	struct tcp_sock *tp = tcp_sk(sk);

	__tcp_push_pending_frames(sk, tcp_current_mss(sk), tp->nonagle);
}


接下来是tcp_ack_snd_check,它主要用来判断是否需要发送ack,还是说delay ack。

static inline void tcp_ack_snd_check(struct sock *sk)
{
	if (!inet_csk_ack_scheduled(sk)) {
		/* We sent a data segment already. */
		return;
	}
///这个函数前面已经分析过了,就是判断是要立即ack还是delay ack。
	__tcp_ack_snd_check(sk, 1);
}








分享到:
评论

相关推荐

    TCP-IP详解卷3:TCP事务协议

    第11章 T/TCP实现:TCP输入 101 11.1 概述 101 11.2 预处理 103 11.3 首部预测 104 11.4 被动打开的启动 105 11.5 主动打开的启动 108 11.6 PAWS:防止序号重复 114 11.7 ACK处理 115 11.8 完成被动打开和同时打开 ...

    网络TCP串口RS232数据通讯转键盘USBKeyBoard HID输入到文本框

    可以将来自网络或者串口的数据转换成字符串打印在光标所在位置,支持TCP或者RS232串口通信。可以设置数据传输间隔,对特殊字符进行处理、添加自定义头尾数据,添加分隔符。输出速度快,稳定,支持开机自动启动。

    TCP/IP协议详解卷二:实现

    1.10 输入处理 1.11 网络实现概述 1.12 中断级别与并发 1.13 源代码组织 1.14 测试网络 1.15 小结 第二章 mduf:存储器缓存 2.1 引言 2.2 代码介绍 2.3 mduf的定义 2.4 mduf结构 2.5 简单的mduf宏和函数 2.6 m_devget...

    TCP/IP详解 卷3:TCP事务协议、HTTP、NNTP和UNIX域协议

    第11章 T/TCP实现:TCP输入 11.1 概述 11.2 预处理 11.3 首部预测 11.4 被动打开的启动 11.5 主动打开的启动 11.6 PAWS:防止序号重复 11.7 ACK处理 11.8 完成被动打开和同时打开 11.9 ACK处理(续) 11.10 FIN处理 ...

    TCP-IP详解卷三

    第11章 T/TCP实现:TCP输入 101 11.1 概述 101 11.2 预处理 103 11.3 首部预测 104 11.4 被动打开的启动 105 11.5 主动打开的启动 108 11.6 PAWS:防止序号重复 114 11.7 ACK处理 115 11.8 完成被动打开和同时打开 ...

    Android TCP Socket通信实例Demo源码Apk下载

    以及对接硬件的项目数据在十六进制&&byte&&int的转换处理。 要注意BufferedReader的readLine()方法的阻塞问题: 读取socket输入流的时候很多代码都会这么写,一般也不会有什么问题,但是readLine()方法读取不到换行...

    TCP-IP详解卷三:TCP事务协议,HTTP,NNTP和UNIX域协议——高清文字(china-pub经典系列)

    第11章 T/TCP实现:TCP输入 101 11.1 概述 101 11.2 预处理 103 11.3 首部预测 104 11.4 被动打开的启动 105 11.5 主动打开的启动 108 11.6 PAWS:防止序号重复 114 11.7 ACK处理 115 11.8 完成被动打开和同时打开 ...

    TCPNetKit_with_sourceCode.rar_TCP服务器_tcp_tcp vc_tcp封装_网络调试 源码

    TCP客户端可以对某个IP(或者直接输入域名)的端口进行连接,实时显示已经连接的服务器发送的信息,可以手动输入需要发送到服务器的内容。 主要就是用了winsock的一些函数进行封装。 源码中包含了用VC与EVC编译的...

    TCP-IP详解卷2:实现.part1

    1.10 输入处理 15 1.10.1 以太网输入 15 1.10.2 IP输入 15 1.10.3 UDP输入 16 1.10.4 进程输入 17 1.11 网络实现概述(续) 17 1.12 中断级别与并发 18 1.13 源代码组织 20 1.14 测试网络 21 1.15 小结 22 第2章 mbuf...

    TCP网络调试程序(VC和EVC两个版本的全部代码)

    TCP客户端可以对某个IP(或者直接输入域名)的端口进行连接,实时显示已经连接的服务器发送的信息,可以手动输入需要发送到服务器的内容。 这个程序在对一些自己编写的服务器或者客户端的程序进行调试的时候比较...

    TCP_IP详解卷1

    6.6 ICMP报文的4.4BSD处理 59 6.7 小结 60 第7章 Ping程序 61 7.1 引言 61 7.2 Ping程序 61 7.2.1 LAN输出 62 7.2.2 WAN输出 63 7.2.3 线路SLIP链接 64 7.2.4 拨号SLIP链路 65 7.3 IP记录路由选项 65 7.3.1 通常的...

    TCP-IP详解卷1:协议

    6.6 ICMP报文的4.4BSD处理 59 6.7 小结 60 第7章 Ping程序 61 7.1 引言 61 7.2 Ping程序 61 7.2.1 LAN输出 62 7.2.2 WAN输出 63 7.2.3 线路SLIP链接 64 7.2.4 拨号SLIP链路 65 7.3 IP记录路由选项 65 7.3.1 通常的...

    TCP-IP详细协议

    6.6 ICMP报文的4.4BSD处理 59 6.7 小结 60 第7章 Ping程序 61 7.1 引言 61 7.2 Ping程序 61 7.2.1 LAN输出 62 7.2.2 WAN输出 63 7.2.3 线路SLIP链接 64 7.2.4 拨号SLIP链路 65 7.3 IP记录路由选项 65 7.3.1 通常的...

    TCP-IP详解卷二 TCP-IP详解卷二 TCP-IP详解卷二

    第1章概述 第2章mbuf:存储器缓存 第3章接口层 第4章接口:以太网 ...第29章TCP的输入(续) 第30章TCP的用户需求 第31章BPF:BSD分组过滤程序 第32章原始IP 附录A部分习题的解答 附录B源代码的获取

    TCP/IP详解part_2

    6.6 ICMP报文的4.4BSD处理 59 6.7 小结 60 第7章 Ping程序 61 7.1 引言 61 7.2 Ping程序 61 7.2.1 LAN输出 62 7.2.2 WAN输出 63 7.2.3 线路SLIP链接 64 7.2.4 拨号SLIP链路 65 7.3 IP记录路由选项 65 7.3.1 通常的...

    TCP/IP详解 (卷2:实现)

    1.10 输入处理 1.10.1 以太网输入 1.10.2 IP输入 1.10.3 UDP输入 1.10.4 进程输入 1.11 网络实现概述(续) 1.12 中断级别与并发 1.13 源代码组织 1.14 测试网络 1.15 小结 第2章 mbuf:存储器缓存 2.1 ...

    TCP/IP详解 卷1完整版

    6.6 ICMP报文的4.4BSD处理 59 6.7 小结 60 第7章 Ping程序 61 7.1 引言 61 7.2 Ping程序 61 7.2.1 LAN输出 62 7.2.2 WAN输出 63 7.2.3 线路SLIP链接 64 7.2.4 拨号SLIP链路 65 7.3 IP记录路由选项 65 7.3.1 通常的...

    TCPIP详解--共三卷

    6.6 ICMP报文的4.4BSD处理 59 6.7 小结 60 第7章 Ping程序 61 7.1 引言 61 7.2 Ping程序 61 7.2.1 LAN输出 62 7.2.2 WAN输出 63 7.2.3 线路SLIP链接 64 7.2.4 拨号SLIP链路 65 7.3 IP记录路由选项 65 7.3.1 通常的...

    TCP/IP详解卷 pdf格式

    6.6 ICMP报文的4.4BSD处理 59 6.7 小结 60 第7章 Ping程序 61 7.1 引言 61 7.2 Ping程序 61 7.2.1 LAN输出 62 7.2.2 WAN输出 63 7.2.3 线路SLIP链接 64 7.2.4 拨号SLIP链路 65 7.3 IP记录路由选项 65 7.3.1 通常的...

    TCP-IP详解卷2

    1.10 输入处理 15 1.10.1 以太网输入 15 1.10.2 IP输入 15 1.10.3 UDP输入 16 1.10.4 进程输入 17 1.11 网络实现概述(续) 17 1.12 中断级别与并发 18 1.13 源代码组织 20 1.14 测试网络 21 1.15 小结 22 第2章 mbuf...

Global site tag (gtag.js) - Google Analytics