TCP接收通告窗口

一些TCP协议栈将TCP头部16bit的窗口字段解释为有符号的整数,为了兼容这些系统,Linux内核定义了在没有窗口扩展系数选项的情况下,最大的窗口值为宏MAX_TCP_WINDOW,其为最大的16bit的有符号数。但是默认情况下,内核未开启此功能。PROC文件tcp_workaround_signed_windows默认为0,用户可置1开启此兼容功能。如果接收到对端系统发送的TCP窗口扩张系数选项,内核认为对端系统可正确解释16bit窗口值为无符号值。

#define MAX_TCP_WINDOW      32767U
  
$ cat /proc/sys/net/ipv4/tcp_workaround_signed_windows
0
$

初始接收窗口

基础函数tcp_select_initial_window用于在TCP连接建立前选择初始的窗口值。如下所示窗口值由space值开始推算,space初始化为第二个参数__space的值,其通常为套接口的当前最大接收缓存长度,如果其小于0,初始化为0。内核将space值钳制在windown_clamp(初始为最大窗口值)之内,之后space值向下修正为最大分段长度mss的整数倍(调用者要保证mss的值至少大于等于1)。如果用户要求对一些将TCP窗口值作为有符号数解释的系统进行兼容,即PROC文件tcp_workaround_signed_windows为真,将接收窗口值rcv_wnd限定在MAX_TCP_WINDOW之内,反之,取space的值为窗口值。

/* Maximal number of window scale according to RFC1323 */
#define TCP_MAX_WSCALE      14U

void tcp_select_initial_window(const struct sock *sk, int __space, __u32 mss, __u32 *rcv_wnd, __u32 *window_clamp, int wscale_ok, __u8 *rcv_wscale, __u32 init_rcv_wnd)
{
    unsigned int space = (__space < 0 ? 0 : __space);

    if (*window_clamp == 0)
        (*window_clamp) = (U16_MAX << TCP_MAX_WSCALE);
    space = min(*window_clamp, space);

    if (space > mss)
        space = rounddown(space, mss);

    if (sock_net(sk)->ipv4.sysctl_tcp_workaround_signed_windows)
        (*rcv_wnd) = min(space, MAX_TCP_WINDOW);
    else
        (*rcv_wnd) = space;

接下来要确定窗口扩张系数的值,如果PROC文件tcp_rmem定义的接收缓存最大值(第三个值),或者rmem_max文件指定的最大内存值,其中之一大于space值将其调整为三者之中的最大值。其次,space值应确保小于窗口钳制值windown_clamp,最后,内核将space值分拆为接收窗口的16bit基础值和扩张系数值两个部分,基础值部分不能大于16bit的最大值,扩张系数不能大于系统定义的最大扩张系数TCP_MAX_WSCALE,拆分完成之后得到需要的扩张系数值rcv_wscale。

由以上可见,初始的窗口值相当于系统能够提供的接收缓存的最大限定值。

    (*rcv_wscale) = 0;
    if (wscale_ok) {
        /* Set window scaling on max possible window */
        space = max_t(u32, space, sock_net(sk)->ipv4.sysctl_tcp_rmem[2]);
        space = max_t(u32, space, sysctl_rmem_max);
        space = min_t(u32, space, *window_clamp);
        while (space > U16_MAX && (*rcv_wscale) < TCP_MAX_WSCALE) {
            space >>= 1;
            (*rcv_wscale)++;
        }
    }

    if (mss > (1 << *rcv_wscale)) {
        if (!init_rcv_wnd)
            init_rcv_wnd = tcp_default_init_rwnd(mss);
        *rcv_wnd = min(*rcv_wnd, init_rcv_wnd * mss);
    }

    (*window_clamp) = min_t(__u32, U16_MAX << (*rcv_wscale), *window_clamp);
}

如果TCP最大分段长度MSS的值大于最小非零的可用窗口值(1 << *rcv_wscale),就需要重新调整接收窗口值。将其值限制在初始窗口值init_rcv_wnd与MSS乘积值以下。通常情况下初始接收窗口为0,用户可通过BPF或者路由系统指定其值。内核默认定义的初始窗口值为初始拥塞窗口值的2倍,其以MSS为单位,MSS默认大小为1460,以字节为单位的话即10*2*1460,见以下tcp_default_init_rwnd函数。如果实际的MSS值大于1460,就需要将初始窗口换算为以实际MSS大小为单位的值,最小不能小于2。但是,如果实际的MSS值小于1460的话,内核也不再增大初始窗口值了。

函数tcp_select_initial_window的最后,更新了窗口的钳制值。其不能大于接收窗口的最大基础值左移扩张系数的结果。初始的窗口钳制值可通过路由系统指定,或者通过setsockopt的选项TCP_WINDOW_CLAMP设定,其值不能小于最小接收缓存值(SOCK_MIN_RCVBUF)的一半。

#define TCP_INIT_CWND       10

u32 tcp_default_init_rwnd(u32 mss)
{
    u32 init_rwnd = TCP_INIT_CWND * 2;
    if (mss > 1460)
        init_rwnd = max((1460 * init_rwnd) / mss, 2U);
    return init_rwnd;
}

服务端窗口初始值

