`
simohayha
  • 浏览: 1400073 次
  • 性别: 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报文段发送接收模拟

    TCP报文段是TCP通信的基本单位,它包含了数据和控制信息,确保数据在网络中准确无误地传输。下面将详细讨论如何使用Java来模拟TCP报文段的发送和接收过程。 首先,模拟TCP报文段的发送和接收,我们需要理解TCP的...

    TCP调试助手源码_tcp助手源码_TCP助手源代码_TCP助手源码_

    通过TCP调试助手源码,你可以看到如何实现这些功能的具体代码,包括如何创建socket、设置套接字选项、建立和断开连接、处理输入输出缓冲区、执行错误检查和异常处理等。这将帮助开发者更好地理解TCP协议的内在工作...

    android TCP server 和TCP client通信源码

    TCP服务器是等待客户端连接并处理请求的程序,而TCP客户端则是主动发起连接请求并发送数据的程序。在Android上,我们可以使用Java的Socket类来实现这两部分。 对于Android TCP服务器,我们需要创建一个线程来监听...

    tcp转发工具,中转TCP请求

    6. **软件开发**:开发TCP转发工具需要对网络编程有深入的理解,包括套接字编程、多线程处理、异常处理等。通过分析和运行提供的Java源码,开发者可以学习如何构建此类工具,并根据需求进行定制。 为了使用这个TCP...

    c++ tcp\ip,Modbus tcp\ip通讯,源码

    这些源码对于学习和理解C++如何处理网络通信以及工业设备间的Modbus通信具有很高的参考价值。 总之,这个项目为开发者提供了一个实践TCP/IP和Modbus TCP/IP通信的平台,有助于深入理解和应用这两种技术,特别是在...

    TCP和串口通讯接收到数据以文本形式输入到鼠标光标的位置,类似于USB扫码枪

    在TCP通信中,数据被分割成多个数据段,每个段都有序号和确认号,确保接收方能够按正确的顺序重组数据,并检测和处理丢失或重复的数据。这种特性使得TCP非常适合于需要高可靠性的应用,如文件传输、网页浏览等。 ...

    TCP.rar_TCP 键盘

    在客户端,键盘输入会被捕获并写入到TCP输出流,而在服务器端,数据从TCP输入流读取并显示在屏幕上。这种交互通常涉及到缓冲区管理和同步机制,以确保数据的正确处理和显示。 在实际编程中,可能会使用各种编程语言...

    TCP-IP详解卷二:实现\028.PDF

    TCP输入处理涉及对传入TCP报文段的验证、解析和响应。以下是本章核心知识点的详细说明: 1. **TCP输入处理概述**: TCP输入处理代码通常较长,`tcp_input`函数大约有1100行,用于处理接收到的TCP报文段。处理流程...

    AB PLC ModbusTCP以太网通讯

    总的来说,AB PLC通过以太网进行ModbusTCP通讯涉及网络配置、ModbusTCP协议理解、编程和错误处理等多个方面。正确理解和实施这些步骤,你可以成功实现AB PLC与任何支持ModbusTCP的第三方设备的高效数据交换。

    TCP_modbus_modbusTCP_

    3. **数据传输**:将构造好的Modbus请求封装在TCP数据段中,然后通过send或sendto函数发送到网络。 4. **接收响应**:等待来自从设备的响应,使用recv或recvfrom函数接收数据。接收到的数据需要解码以提取Modbus...

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

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

    java tcp server 创建线程监听端口,创建线程处理连接

    // 在这里处理客户端的输入/输出流,进行通信 } } ``` 五、创建并启动线程 每当有新的客户端连接时,我们创建一个`ClientHandler`实例,并将其放入`Thread`中运行: ```java while (true) { Socket clientSocket...

    labview以太网的TCP数据采集

    5. **错误处理**:在编写TCP通信程序时,错误处理是至关重要的。LabVIEW提供了丰富的错误处理结构,如错误簇和错误处理函数,可以帮助我们检测和处理可能出现的问题,如连接失败、数据读写错误等。 6. **LABview...

    Socket_tcp多事务处理程序框架

    Socket_TCP多事务处理程序框架是Java编程中用于构建高性能、可扩展网络应用程序的关键技术。它允许程序员通过TCP/IP协议在不同计算机之间建立连接,进行数据的双向通信。在这个框架下,多个事务可以并发处理,提高了...

    Linux 4.4.0 内核源码分析 TCP实现

    至于TCP的输入部分,Linux内核网络数据接收流程是一个复杂的多层处理过程。从接收到网络数据包开始,内核会逐层向上处理,从数据链路层、网络层、传输层,最终到达应用层。在处理过程中,既有可能是从下而上的处理,...

    ModbusTCP_Slave_R102.rar_ModbusTCP_Slave_ab ModbusTCP_ab做modbus

    4. **编写Modbus TCP程序**:使用罗克韦尔的编程软件RSLogix 5000或类似的工具,编写程序来处理Modbus TCP请求。程序应包含接收和响应Modbus请求的子例程,以及处理数据读写操作的代码。 5. **测试通讯**:连接一个...

    C#,winform,Tcp通信源码 使用TcpListener和TcpClient 源码

    通常,客户端会有一个输入输出流的处理逻辑,如异步读写数据。 在实际应用中,需要注意处理异常、同步问题(例如使用锁或异步编程),以及确保资源的正确释放,避免内存泄漏。 总的来说,TcpListener和TcpClient是...

    tcp客户端,检测主机能否连通

    这在处理大量并发连接请求或者需要快速响应用户输入的场景下非常有用。下面我们将详细讨论这个知识点。 首先,TCP客户端的基本工作流程包括以下步骤: 1. 创建套接字:使用socket()函数创建一个TCP套接字,指定协议...

    TCP原理和TCP协议介绍

    4. **全双工通信**:TCP连接允许数据同时双向流动,允许两端独立发送和接收数据,通过缓冲区管理输入和输出。 5. **流接口**:TCP提供无边界的字节流接口,应用程序可以连续发送数据,TCP负责分段和重组。 6. **...

    java Tcp协议验证

    本文将详细探讨使用Java编程语言实现TCP协议验证的相关知识点,包括TCP的特性、可靠传输机制、丢包与错误处理,以及TCP校验和的计算。 首先,TCP是基于连接的协议,这意味着在数据传输之前,必须先建立一个连接。...

Global site tag (gtag.js) - Google Analytics