MSS(Maximum Segment Size,最大报文段大小)的概念是指TCP层所能够接收的最大段大小,该值只包括TCP段的数据部分,不包括选项部分。
另外,在TCP首部有一个MSS选项,在三次握手过程中,TCP发送端使用该选项告诉对方自己所能接受的最大段大小。
MSS选项虽然作为一个选项存在,原则上讲是可有可无的,但是目前绝大多数的TCP通信过程中都会携带该选项,所以可以说它是TCP中最重要的一个选项了。
关于MSS的内容,主要包括两个方面:
从客户端和服务器端的三次握手过程中可以看到RMSS是如何确定的。从连接态的数据发送过程可以看到SMSS是如何发乎作用的。
客户端处理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);
}
...
}
advertise为广告的意思,该函数用来计算要告诉对端的MSS值,对应到TCB中,其实就是根据本端设备的MTU计算tp->advmss的值。
static __u16 tcp_advertise_mss(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
//获取路由
struct dst_entry *dst = __sk_dst_get(sk);
//用tp->advmss初始化临时变量mss
int mss = tp->advmss;
//如果路由有效并且路由中的MSS比当前值小,那么用路由中的MSS更新tp->advmss
//因为路由中的MSS是根据网络设备的MTU得来的,必须尊重,可以认为路由中的MSS
//取值为min(65536-40, MTU-40),其中65535为IP报文的最大长度
if (dst && dst_metric(dst, RTAX_ADVMSS) < mss) {
mss = dst_metric(dst, RTAX_ADVMSS);
tp->advmss = mss;
}
//返回确定的mss
return (__u16)mss;
}
从上面可以看出,tcp_advertise_mss()实际上就是取当前tp->advmss和路由表MSS二者中的最小值,而后者就是本地网卡的MTU-40,下面需要继续看下tp->advmss的初始值的如何确定的。
搜索代码可以发现,该字段的初始化是在tcp_connect_init()中完成的,该函数被tcp_connect()调用,也就是说该值的初始化是在SYN段的发送过程中进行的,代码如下:
static void tcp_connect_init(struct sock *sk)
{
...
//即也是来源于本端MTU
tp->advmss = dst_metric(dst, RTAX_ADVMSS);
...
}
小结一下:SYN段中携带的MSS选项值实际上就是本端网络设备的MTU-40。
接收SYN+ACK段是在tcp_rcv_synsent_state_process()中处理的,其中会调用tcp_paser_option()解析SYN段中携带的选项,其中和MSS选项相关的代码如下:
void tcp_parse_options(struct sk_buff *skb, struct tcp_options_received *opt_rx,
int estab)
{
...
switch (opcode) {
case TCPOPT_MSS:
if (opsize == TCPOLEN_MSS && th->syn && !estab) {
//取得选项中携带的MSS值记录到临时变量in_mss中
u16 in_mss = ntohs(get_unaligned((__be16 *)ptr));
if (in_mss) {
//user_mss是应用程序通过TCP选项TCP_MAXSEG设定的,如果不设置,默认为0;
//如果设定了user_mss并且设定的值小于对端通告的,那么调整in_mss为两者中的最小值。
if (opt_rx->user_mss &&
opt_rx->user_mss < in_mss)
in_mss = opt_rx->user_mss;
//将min(user_mss, in_mss)记录到mss_clamp中
opt_rx->mss_clamp = in_mss;
}
}
break;
}
...
}
小结一下:客户端在收到服务器端通告的MSS后,将其与应用程序通过TCP_MAXSEG设定的MSS值比较,将二者中的较小值保存在tp->rx_opt.mss_clamp中,该值会影响到客户端发送MSS的确定,见下文。
服务器端处理MSS的机会为:
SYN段的核心处理在tcp_v4_conn_request()中完成的,其中和MSS选项解析相关的内容如下:
int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
...
//将SYN段中携带的选项先解析到临时变量tmp_opt中
struct tcp_options_received tmp_opt;
struct request_sock *req;
...
//先清空临时变量
tcp_clear_options(&tmp_opt);
//将临时变量中的mss_clamp初始化为536
tmp_opt.mss_clamp = 536;
//user_mss设置为用户通过套接字选项TCP_MAXSEG设定的值
tmp_opt.user_mss = tcp_sk(sk)->rx_opt.user_mss;
//该函数上面已经见过,它会比较SYN段中携带的MSS和user_mss,
//然后取二者中较小者保存在tmp_opt.mss_clamp中
tcp_parse_options(skb, &tmp_opt, 0);
...
//初始化连接请求块
tcp_openreq_init(req, &tmp_opt, skb);
...
}
static inline void tcp_openreq_init(struct request_sock *req,
struct tcp_options_received *rx_opt,
struct sk_buff *skb)
{
...
//最终确定下来的MSS记录到了连接请求块的mss字段中
req->mss = rx_opt->mss_clamp;
...
}
记录到req中的mss,在三次握手完成后,创建子套接字的TCB时会赋值给tp->rx_opt.mss_clamp,代码如下:
struct sock *tcp_create_openreq_child(struct sock *sk, struct request_sock *req, struct sk_buff *skb)
{
...
newtp->rx_opt.mss_clamp = req->mss;
...
}
小结一下:从代码上看,其实服务器端接收到SYN后,对MSS选项的处理流程与客户端收到SYN+ACK段后对MSS选项的处理流程是相同的。
SYN+ACK段的发送过程主要由tcp_v4_send_synack()完成,其中和MSS选项相关的内容如下:
static int tcp_v4_send_synack(struct sock *sk, struct request_sock *req,
struct dst_entry *dst)
{
...
//创建SYN+ACK段
skb = tcp_make_synack(sk, dst, req);
...
}
struct sk_buff *tcp_make_synack(struct sock *sk, struct dst_entry *dst,
struct request_sock *req)
{
...
//为SYN+ACK段构造TCP选项,第二个参数就是要发送给客户端的MSS,来自于路由
tcp_syn_build_options((__be32 *)(th + 1), dst_metric(dst, RTAX_ADVMSS), ireq->tstamp_ok,
ireq->sack_ok, ireq->wscale_ok, ireq->rcv_wscale,
TCP_SKB_CB(skb)->when,
req->ts_recent,
(
#ifdef CONFIG_TCP_MD5SIG
md5 ? &md5_hash_location :
#endif
NULL)
);
...
}
小结一下:从代码上看,其实服务器端发送SYN+ACK段时,对MSS选项的处理流程与客户端发送SYN段时对MSS选项的处理流程是相同的。
在上面,我们并没有看到服务器端对tp->advmss的初始化,实际上,这个过程是在收到握手的第三个ACK报文后执行的,代码如下:
struct sock *tcp_v4_syn_recv_sock(struct sock *sk, struct sk_buff *skb,
struct request_sock *req,
struct dst_entry *dst)
{
...
//取值就是路由中的MSS值
newtp->advmss = dst_metric(dst, RTAX_ADVMSS);
...
}
从前面的代码中可以看到,无论是服务器端还是客户端,它们对通告给对端的MSS值的选择方式是一样的,都是取自本地网卡的MTU-40,该值会被记录在tp->advmss中;收到对端通告的MSS后,和应用程序设定的tp->rx_opt.user_mss比较后,取二者中的较小者保存在tp->rx_opt.mss_clamp中,mss_clamp会影响发送过程中发送MSS的选择。
在TCP数据发送之tcp_sendmsg()中有看到,tcp_sendmsg()的核心逻辑就是根据MSS将待发送数据切割成一个个的skb,过程中是通过调用tcp_current_mss()确定当前的发送MSS的。这篇笔记前面有提到,对端通告的MSS经过矫正后最终保存在了tp->rx_opt.mss_clamp中,按道理我们使用该值作为发送MSS就很好啊,然而并非如此简单,如下:
int tcp_sendmsg(struct kiocb *iocb, struct socket *sock, struct msghdr *msg,
size_t size)
{
...
//获取当前有效的MSS,如果没有带外数据,就可以使用更大的段(TSO相关)
mss_now = tcp_current_mss(sk, !(flags&MSG_OOB));
size_goal = tp->xmit_size_goal;
...
}
该接口用于获取当前有效的MSS,并且会根据MTU的大小设定tp->xmit_size_goal变量,该变量后续将用于组织skb,它的取值和TSO、GSO等特性相关,不是这篇笔记要讨论的话题,先忽略。
/* Compute the current effective MSS, taking SACKs and IP options,
* and even PMTU discovery events into account.
*
* LARGESEND note: !urg_mode is overkill, only frames up to snd_up
* cannot be large. However, taking into account rare use of URG, this
* is not a big flaw.
*/
unsigned int tcp_current_mss(struct sock *sk, int large_allowed)
{
struct tcp_sock *tp = tcp_sk(sk);
struct dst_entry *dst = __sk_dst_get(sk);
//下面最终决策出的当前有效发送MSS值会记录到该变量中
u32 mss_now;
u16 xmit_size_goal;
int doing_tso = 0;
//mss_now来自于tp->mss_cache,一脸懵逼,到目前为止还没见过该字段(见下文)
mss_now = tp->mss_cache;
//判断是否允许TSO,忽略
if (large_allowed && sk_can_gso(sk) && !tp->urg_mode)
doing_tso = 1;
if (dst) {
//获取路由中保存的PMTU值
u32 mtu = dst_mtu(dst);
//icsk_pmut_cookie为上次缓存的PMTU值,其初始值为本端MTU大小,
//如果二者不等,则说明PMTU发生了变化,需要调用tcp_sync_mss()更新MSS
if (mtu != inet_csk(sk)->icsk_pmtu_cookie)
mss_now = tcp_sync_mss(sk, mtu);
}
//如果TCP有SACK选项,则从MSS中减去相应的开销
if (tp->rx_opt.eff_sacks)
mss_now -= (TCPOLEN_SACK_BASE_ALIGNED + (tp->rx_opt.eff_sacks * TCPOLEN_SACK_PERBLOCK));
#ifdef CONFIG_TCP_MD5SIG
if (tp->af_specific->md5_lookup(sk, sk))
mss_now -= TCPOLEN_MD5SIG_ALIGNED;
#endif
//下面的代码用来确定xmit_size_goal的值,该值和TSO相关,先忽略
xmit_size_goal = mss_now;
if (doing_tso) {
xmit_size_goal = (65535 -
inet_csk(sk)->icsk_af_ops->net_header_len -
inet_csk(sk)->icsk_ext_hdr_len -
tp->tcp_header_len);
xmit_size_goal = tcp_bound_to_half_wnd(tp, xmit_size_goal);
xmit_size_goal -= (xmit_size_goal % mss_now);
}
tp->xmit_size_goal = xmit_size_goal;
//返回当前有效的MSS值
return mss_now;
}
该函数用参数pmtu更新PMTU相关字段,其中icsk->icsk_pmtu_cookie保存的就是之前缓存的PMTU值,根据该PMTU值计算MSS后,将计算结果保存到tp->mss_cache中,该值就是当前最新的MSS值。
/* This function synchronize snd mss to current pmtu/exthdr set.
tp->rx_opt.user_mss is mss set by user by TCP_MAXSEG. It does NOT counts
for TCP options, but includes only bare TCP header.
tp->rx_opt.mss_clamp is mss negotiated at connection setup.
It is minimum of user_mss and mss received with SYN.
It also does not include TCP options.
inet_csk(sk)->icsk_pmtu_cookie is last pmtu, seen by this function.
tp->mss_cache is current effective sending mss, including
all tcp options except for SACKs. It is evaluated,
taking into account current pmtu, but never exceeds
tp->rx_opt.mss_clamp.
NOTE1. rfc1122 clearly states that advertised MSS
DOES NOT include either tcp or ip options.
NOTE2. inet_csk(sk)->icsk_pmtu_cookie and tp->mss_cache
are READ ONLY outside this function. --ANK (980731)
*/
//注释很重要,仔细看
unsigned int tcp_sync_mss(struct sock *sk, u32 pmtu)
{
struct tcp_sock *tp = tcp_sk(sk);
struct inet_connection_sock *icsk = inet_csk(sk);
int mss_now;
if (icsk->icsk_mtup.search_high > pmtu)
icsk->icsk_mtup.search_high = pmtu;
//根据PMTU值计算MSS,这里除了标准的IP、TCP首部外,还会考虑IP选项、TCP选项
mss_now = tcp_mtu_to_mss(sk, pmtu);
//调整MSS为当前发送窗口的一半
mss_now = tcp_bound_to_half_wnd(tp, mss_now);
/* And store cached results */
//将PMTU缓存到icsk_pmtu_cookie中
icsk->icsk_pmtu_cookie = pmtu;
if (icsk->icsk_mtup.enabled)
mss_now = min(mss_now, tcp_mtu_to_mss(sk, icsk->icsk_mtup.search_low));
//最终决策出来的MSS保存在mss_cache中
tp->mss_cache = mss_now;
return mss_now;
}
注:上面有几个和PMTU机制强相关的几个字段还没有弄清楚,弄清楚后回来补充…
该函数将MTU转变为MSS值,会考虑IP选项、TCP选项。
/* Not accounting for SACKs here. */
int tcp_mtu_to_mss(struct sock *sk, int pmtu)
{
struct tcp_sock *tp = tcp_sk(sk);
struct inet_connection_sock *icsk = inet_csk(sk);
int mss_now;
/* Calculate base mss without TCP options:
It is MMS_S - sizeof(tcphdr) of rfc1122
*/
//从MTU中减去标准TCP首部、IP首部
mss_now = pmtu - icsk->icsk_af_ops->net_header_len - sizeof(struct tcphdr);
/* Clamp it (mss_clamp does not include tcp options) */
//MSS不能超过对端通告的MSS
if (mss_now > tp->rx_opt.mss_clamp)
mss_now = tp->rx_opt.mss_clamp;
/* Now subtract optional transport overhead */
//减去扩展首部,启用IPsec时,会有扩展首部
mss_now -= icsk->icsk_ext_hdr_len;
/* Then reserve room for full set of TCP options and 8 bytes of data */
//MSS最小不能小于48字节
if (mss_now < 48)
mss_now = 48;
/* Now subtract TCP options size, not including SACKs */
//减去TCP选项长度(不包括选择ACK选项),tp->tcp_header_len的值是该TCP连接中可能的最大TCP
//首部长度,该值是在三次握手过程中根据双方对TCP选项的支持情况确定的
mss_now -= tp->tcp_header_len - sizeof(struct tcphdr);
return mss_now;
}
小结一下:实际上在三次握手过程中,TCP会多次调用tcp_sync_mss()更新MSS值,不过其原理相同,无非是根据设备MTU或者当前最新的PMTU更新,这里不再一一列举。
正如开头所说,MSS相关内容可以分为发送MSS和接收MSS两部分,上面详细分析了这两种MSS在代码中的相关处理逻辑,这里用一张图来表示它们之间的关系:
图30-9是发送MSS的更新规则。PMTU会更新到路由metrics[RTAX_MTU]中,每次发送操作时,都会重新检查是否需要更新发送MSS,此时会首先检查路由中的MSS和icsk_pmtu_cookie中缓存的上一次PMTU值,如果二者不等,说明两次发送期间MTU发生了变化,这时会用路由MSS更新icsk_pmtu_cookie。然后决定最终的发送MSS时也会参考三次握手过程中协商的值mss_clamp,保证最终值不会超过该值,而mss_clamp又受限于TCP选项TCP_MAXSEG。
图30-10是通告MSS的确定规则。非常直接,就是来自于网络设备的MTU。