在接收到客户端的SYN请求之后,服务端要初始请求套接口,如下函数tcp_openreq_init,列出了与窗口相关的变量。

static void tcp_openreq_init(struct request_sock *req, const struct tcp_options_received *rx_opt, struct sk_buff *skb, const struct sock *sk)
{
    struct inet_request_sock *ireq = inet_rsk(req);

    req->rsk_rcv_wnd = 0;       /* So that tcp_send_synack() knows! */
    req->mss = rx_opt->mss_clamp;
    req->ts_recent = rx_opt->saw_tstamp ? rx_opt->rcv_tsval : 0;
    ireq->tstamp_ok = rx_opt->tstamp_ok;
    ireq->wscale_ok = rx_opt->wscale_ok;
}

接下来TCP服务端会使用函数tcp_openreq_init_rwin初始化接收窗口,其主要功能在于选择上文介绍的函数tcp_select_initial_window所需的参数,再调用其初始化接收窗口相关信息。

首先是参数__space,其值由函数tcp_full_space给出,等于监听套接口的最大接收缓存减去相关数据结构所占用的内存开销之后的所得值。关于相关结构而非数据所占用的内存开销,内核使用PROC文件tcp_adv_win_scale控制其比例,如果tcp_adv_win_scale大于0,开销为2^tcp_adv_win_scale次幂分之一;如果小于零,结果翻转,开销为(1-(1/(2^-tcp_adv_win_scale)))分之一,可见,tcp_adv_win_scale的值越小开销越大。内核默认值为1,开销为1/2,表明在套接口接收缓存中,数据与表示数据的相关结构各占一半缓存。tcp_adv_win_scale的合法取值范围为[-31, 31]。 

如果BPF系统指定了初始窗口值,并且full_space值小于初始窗口值与MSS的乘积,将full_space最终定义为乘积的结果值。

$ cat /proc/sys/net/ipv4/tcp_adv_win_scale
1

static inline int tcp_win_from_space(const struct sock *sk, int space)
{
    int tcp_adv_win_scale = sock_net(sk)->ipv4.sysctl_tcp_adv_win_scale;
    return tcp_adv_win_scale <= 0 ? (space>>(-tcp_adv_win_scale)) : space - (space>>tcp_adv_win_scale);
}       
static inline int tcp_full_space(const struct sock *sk)
{
    return tcp_win_from_space(sk, sk->sk_rcvbuf);
}

其次,要确定TCP通告的最大分段值MSS。MSS可由路由METRIC的advmss属性取得,如下的IP命令配置advmss,如果路由中未指定(advmss等于0),取默认的MSS值。对于IPv4而言,回调函数default_advmss为ipv4_default_advmss,其由路由出口设备的MTU值或者路由METRIC的MTU属性值减去TCP标准头部长度和IP标准头部长度而得到。

由函数tcp_mss_clamp可知,如果用户通过setsockopt选项TCP_MAXSEG指定了MSS,并且其值小于由路由函数dst_metric_advmss得到的值,使用用户指定的值;否则,使用路由METRIC中得到的值。如果当前的TCP连接有timestamp选项,最终的MSS值应减去其长度。

$ ip route add 192.168.20.2 via 192.168.1.1 mtu lock 1040 advmss 1000

static inline u32 dst_metric_advmss(const struct dst_entry *dst)
{   
    u32 advmss = dst_metric_raw(dst, RTAX_ADVMSS);
    if (!advmss)
        advmss = dst->ops->default_advmss(dst);
    return advmss;
}  
static inline u16 tcp_mss_clamp(const struct tcp_sock *tp, u16 mss)
{
    u16 user_mss = READ_ONCE(tp->rx_opt.user_mss);
    return (user_mss && user_mss < mss) ? user_mss : mss; 
}

再者,就是初始窗口值rcv_wnd,通常情况下为0,其可通过BPF系统指定,也可通过IP路由命令指定其初值,如下指定initrwnd值为2。所有到目的地址为192.168.5.5的TCP连接初始接收窗口为2*MSS的值。

$ ip route add 192.168.5.5 via 192.168.1.1 initrwnd 2 

最后,计算窗口钳制值rsk_window_clamp。如果用户通过setsockopt选项TCP_WINDOW_CLAMP设置了套接口的窗口钳制值,rsk_window_clamp使用用户的设置值,否则使用此TCP连接的路由表项中指定的值,如下的IP命令。如果用户使用setsockopt选项SO_RCVBUF/SO_SNDBUFFORCE设置了接收缓存最大值,并且套接口的窗口钳制值rsk_window_clamp大于full_space或者其等于0,使用full_space作为窗口钳制值。

函数tcp_select_initial_window的逻辑如上节所述。

$ ip route add 192.168.6.6 via 192.168.1.1 window 4096

