TCP选项之MSS
✏️ 1、三次握手
🖋️ 1.1、客户端三次握手
客户端处理MSS的机会为:
发送SYN段时告诉服务器端本端能够接收的
MSS;收到
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-40,tp->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的机会为:
收到SYN段后对客户端通告的
MSS的处理;发送
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()
tcp_current_mss()该接口用于获取当前有效的MSS,并且会根据MTU的大小设定tp->xmit_size_goal变量,该变量后续将用于组织skb,它的取值和TSO、GSO等特性相关,不是这篇笔记要讨论的话题,先忽略。
tcp_sync_mss()
tcp_sync_mss()该函数用参数pmtu更新PMTU相关字段,其中icsk->icsk_pmtu_cookie保存的就是之前缓存的PMTU值,根据该PMTU值计算MSS后,将计算结果保存到tp->mss_cache中,该值就是当前最新的MSS值。
tcp_mtu_to_mss()
tcp_mtu_to_mss()该函数将MTU转变为MSS值,会考虑IP选项、TCP选项。
实际上在三次握手过程中,TCP会多次调用tcp_sync_mss()更新MSS值,不过其原理相同,无非是根据设备MTU或者当前最新的PMTU更新。
✏️ 3、总结

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