TCP选项之MSS

✏️ 1、三次握手

🖋️ 1.1、客户端三次握手

客户端处理MSS的机会为:

  1. 发送SYN段时告诉服务器端本端能够接收的MSS

  2. 收到SYN+ACK后接收服务器端通告的MSS

🐹 1.1.1、SYN段MSS选项值

SYN段也是通过tcp_transmit_skb()发送的,在该函数中会调用tcp_syn_build_options()构造SYN段携带的选项,代码如下:

static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it,
			    gfp_t gfp_mask)
{
...
	//如果发送的是SYN段,则调用tcp_syn_build_options()构造要携带的TCP选项,其中
	//MSS选项的值就是tcp_advertise_mss()的返回值
	if (unlikely(tcb->flags & TCPCB_FLAG_SYN)) {
		tcp_syn_build_options((__be32 *)(th + 1),
				      tcp_advertise_mss(sk),
				      (sysctl_flags & SYSCTL_FLAG_TSTAMPS),
				      (sysctl_flags & SYSCTL_FLAG_SACK),
				      (sysctl_flags & SYSCTL_FLAG_WSCALE),
				      tp->rx_opt.rcv_wscale,
				      tcb->when,
				      tp->rx_opt.ts_recent,
#ifdef CONFIG_TCP_MD5SIG
				      md5 ? &md5_hash_location :
#endif
				      NULL);
	}
...
}

tcp_advertise_mss()

advertise为广告的意思,该函数用来计算要告诉对端的MSS值,对应到TCB中,其实就是根据本端设备的MTU计算tp->advmss的值。

从上面可以看出,tcp_advertise_mss()实际上就是取当前tp->advmss和路由表MSS二者中的最小值,而后者就是本地网卡的MTU-40tp->advmss的初始化是在tcp_connect_init()中完成的,该函数被tcp_connect()调用,也就是说该值的初始化是在SYN段的发送过程中进行的,代码如下:

SYN段中携带的MSS选项值实际上就是本端网络设备的MTU-40

🐹 1.1.2、接收SYN+ACK段

接收SYN+ACK段是在tcp_rcv_synsent_state_process()中处理的,其中会调用tcp_paser_option()解析SYN段中携带的选项,其中和MSS选项相关的代码如下:

总结:客户端在收到服务器端通告的MSS后,将其与应用程序通过TCP_MAXSEG设定的MSS值比较,将二者中的较小值保存在tp->rx_opt.mss_clamp中,该值会影响到客户端发送MSS的确定。

🖋️ 1.2、服务端三次握手

服务器端处理MSS的机会为:

  1. 收到SYN段后对客户端通告的MSS的处理;

  2. 发送SYN+ACK时携带的MSS选项值的确定;

🐹 1.2.1、接收SYN段

SYN段的核心处理在tcp_v4_conn_request()中完成的,其中和MSS选项解析相关的内容如下:

记录到req中的mss,在三次握手完成后,创建子套接字的TCB时会赋值给tp->rx_opt.mss_clamp,代码如下:

从代码上看,其实服务器端接收到SYN后,对MSS选项的处理流程与客户端收到SYN+ACK段后对MSS选项的处理流程是相同的。

🐹 1.2.2、发送SYN+ACK段

SYN+ACK段的发送过程主要由tcp_v4_send_synack()完成,其中和MSS选项相关的内容如下:

从代码上看,其实服务器端发送SYN+ACK段时,对MSS选项的处理流程与客户端发送SYN段时对MSS选项的处理流程是相同的。

🐹 1.2.3、收到ACK段

在上面,我们并没有看到服务器端对tp->advmss的初始化,实际上,这个过程是在收到握手的第三个ACK报文后执行的,代码如下:

🖋️ 1.3、三次握手总结

从前面的代码中可以看到,无论是服务器端还是客户端,它们对通告给对端的MSS值的选择方式是一样的,都是取自本地网卡的MTU-40,该值会被记录在tp->advmss中;收到对端通告的MSS后,和应用程序设定的tp->rx_opt.user_mss比较后,取二者中的较小者保存在tp->rx_opt.mss_clamp中,mss_clamp会影响发送过程中发送MSS的选择。

✏️ 2、发送过程中获取MSS

TCP数据发送之tcp_sendmsg()中有看到,tcp_sendmsg()的核心逻辑就是根据MSS将待发送数据切割成一个个的skb,过程中是通过调用tcp_current_mss()确定当前的发送MSS的。这篇笔记前面有提到,对端通告的MSS经过矫正后最终保存在了tp->rx_opt.mss_clamp中,按道理我们使用该值作为发送MSS就很好,然而并非如此简单,如下:

tcp_current_mss()

该接口用于获取当前有效的MSS,并且会根据MTU的大小设定tp->xmit_size_goal变量,该变量后续将用于组织skb,它的取值和TSOGSO等特性相关,不是这篇笔记要讨论的话题,先忽略。

tcp_sync_mss()

该函数用参数pmtu更新PMTU相关字段,其中icsk->icsk_pmtu_cookie保存的就是之前缓存的PMTU值,根据该PMTU值计算MSS后,将计算结果保存到tp->mss_cache中,该值就是当前最新的MSS值。

tcp_mtu_to_mss()

该函数将MTU转变为MSS值,会考虑IP选项、TCP选项。

实际上在三次握手过程中,TCP会多次调用tcp_sync_mss()更新MSS值,不过其原理相同,无非是根据设备MTU或者当前最新的PMTU更新。

✏️ 3、总结

MSS的更新规则:PMTU会更新到路由metrics[RTAX_MTU]中,每次发送操作时,都会重新检查是否需要更新发送MSS,此时会首先检查路由中的MSSicsk_pmtu_cookie中缓存的上一次PMTU值,如果二者不等,说明两次发送期间MTU发生了变化,这时会用路由MSS更新icsk_pmtu_cookie。然后决定最终的发送MSS时也会参考三次握手过程中协商的值mss_clamp,保证最终值不会超过该值,而mss_clamp又受限于TCP选项TCP_MAXSEG

Last updated

Was this helpful?