void tcp_openreq_init_rwin(struct request_sock *req, const struct sock *sk_listener, const struct dst_entry *dst)
{
    struct inet_request_sock *ireq = inet_rsk(req);
    const struct tcp_sock *tp = tcp_sk(sk_listener);
    int full_space = tcp_full_space(sk_listener);

    mss = tcp_mss_clamp(tp, dst_metric_advmss(dst));
    window_clamp = READ_ONCE(tp->window_clamp);
    req->rsk_window_clamp = window_clamp ? : dst_metric(dst, RTAX_WINDOW);

    if (sk_listener->sk_userlocks & SOCK_RCVBUF_LOCK && (req->rsk_window_clamp > full_space || req->rsk_window_clamp == 0))
        req->rsk_window_clamp = full_space;

    rcv_wnd = tcp_rwnd_init_bpf((struct sock *)req);
    if (rcv_wnd == 0)
        rcv_wnd = dst_metric(dst, RTAX_INITRWND);
    else if (full_space < rcv_wnd * mss)
        full_space = rcv_wnd * mss;

    tcp_select_initial_window(sk_listener, full_space, mss - (ireq->tstamp_ok ? TCPOLEN_TSTAMP_ALIGNED : 0),
        &req->rsk_rcv_wnd, &req->rsk_window_clamp, ireq->wscale_ok, &rcv_wscale, rcv_wnd);
    ireq->rcv_wscale = rcv_wscale;
}

函数tcp_make_synack创建对客户端SYN报文的回复报文SYN+ACK。需要注意的是在此报文的TCP头部窗口字段window,保存的是未进行扩张处理的窗口值,SYN和SYN+ACK两种报文的窗口都不进行扩张。

struct sk_buff *tcp_make_synack(const struct sock *sk, struct dst_entry *dst, struct request_sock *req, struct tcp_fastopen_cookie *foc, enum tcp_synack_type synack_type)
{
    th = (struct tcphdr *)skb->data;
    memset(th, 0, sizeof(struct tcphdr));
    th->syn = 1;
    th->ack = 1;

    /* RFC1323: The window in SYN & SYN/ACK segments is never scaled. */
    th->window = htons(min(req->rsk_rcv_wnd, 65535U));
}

在TCP三次握手完成之后,请求套接口结构inet_request_sock中保存的窗口相关信息都将赋值给新创建的子套接口。如下函数tcp_create_openreq_child,其中如果支持窗口扩张,保存扩张系数,否则的话将子套接口中的扩张系数清零,并且要保证窗口钳制值不大于16bit的最大无符号数(65535)。

struct sock *tcp_create_openreq_child(const struct sock *sk, struct request_sock *req, struct sk_buff *skb)
{
    struct sock *newsk = inet_csk_clone_lock(sk, req, GFP_ATOMIC);
    if (newsk) {
        newtp->window_clamp = req->rsk_window_clamp;
        newtp->rcv_ssthresh = req->rsk_rcv_wnd;
        newtp->rcv_wnd = req->rsk_rcv_wnd;
        newtp->rx_opt.wscale_ok = ireq->wscale_ok;
        if (newtp->rx_opt.wscale_ok) {
            newtp->rx_opt.rcv_wscale = ireq->rcv_wscale;
        } else {
            newtp->rx_opt.rcv_wscale = 0;
            newtp->window_clamp = min(newtp->window_clamp, 65535U);
        }
	}
}

在TCP三次握手完成之后,服务端处理函数tcp_rcv_state_process和客户端处理函数tcp_finish_connect,都将TCP状态设置为TCP_ESTABLISHED状态。并且通过tcp_init_transfer函数,初始化窗口信息,具体见函数tcp_init_buffer_space。
 
首先,确定一下初始的接收缓存sk_rcvbuf值,前面介绍过tcp_default_init_rwnd函数实际返回的为初始的窗口数量,通常为20,以此计算rcvmem就是初始化为40个存储了通告MSS加上MAX_TCP_HEADER长度的数据的skb结构的长度。tcp_moderate_rcvbuf默认情况下时打开的,DRS考虑到2至3个RTT的时延,将rcvmem增大4倍。最终,如果sk_rcvbuf小于rcvmem,将sk_rcvbuf的值限制在rcvmem和tcp_rmem最大值两者之间的较小值。

static void tcp_fixup_rcvbuf(struct sock *sk)
{
    u32 mss = tcp_sk(sk)->advmss;

    rcvmem = 2 * SKB_TRUESIZE(mss + MAX_TCP_HEADER) * tcp_default_init_rwnd(mss);

    /* Dynamic Right Sizing (DRS) has 2 to 3 RTT latency Allow enough cushion so that sender is not limited by our window */
    if (sock_net(sk)->ipv4.sysctl_tcp_moderate_rcvbuf)
        rcvmem <<= 2;

    if (sk->sk_rcvbuf < rcvmem)
        sk->sk_rcvbuf = min(rcvmem, sock_net(sk)->ipv4.sysctl_tcp_rmem[2]);
}
$ cat /proc/sys/net/ipv4/tcp_moderate_rcvbuf
1

再次,决定两个窗口钳制值。如果当前的窗口钳制值window_clamp大于等于sk_rcvbuf对应的最大窗口值maxwin,将其限制在maxwin。如果tcp_app_win有值,并且maxwin大于4倍的通过MSS,预留由window/2^tcp_app_win到mss之间的某个值作为应用空间使用。慢启动窗口阈值rcv_ssthresh限制在window_clamp之内。

