由于没有直接的信息可以获得对端的MSS值,内核中的代码实际上是估算以得到对端MSS值。
一、RCV_MSS初始化
初始化对端的MSS值,首先起始值取自本地通告advmss值与当前发送MSS缓存值两者之中的较小值,在TCP的三次握手建立连接过程中,双方协商了MSS的钳制值即最大值,其值介于通告advmss与MSS缓存值mss_cache之间。其次,如果此接收MSS值大于对端发送窗口的二分之一,取后者为新的发送MSS值。如果新增大于默认的MSS值TCP_MSS_DEFAULT(536),取后者为新的发送MSS值。最后,保证rcv_mss的值不小于最小的MSS值TCP_MIN_MSS(88)。最小值是由最大的IP头部和最大的TCP头部长度加上8个字节的数据长度,减去标准IP和TCP头部长度而得到的值。
高估此值将导致ACK确认发送不及时,低估此值没有关系,内核将在函数tcp_measure_rcv_mss中进行修正。
void tcp_initialize_rcv_mss(struct sock *sk)
{
const struct tcp_sock *tp = tcp_sk(sk);
unsigned int hint = min_t(unsigned int, tp->advmss, tp->mss_cache);
hint = min(hint, tp->rcv_wnd / 2);
hint = min(hint, TCP_MSS_DEFAULT);
hint = max(hint, TCP_MIN_MSS);
inet_csk(sk)->icsk_ack.rcv_mss = hint;
}
#define TCP_MSS_DEFAULT 536U /* IPv4 (RFC1122, RFC2581) */
#define TCP_MIN_MSS 88U /* Minimal accepted MSS. It is (60+60+8) - (20+20). */
TCP客户端在发起连接请求,初始化SYN报文时调用tcp_initialize_rcv_mss初始化rcv_mss。接收到服务端的SYN+ACK报文,或者,接收到SYN报文,意味着TCP两端同时发送SYN报文,同时打开连接时,再次初始化话接收MSS值。注意在第二次调用rcv_mss初始化函数之前,内核函数tcp_sync_mss将先更新本地MSS缓存值,所以两次初始化rcv_mss可能得到不一样的值。
static void tcp_connect_init(struct sock *sk)
{
tcp_initialize_rcv_mss(sk);
}
static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb, const struct tcphdr *th)
{
if (th->ack) {
if (!th->syn)
goto discard_and_undo;
tcp_sync_mss(sk, icsk->icsk_pmtu_cookie);
tcp_initialize_rcv_mss(sk);
}
}
TCP服务端接收到SYN报文请求和接收到三次握手的第三个ACK确认报文后都将调用接收MSS初始化函数tcp_initialize_rcv_mss。但是,在第一次调用前服务端将先更新缓存MSS值(见函数tcp_sync_mss)以及MSS通告值advmss。在第二次调用前根据TCP的timestamp选项,首先调整通告MSS的值advmss,之后在初始化接收MSS值。
struct sock *tcp_v4_syn_recv_sock(const struct sock *sk, struct sk_buff *skb, struct request_sock *req, struct dst_entry *dst, struct request_sock *req_unhash, bool *own_req)
{
tcp_sync_mss(newsk, dst_mtu(dst));
newtp->advmss = tcp_mss_clamp(tcp_sk(sk), dst_metric_advmss(dst));
tcp_initialize_rcv_mss(newsk);
}
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
switch (sk->sk_state) {
case TCP_SYN_RECV:
if (tp->rx_opt.tstamp_ok)
tp->advmss -= TCPOLEN_TSTAMP_ALIGNED;
tcp_initialize_rcv_mss(sk);
}
}
二、RCV_MSS估算
入口函数为tcp_event_data_recv,其在接收到对端的TCP数据时被调用,在其中使用tcp_measure_rcv_mss函数估算RCV_MSS的值。
static void tcp_event_data_recv(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
struct inet_connection_sock *icsk = inet_csk(sk);
tcp_measure_rcv_mss(sk, skb);
}
第一种比较简单的情况,接收到的数据长度大于或者等于当前的接收MSS值,如果其小于本地通告的advmss值,将其设置为新的接收MSS值rcv_mss,假设对端在以MSS值的长度发送数据包。反之,如果接收数据长度大于本地的通告MSS值,而且还超出MAX_TCP_OPTION_SPACE(40字节)的长度,通常这种情况不会发生,如果发生并且接收数据长度大于入口设备的MTU值,意味着在网卡接收后内核可能进行了数据包合并(GRO)操作,此举将可能导致TCP性能降低。
#define MAX_TCP_OPTION_SPACE 40
static void tcp_measure_rcv_mss(struct sock *sk, const struct sk_buff *skb)
{
struct inet_connection_sock *icsk = inet_csk(sk);
const unsigned int lss = icsk->icsk_ack.last_seg_size;
icsk->icsk_ack.last_seg_size = 0;
len = skb_shinfo(skb)->gso_size ? : skb->len;
if (len >= icsk->icsk_ack.rcv_mss) {
icsk->icsk_ack.rcv_mss = min_t(unsigned int, len, tcp_sk(sk)->advmss);
if (unlikely(len > icsk->icsk_ack.rcv_mss + MAX_TCP_OPTION_SPACE))
tcp_gro_dev_warn(sk, skb, len);
} else {
第二种情况是接收数据长度小于当前估算的接收MSS值rcv_mss。数据长度加上传输层头部长度(TCP及选项)之和,条件一:如果大于等于MSS默认值TCP_MSS_DEFAULT与标准TCP头部长度之和;条件二:或者大于等于最小MSS值TCP_MIN_MSS与标准TCP头部长度之和,并且未设置TCP头部的PUSH标志位;以上两个条件符合其一,记录本次接收数据长度值last_seg_size,而且如果此值等于上次接收到的数据长度值,意味着已经连续两次接收到此长度数据的报文,更新接收MSS值rcv_mss为此长度值。
对于条件二,如果设置PSH标志的话很有可能并非MSS长度报文,未设置PSH标志,通常情况下接收到的为一个具有MSS长度数据的报文,然而此数据长度大于等于最小MSS值加上标准TCP头部长度表明为合法的长度值,小于默认MSS加上TCP标准头部长度,意味着此TCP连接的路径MTU值较小,应当对接收MSS进行尽快的更新。
len += skb->data - skb_transport_header(skb);
if (len >= TCP_MSS_DEFAULT + sizeof(struct tcphdr) ||
(len >= TCP_MIN_MSS + sizeof(struct tcphdr) && !(tcp_flag_word(tcp_hdr(skb)) & TCP_REMNANT))) {
/* Subtract also invariant (if peer is RFC compliant), tcp header plus fixed timestamp option length.
* Resulting "len" is MSS free of SACK jitter. */
len -= tcp_sk(sk)->tcp_header_len;
icsk->icsk_ack.last_seg_size = len;
if (len == lss) {
icsk->icsk_ack.rcv_mss = len;
return;
}
}
}
}
在介绍最近一次的接收数据长度值last_seg_size,先来看一下TCP头部的长度tcp_header_len的值。对于发起TCP连接的客户端而言,如下函数tcp_connect_init所示,内核默认开启了timestamps选项,tcp_header_len的长度为标准TCP头部长度与timestamps选项长度之和。在接收到服务端回复的SYN+ACK报文后,如果服务端带有timestamp选项,tcp_header_len还是之前两者的和,否则,tcp_header_len仅为TCP标准头部的长度。
$ cat /proc/sys/net/ipv4/tcp_timestamps
1
static void tcp_connect_init(struct sock *sk)
{
tp->tcp_header_len = sizeof(struct tcphdr);
if (sock_net(sk)->ipv4.sysctl_tcp_timestamps)
tp->tcp_header_len += TCPOLEN_TSTAMP_ALIGNED;
}
static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb, const struct tcphdr *th)
{
if (th->ack) {
if (tp->rx_opt.saw_tstamp) {
tp->rx_opt.tstamp_ok = 1;
tp->tcp_header_len = sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED;
tp->advmss -= TCPOLEN_TSTAMP_ALIGNED;
} else {
tp->tcp_header_len = sizeof(struct tcphdr);
}
tcp_initialize_rcv_mss(sk);
}
}
对于TCP服务端而言,如果接收到的客户端SYN报文包含有timestamp选项,tcp_header_len为标准TCP头部长度与timestamp选项的长度之和,否则,其仅为TCP标准头部的长度。另外,对于如果SYN报文带有数据(TCP的fastopen),并且其报文长度大于等于默认MSS长度与tcp_header_len之和,服务端将初始化last_seg_size为报文长度减去TCP头部长度的所的值。
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) {
if (newtp->rx_opt.tstamp_ok) {
newtp->tcp_header_len = sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED;
} else {
newtp->tcp_header_len = sizeof(struct tcphdr);
}
if (skb->len >= TCP_MSS_DEFAULT + newtp->tcp_header_len)
newicsk->icsk_ack.last_seg_size = skb->len - newtp->tcp_header_len;
}
在接收MSS估算函数tcp_measure_rcv_mss中,使用TCP数据的长度加上TCP头部总长度(包括所有选项长度),之后减去tcp_header_len的长度,默认情况下tcp_header_len包括TCP标准头部长度和timestamp选项长度,得到的值为TCP数据长度与SACK选项的长度之和(假设有SACK选项)。此值记录未近次接收数据段长度last_seg_size。
三、对端MSS与本端通告窗口
关于通告接收窗口的内容详见:https://blog.csdn.net/sinat_20184565/article/details/89037265。通告窗口值的选择函数__tcp_select_window,起初内核使用MSS钳制值mss_clamp为基础进行窗口值的推倒,当前改为了使用估算的对端MSS值,参考内核代码中的注释,可能由于rcv_mss的估算抖动导致TCP性能的下降。
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;
}
}
窗口增长函数tcp_grow_window如下,如果数据报文的长度大于等于数据报文所占用空间truesize所换算的窗口空间,表明本端缓存空间充裕,内核将窗口增加本地通告MSS值advmss的两倍。反之如果数据包长度小于truesize换算的空间大小,本地缓存可能将要不足,但是窗口也有可能按照对端MSS的2倍增长。
static void tcp_grow_window(struct sock *sk, const struct sk_buff *skb)
{
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);
}
}
static int __tcp_grow_window(const struct sock *sk, const struct sk_buff *skb)
{
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;
}