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。

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


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中。

[quote] 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)".[/quote]

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

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);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值