void tcp_init_buffer_space(struct sock *sk)
{   
    int tcp_app_win = sock_net(sk)->ipv4.sysctl_tcp_app_win;
    
    if (!(sk->sk_userlocks & SOCK_RCVBUF_LOCK))
        tcp_fixup_rcvbuf(sk);

    maxwin = tcp_full_space(sk);
    
    if (tp->window_clamp >= maxwin) {
        tp->window_clamp = maxwin;
        if (tcp_app_win && maxwin > 4 * tp->advmss)
            tp->window_clamp = max(maxwin - (maxwin >> tcp_app_win), 4 * tp->advmss);
    }
    /* Force reservation of one segment. */
    if (tcp_app_win &&
        tp->window_clamp > 2 * tp->advmss &&
        tp->window_clamp + tp->advmss > maxwin)
        tp->window_clamp = max(2 * tp->advmss, maxwin - tp->advmss);
    
    tp->rcv_ssthresh = min(tp->rcv_ssthresh, tp->window_clamp);
}

$ cat /proc/sys/net/ipv4/tcp_app_win
31

 

客户端窗口初始值


TCP客户端在tcp_connect_init函数中初始化接收窗口相关参数,参数意义大体上与服务端类似。PROC文件tcp_window_scaling决定是否启用TCP的窗口扩张系数选项,内核默认是启用状态,反之未启用的话tcp_select_initial_window函数返回的扩张系数rcv_wscale值将为0。另外,如果此套接口接收过timestamp选项,ts_recent_stamp记录了接收的时间戳,需要将通告MSS(advmss)的值减去TCP选项的长度值,才是真正的数据长度。TCP选项的长度由TCP头部总长度减去TCP基础头部长度(20字节)得到。

最后,将慢启动阈值rcv_ssthresh初始化为接收窗口的大小。

$ cat /proc/sys/net/ipv4/tcp_window_scaling
1
$ 

static void tcp_connect_init(struct sock *sk)
{
    if (!tp->window_clamp)
        tp->window_clamp = dst_metric(dst, RTAX_WINDOW);
    tp->advmss = tcp_mss_clamp(tp, dst_metric_advmss(dst));

    if (sk->sk_userlocks & SOCK_RCVBUF_LOCK && (tp->window_clamp > tcp_full_space(sk) || tp->window_clamp == 0))
        tp->window_clamp = tcp_full_space(sk);
    
    rcv_wnd = tcp_rwnd_init_bpf(sk);
    if (rcv_wnd == 0)
        rcv_wnd = dst_metric(dst, RTAX_INITRWND);

    tcp_select_initial_window(sk, tcp_full_space(sk),
                  tp->advmss - (tp->rx_opt.ts_recent_stamp ? tp->tcp_header_len - sizeof(struct tcphdr) : 0),
                  &tp->rcv_wnd, &tp->window_clamp, sock_net(sk)->ipv4.sysctl_tcp_window_scaling, &rcv_wscale, rcv_wnd);
    
    tp->rx_opt.rcv_wscale = rcv_wscale;
    tp->rcv_ssthresh = tp->rcv_wnd;
}

TCP通告窗口值

首先看一下系统的当前接收窗口,由函数tcp_receive_window实现。TCP接收窗口的右边界(最大序号)等于上一次通告的接收窗口值rcv_wnd,与通告之时窗口的左边界rcv_wup之和,使用窗口的右边界序号减去当前接收到数据的最后序号值,得到当前剩余的可用接收窗口。当然,如果对端不顾窗口大小而发送了大量的数据将导致rcv_nxt超出窗口右边界,导致可用接收窗口为负数,限定为0。

static inline u32 tcp_receive_window(const struct tcp_sock *tp)
{       
    s32 win = tp->rcv_wup + tp->rcv_wnd - tp->rcv_nxt;
    if (win < 0) 
        win = 0;
    return (u32) win;
}

基础函数__tcp_select_window用于选择新的通告接收窗口大小。首先了解三个缓存空间值,分别是free_space空闲缓存空间,其等于最大接收空间减去已使用空间的结果值可容纳的单纯数据的量(tcp_space);可允许空间allowed_space表示套接口最大接收空间可容纳的单纯数据量;最后一个缓存空间为最大满负荷空间full_space,其等于allowed_space与套接口窗口钳制值两者之中的较小值。

如果TCP连接对端的最大分段长度MSS大于本地满负荷空间值full_space,说明本地已无空间接收一个完整的MSS长度的报文,以下将使用full_space的值代替MSS做相关计算。如果full_space小于等于0,直接返回0窗口。

另外,参见内核中的注释,由于无法准确获得对端的MSS值,此处的MSS为内核估算值,所以如果其值发生震荡的话,将会引起性能的下降。

static inline int tcp_space(const struct sock *sk)
{
    return tcp_win_from_space(sk, sk->sk_rcvbuf - atomic_read(&sk->sk_rmem_alloc));
}
u32 __tcp_select_window(struct sock *sk)
{
    int mss = icsk->icsk_ack.rcv_mss;
    int free_space = tcp_space(sk);
    int allowed_space = tcp_full_space(sk);
    int full_space = min_t(int, tp->window_clamp, allowed_space);

    if (unlikely(mss > full_space)) {
        mss = full_space;
        if (mss <= 0)
            return 0;
    }

以下由空闲空间值开始,逐步导出新的窗口值。如果空闲空间free_space小于最大满负荷空间full_space的一半,表明空闲空间将要变得紧急,尝试降低窗口值。先将freee_space的值向下修正到最小扩张空间的整数倍,避免因扩张系数导致不必要的窗口增长,之后在判断如果free_space空间已恶化到小于估算MSS的值,或者小于最大允许接收空间的1/16时,表明空闲空间已非常紧急,直接返回0窗口。为避免糊涂窗口综合征(Silly Window Syndrome),不要通告小窗口值。而且如果不通告零窗口,发送端将继续发送报文,最终导致在TCP接收路径中遭遇空间不足,此时tcp_clamp_window函数将增加接收缓存sk_rcvbuf的长度,直至到最大缓存长度(PROC文件tcp_rmem的第三个值),此后进来的数据包将因内存限值而被丢弃。

如果通告窗口较大,意味着对端可发送大量数据,将很快触发空闲空间小于最大允许空间的1/16的条件,空闲空间小于MSS值的条件有一定的滞后。另外,如果TCP协议的缓存空间处于承压状态,将慢启动窗口阈值rcv_ssthresh控制在4倍的通告MSS值之内。free_space的值限制在rcv_ssthresh阈值之内。

    if (free_space < (full_space >> 1)) {
        icsk->icsk_ack.quick = 0;

        if (tcp_under_memory_pressure(sk))
            tp->rcv_ssthresh = min(tp->rcv_ssthresh, 4U * tp->advmss);

        free_space = round_down(free_space, 1 << tp->rx_opt.rcv_wscale);

        if (free_space < (allowed_space >> 4) || free_space < mss)
            return 0;
    }

    if (free_space > tp->rcv_ssthresh)
        free_space = tp->rcv_ssthresh;

如果开启了窗口扩张选项,将以上计算的free_space换算为2的扩张系数次幂的整数倍(即对齐到1 << tp->rx_opt.rcv_wscale),结果值作为通告的窗口,此处不将窗口值修正为MSS的整数倍,因为即使修正,最终扩张换算的窗口值也可能不再是MSS整数倍。

反之,没有开启窗口扩张选项,并且当前的通告窗口rcv_wnd再增加MSS的量也不超过空闲空间的值,表明空闲量至少比窗口大于一个MSS值(未通告空间大于MSS),即可增大窗口值,此处要求窗口值为mss的整数倍,将free_space向下修正到MSS的整数倍作为新窗口值;或者当前通告窗口大于空闲空间值,表明空闲空间不足而窗口过大,同样将free_space向下修正到MSS的整数倍作为新的窗口值。

最后,如果当前通告窗口值增加MSS的量之后大于free_space空闲空间的值(未通告空间小于MSS),并且,满负荷空间full_space很小仅等于MSS的值,而空闲空间free_space大于窗口值加上满负荷空间的一半(也即MSS的1/2)的结果值,表明未通告空间大于MSS的1/2,将新窗口增加为free_space的值,此处不能修正到MSS的整数倍,因为增加值太小(不到一个MSS)。以上条件都不满足,继续通告之前的窗口值。

    if (tp->rx_opt.rcv_wscale) {
        window = free_space;
        window = ALIGN(window, (1 << tp->rx_opt.rcv_wscale));
    } else {
        window = tp->rcv_wnd;

        if (window <= free_space - mss || window > free_space)
            window = rounddown(free_space, mss);
        else if (mss == full_space && free_space > window + (full_space >> 1))
            window = free_space;
    }
    return window;
}

TCP窗口值通告


TCP发送函数tcp_transmit_skb调用tcp_select_window将套接口当前的接收窗口大小通告于对端系统。对于SYN和SYN+ACK数据包而言,其通告的窗口值不支持TCP扩张选项,直接通告rcv_wnd的值。除此之外的数据包,使用函数tcp_select_window获得。

static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it, gfp_t gfp_mask)
{
    if (unlikely(tcb->tcp_flags & TCPHDR_SYN))
        tcp_options_size = tcp_syn_options(sk, skb, &opts, &md5);
    else
        tcp_options_size = tcp_established_options(sk, skb, &opts, &md5);

    if (likely(!(tcb->tcp_flags & TCPHDR_SYN))) {
        th->window      = htons(tcp_select_window(sk));
        tcp_ecn_send(sk, skb, th, tcp_header_size);
    } else {
        th->window  = htons(min(tp->rcv_wnd, 65535U));
    }
}

函数tcp_select_window如下所示,其返回的窗口值可用于赋值TCP头部的通告窗口window字段。首先不能通告一个比当前可用接收窗口还要小的新窗口值,否则,将新窗口值更新为当前的可用接收窗口值。

更新rcv_wnd接收窗口值,并且将等待接收的下一个数据的序号rcv_nxt保存到rcv_wup变量中,在计算当前窗口时需要此值,见函数tcp_receive_window介绍。

static u16 tcp_select_window(struct sock *sk)
{
    u32 cur_win = tcp_receive_window(tp);
    u32 new_win = __tcp_select_window(sk);

    if (new_win < cur_win) {
        new_win = ALIGN(cur_win, 1 << tp->rx_opt.rcv_wscale);
    }
    tp->rcv_wnd = new_win;
    tp->rcv_wup = tp->rcv_nxt;

只有在对端系统未发送接收窗口扩张系数选项,并且PROC文件tcp_workaround_signed_windows的值为1时,意味着要兼容一些将窗口值解释为16bit有符号数的系统,内核才将新窗口值限制在MAX_TCP_WINDOW(32767)之内。否则,将限制值提高到16bit无符号最大值(65535)左移窗口扩张系数位数的所得值。

最后,如果新通告的窗口值为0,关闭TCP的快速路径接收功能,详情请见:https://blog.csdn.net/sinat_20184565/article/details/88904103。

    if (!tp->rx_opt.rcv_wscale && sock_net(sk)->ipv4.sysctl_tcp_workaround_signed_windows)
        new_win = min(new_win, MAX_TCP_WINDOW);
    else
        new_win = min(new_win, (65535U << tp->rx_opt.rcv_wscale));
    new_win >>= tp->rx_opt.rcv_wscale;

    /* If we advertise zero window, disable fast path. */
    if (new_win == 0) {
        tp->pred_flags = 0;
    }
    return new_win;
}

TCP服务端的窗口扩张选项在函数tcp_synack_options中处理。如果客户端在SYN请求报文中携带了TCPOPT_WINDOW选项,并且本地服务端支持窗口扩张(PROC文件tcp_window_scaling),在回复SYN+ACK报文时,增加服务端的TCP窗口扩张选项。

static unsigned int tcp_synack_options(const struct sock *sk, struct request_sock *req, unsigned int mss, struct sk_buff *skb, struct tcp_out_options *opts, ...)
{
    struct inet_request_sock *ireq = inet_rsk(req);
    unsigned int remaining = MAX_TCP_OPTION_SPACE;

    opts->mss = mss;
    remaining -= TCPOLEN_MSS_ALIGNED;

    if (likely(ireq->wscale_ok)) {
        opts->ws = ireq->rcv_wscale;
        opts->options |= OPTION_WSCALE;
        remaining -= TCPOLEN_WSCALE_ALIGNED;
    }
}

对于TCP客户端,在发送SYN请求报文时由函数tcp_syn_options添加选项信息。如下可见,如果PROC文件/proc/sys/net/ipv4/tcp_window_scaling内容为真,添加窗口扩张选项。

static unsigned int tcp_syn_options(struct sock *sk, struct sk_buff *skb, struct tcp_out_options *opts, struct tcp_md5sig_key **md5)
{
    if (likely(sock_net(sk)->ipv4.sysctl_tcp_window_scaling)) {
        opts->ws = tp->rx_opt.rcv_wscale;
        opts->options |= OPTION_WSCALE;
        remaining -= TCPOLEN_WSCALE_ALIGNED;
    }
}

最后如果用户层由接收队列拷贝走了数据(copied大于0),并且当前接收窗口的2倍值小于等于窗口钳制值,内核检查一下可能的新窗口值是否大于等于当前接收窗口的2倍值,如果成立通告新的接收窗口值。注意,由于__tcp_select_window函数的执行需要较多的CPU指令周期,在调用之前,内核提前判断了窗口钳制值,避免对__tcp_select_window函数的无谓调用。

只有在用户读取了一定量的数据,致使可用新窗口足够大时,才会通告新的窗口值。通告由ACK报文发送。

static void tcp_cleanup_rbuf(struct sock *sk, int copied)
{
    if (inet_csk_ack_scheduled(sk)) {
    } 

    if (copied > 0 && !time_to_ack && !(sk->sk_shutdown & RCV_SHUTDOWN)) {
        __u32 rcv_window_now = tcp_receive_window(tp);

        if (2*rcv_window_now <= tp->window_clamp) {
            __u32 new_window = __tcp_select_window(sk);

            if (new_window && new_window >= 2 * rcv_window_now)
                time_to_ack = true;
        }
    }
    if (time_to_ack)
        tcp_send_ack(sk);
}

函数tcp_cleanup_rbuf的调用位于应用层接收类函数中,例如tcp_read_sock函数和tcp_recvmsg函数。如下为函数tcp_read_sock。

int tcp_read_sock(struct sock *sk, read_descriptor_t *desc, sk_read_actor_t recv_actor)
{
    if (copied > 0) {
        tcp_recv_skb(sk, seq, &offset);
        tcp_cleanup_rbuf(sk, copied);
    }
    return copied;
}

在接收过程中,如果已分配的接收缓存超出限定的接收缓存值,使用tcp_clamp_window函数增加窗口的限制因素之一sk_rcvbuf的值,条件是sk_rcvbuf的值小于tcp_rmem定义的最大值、用户未锁定sk_rcvbuf的值、TCP协议的总内存未处在承压状态以及TCP协议的总占用缓存小于限制值的最小值,所有这些条件都成立的话,将sk_rcvbuf的值更新为已分配接收缓存sk_rmem_alloc与tcp_rmem定义的最大值二者之中的较小值。

如果已分配接收缓存sk_rmem_alloc仍旧大于sk_rcvbuf的值,将窗口限制因素之一的慢启动窗口阈值(rcv_ssthresh)更新为窗口钳制值window_clamp和2倍的套接口通告MSS之中的最小值。在以上的窗口选择函数__tcp_select_window逻辑中,新的窗口值不能大小rcv_ssthresh的值。

在函数tcp_prune_queue中,如果已分配的接收缓存并未超出限定的接收缓存值,但是TCP协议的整体缓存处于承压状态的话,将rcv_ssthresh限制在4倍的套接口通告MSS值之内。

static int tcp_prune_queue(struct sock *sk)
{
    if (atomic_read(&sk->sk_rmem_alloc) >= sk->sk_rcvbuf)
        tcp_clamp_window(sk);
    else if (tcp_under_memory_pressure(sk))
        tp->rcv_ssthresh = min(tp->rcv_ssthresh, 4U * tp->advmss);
}
static void tcp_clamp_window(struct sock *sk)
{
    if (sk->sk_rcvbuf < net->ipv4.sysctl_tcp_rmem[2] && !(sk->sk_userlocks & SOCK_RCVBUF_LOCK) && 
	    !tcp_under_memory_pressure(sk) && sk_memory_allocated(sk) < sk_prot_mem_limits(sk, 0)) {
        sk->sk_rcvbuf = min(atomic_read(&sk->sk_rmem_alloc), net->ipv4.sysctl_tcp_rmem[2]);
    }
    if (atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf)
        tp->rcv_ssthresh = min(tp->window_clamp, 2U * tp->advmss);
}

窗口值的增长


在接收路径上,处理函数tcp_event_data_recv与tcp_data_queue_ofo函数都会根据条件调用窗口增长函数。前者在接收到正常序号的数据时调用(包括TCP的快速路径和慢速路径接收),后者在处理乱序数据时根据条件调用窗口处理函数。如下tcp_event_data_recv函数,在接收报文的长度满足大于等于128时,调用窗口增长函数,反之接收到小报文,不调整窗口值。

static void tcp_event_data_recv(struct sock *sk, struct sk_buff *skb)
{
    if (skb->len >= 128)
        tcp_grow_window(sk, skb);
}

慢启动窗口阈值实际上为当前的窗口钳制值,窗口增长的第一个条件就是,rcv_ssthresh值小于窗口钳制值,并且小于空闲的接收缓存空间,而且TCP协议的缓存空间未处于承压状态。之后,由于在函数tcp_grow_window调用之前,调用者已经对数据报文skb的truesize做了判断,将报文添加到了接收缓存队列中,这样意味着接收缓存增加truesize之后并未超出系统限定的套接口接收缓存长度。

接下来第二个判断条件,有个前提是内核不能够将全部可用接收缓存通告为窗口,因为接收缓存不仅仅是保存TCP数据,还要有一部分空间用来保存skb结构等的元数据(额外开销),关于一定的空间有多少比例可以转化为窗口,内核使用函数tcp_win_from_space实现(默认1:1)。以下的判断是如果纯数据长度skb->len大于或者等于保存此数据的skb的总长度truesize所对应比例的通告窗口长度,也就是说对端发送的数据等于或者大于本地通告的窗口值,导致数据占用了一部分预留的额外开销空间,但是这没有关系,不会造成溢出。由于满足第一个条件即接收缓存还有空余,可放心的增大通告窗口,因为尽管对端可能发送大于窗口的数据,内核已经为增大的窗口预留了足够的额外开销空间,可存储超出窗口的纯数据长度,没有溢出风险。窗口值增加两倍的通告MSS。

    |-------------------------------->  sk->sk_rcvbuf  <--------------------------------------|
	
    |                                                |
    |--->   通告窗口    <---|        预留额外开销     |
    |----------------------|---|---------------------|
    |                          |                     |
    |--->  接收纯数据长度    <--|    真实的额外开销   |
    
    |------------> skb->truesize  <------------------|
                           |   | 
                      ---> |   | <--借用的预留额外开销

反之,如果纯数据skb->len长度小于按照skb->truesize长度通告的窗口比例,就比较危险了。由于真实的skb额外开销大于接收缓存预留的额外开销,此时再增大窗口将有可能导致skb额外开销超出接收缓存预留的额外开销空间。但是,这不是一定会溢出的,因为既然接收缓存的窗口空间有余量,数据包的额外开销也可借用其中一部分,逻辑见__tcp_grow_window函数。

static void tcp_grow_window(struct sock *sk, const struct sk_buff *skb)
{
    /* Check #1 */
    if (tp->rcv_ssthresh < tp->window_clamp && (int)tp->rcv_ssthresh < tcp_space(sk) && !tcp_under_memory_pressure(sk)) {

        /* Check #2. Increase window, if skb with such overhead will fit to rcvbuf in future. */
        if (tcp_win_from_space(sk, skb->truesize) <= skb->len)
            incr = 2 * tp->advmss;
        else
            incr = __tcp_grow_window(sk, skb);
}

如下__tcp_grow_window函数。由于skb纯数据长度小于内核按照比例分配的窗口空间,预留的额外开销空间实际上小于真实的skb额外空间,把这个情况放大比例就是如果将整个接收缓存sk_rcvbuf所对应的最大窗口值通告给对端,而对端又发送了足够的数据填满整个通告窗口(skb中数据与开销比例不变),那么接收缓存中预留的额外开销部分是一定容纳不下真实的skb额外开销的。

假如目前通告的窗口值等于sk_rcvbuf所对应的最大窗口的一半,此时对端又发送填满整个通告窗口的数据,非常巧合的是这些数据以及其真实的额外开销占满了接收缓存空间sk_rcvbuf,接下来无窗口可通告,到达临界点。

    |-------------------------------->  sk->sk_rcvbuf  <--------------------------------------|

    |--->           最大通告窗口            <----|--->           预留额外开销               <---|
    |-------------------------------------------|---------------------------------------------|
    |                     |
    |-->  当前通告窗口  <--|
    |                     |
    
    |-->  纯数据长度    <--|
    |                     |
    |-----> skb->truesize 对应通告窗口  <--------|
    |                     |
    |-> 对应通告窗口1/2  <-|
    |                     |----------------------> 真实的额外开销空间  <-----------------------|
    |--------------------------------->  skb->truesize   <------------------------------------|

我们知道truesize所对应比例的通告窗口一定是大于纯数据长度的,否则就进不来__tcp_grow_window函数了。但是如果truesize所对应比例的窗口的一半,不是向上图正好等于纯数据长度,而是仍然大于纯数据长度,那么表明truesize所对应比例的整个通告窗口长度大于sk_rcvbuf的最大通告窗口,意味着truesize长度已经超过sk_rcvbuf的总长度,接收缓存溢出。所以在函数__tcp_grow_window中,要求truesize对应比例的窗口减半值要小于等于纯数据的长度。

前提条件是,最大接收缓存对应的通告窗口值在折半过程中不能小于当前通告窗口的钳制值。最终的窗口增加量为两倍的对端MSS值。内核在此使用的二分法判断,可保证接收缓存不溢出,但是并不是说不符合此条件就一定会溢出。

/* Slow part of check#2. */
static int __tcp_grow_window(const struct sock *sk, const struct sk_buff *skb)
{   
    /* Optimize this! */
    int truesize = tcp_win_from_space(sk, skb->truesize) >> 1;
    int window = tcp_win_from_space(sk, sock_net(sk)->ipv4.sysctl_tcp_rmem[2]) >> 1;
    
    while (tp->rcv_ssthresh <= window) {
        if (truesize <= skb->len)
            return 2 * inet_csk(sk)->icsk_ack.rcv_mss;
        truesize >>= 1;
        window >>= 1;
    }
    return 0;
}

得到窗口增加值incr之后,取incr值与两倍的报文长度二者之中的最大值,之后将其递加到rcv_ssthresh上,并确保最终值不大于窗口钳制值window_clamp。

static void tcp_grow_window(struct sock *sk, const struct sk_buff *skb)
{
        if (incr) {
            incr = max_t(int, incr, 2 * skb->len);
            tp->rcv_ssthresh = min(tp->rcv_ssthresh + incr, tp->window_clamp);
            inet_csk(sk)->icsk_ack.quick |= 1;
        }
    }
}

接收数据序号的窗口相关检查


在TCP的接收路径上如函数tcp_rcv_state_process和tcp_rcv_established,使用tcp_sequence函数检查接收到报文的序号合法性。其结束序号不能位于rcv_wup之前,否则为序号重复的报文,但是开始序号可以位于rcv_wup之前,此时由部分报文重复。另一个条件是,报文的开始序号不能再接收窗口的右侧,否则报文完全落在接收窗口之外。

static inline bool tcp_sequence(const struct tcp_sock *tp, u32 seq, u32 end_seq)
{
    return  !before(end_seq, tp->rcv_wup) && !after(seq, tp->rcv_nxt + tcp_receive_window(tp));
}

在TCP连接建立阶段,子套接口还未建立,使用请求套接口的rsk_rcv_wnd表示窗口大小。如下请求检查函数tcp_check_req,请求套接口的rcv_nxt表示窗口的左边界,其加上窗口长度rsk_rcv_wnd表示右边界。

struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb, struct request_sock *req, bool fastopen)
{
    if (paws_reject || !tcp_in_window(TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq,
                      tcp_rsk(req)->rcv_nxt, tcp_rsk(req)->rcv_nxt + req->rsk_rcv_wnd)) {
        /* Out of window: send ACK and drop. */
        return NULL;
    }
}
static bool tcp_in_window(u32 seq, u32 end_seq, u32 s_win, u32 e_win)
{
    if (seq == s_win)
        return true;
    if (after(end_seq, s_win) && before(seq, e_win))
        return true;
    return seq == e_win && seq == end_seq;
}

确认报文ACK的发送检查函数__tcp_ack_snd_check,如果当前接收到的数据的序号减去窗口的左边界已经大于发送端的MSS值,并且可用窗口不小于当前的通过窗口rcv_wnd,即窗口右边界可向右移动大于MSS的空间,说明发送ACK的时机已到。

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 && !RB_EMPTY_ROOT(&tp->out_of_order_queue))) {
        tcp_send_ack(sk);
    } else {
        tcp_send_delayed_ack(sk);
    }
}

 

内核版本 4.15

 

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页