涉及到通过MSS值mss_clamp,用户设置MSS值user_mss和当前使用的MSS值mss_cache。
内核依照RFC1122, RFC2581中的规定,将默认的MSS钳制值定义为536,见宏TCP_MSS_DEFAULT。
#define TCP_MSS_DEFAULT 536U
客户端在发送SYN请求报文前,设置MSS钳制值为TCP_MSS_DEFAULT。
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
tp->rx_opt.mss_clamp = TCP_MSS_DEFAULT;
err = tcp_connect(sk);
}
当TCP客户端发起连接建立请求时,通过函数tcp_connect实现,其中调用tcp_connect_init函数初始化连接相关参数,包括最大MSS协商值mss_clamp。如果用户层通过setsockopt选项TCP_MAXSEG指定了MSS值,使用用户指定值。函数tcp_sync_mss将当前连接的路径MTU转换为缓存MSS值mss_cache。tcp_mss_clamp函数将通过路由系统取到的MSS值与通过setsockopt设置的值user_mss比较,采用较小的一个作为最终通告值advmss。
static void tcp_connect_init(struct sock *sk)
{
const struct dst_entry *dst = __sk_dst_get(sk);
if (tp->rx_opt.user_mss)
tp->rx_opt.mss_clamp = tp->rx_opt.user_mss;
tp->max_window = 0;
tcp_sync_mss(sk, dst_mtu(dst));
tp->advmss = tcp_mss_clamp(tp, dst_metric_advmss(dst));
tcp_initialize_rcv_mss(sk);
}
用户可通过IP命令设置某条路由的通告MSS值,如未进行设置,使用具体协议的默认通告MSS获取函数,对于IPv4而言其为ipv4_default_advmss。如果用户通过IP路由命令锁定了MTU值(mtu lock),取其值,否则取路由出口设备的MTU值,需要减去IP和TCP头部长度而得到所需的MSS值,并且MSS值不能小于最小的advmss值ip_rt_min_advmss,可通过PROC文件min_adv_mss设置此值,默认为256。最终值不能大于最大PMTU值65535减去IP和TCP头部长度得到的值。
$ ip route add 192.168.20.2 via 192.168.1.1 mtu lock 1040 advmss 1000
#define IPV4_MAX_PMTU 65535
static int ip_rt_min_advmss __read_mostly = 256;
static struct dst_ops ipv4_dst_ops = {
.family = AF_INET,
.default_advmss = ipv4_default_advmss,
}
static unsigned int ipv4_default_advmss(const struct dst_entry *dst)
{
unsigned int header_size = sizeof(struct tcphdr) + sizeof(struct iphdr);
unsigned int advmss = max_t(unsigned int, ipv4_mtu(dst) - header_size, ip_rt_min_advmss);
return min(advmss, IPV4_MAX_PMTU - header_size);
}
$ cat /proc/sys/net/ipv4/route/min_adv_mss
256
随后,在发送SYN报文时,使用函数tcp_syn_options添加MSS选项,其值由函数tcp_advertise_mss获得。以下代码可知,路由系统获取到的MSS值优先级高于advmss值,当前值小于advmss时,通告使用由路由中得到的值。否则,使用advmss保存的值。
static __u16 tcp_advertise_mss(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
const struct dst_entry *dst = __sk_dst_get(sk);
int mss = tp->advmss;
if (dst) {
unsigned int metric = dst_metric_advmss(dst);
if (metric < mss) {
mss = metric;
tp->advmss = mss;
}
}
return (__u16)mss;
}
static unsigned int tcp_syn_options(struct sock *sk, struct sk_buff *skb, struct tcp_out_options *opts, struct tcp_md5sig_key **md5)
{
opts->mss = tcp_advertise_mss(sk);
}
客户端在接收到服务端的SYN+ACK回复报文后,如果发现对端发送了TCP的timestamp选项,需要将tcp_header_len增加上此选项的长度TCPOLEN_TSTAMP_ALIGNED,并且通告MSS值advmss也需要减去此选项的长度。
static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb, const struct tcphdr *th)
{
int saved_clamp = tp->rx_opt.mss_clamp;
tcp_parse_options(sock_net(sk), skb, &tp->rx_opt, 0, &foc);
if (th->ack) {
if (!th->syn)
goto discard_and_undo;
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;
tcp_store_ts_recent(tp);
} else {
tp->tcp_header_len = sizeof(struct tcphdr);
}
tcp_sync_mss(sk, icsk->icsk_pmtu_cookie);
}
/* "fifth, if neither of the SYN or RST bits is set then drop the segment and return." */
discard_and_undo:
tcp_clear_options(&tp->rx_opt);
tp->rx_opt.mss_clamp = saved_clamp;
goto discard;
reset_and_undo:
tcp_clear_options(&tp->rx_opt);
tp->rx_opt.mss_clamp = saved_clamp;
return 1;
}
处理函数tcp_rcv_synsent_state_process另外一个相关功能就是解析服务端发送来的TCP选项信息,参见函数tcp_parse_options,注意服务端同样适用此函数解析客户端发送的TCP选项信息。对于MSS选项,如果服务端发送而来的MSS值小于用户在本地套接口设置的值user_mss,适用服务端的通告MSS值作为本地MSS的钳制值mss_clamp。
在函数tcp_rcv_synsent_state_process处理之前,预先保存钳制值mss_clamp,遇到错误情况时,恢复之前保存的mss_clamp值。
void tcp_parse_options(const struct net *net, const struct sk_buff *skb, struct tcp_options_received *opt_rx, int estab, struct tcp_fastopen_cookie *foc)
{
int length = (th->doff * 4) - sizeof(struct tcphdr);
while (length > 0) {
switch (opcode) {
default:
opsize = *ptr++;
if (opsize < 2) /* "silly options" */
return;
if (opsize > length)
return; /* don't parse partial options */
switch (opcode) {
case TCPOPT_MSS:
if (opsize == TCPOLEN_MSS && th->syn && !estab) {
u16 in_mss = get_unaligned_be16(ptr);
if (in_mss) {
if (opt_rx->user_mss && opt_rx->user_mss < in_mss)
in_mss = opt_rx->user_mss;
opt_rx->mss_clamp = in_mss;
}
}
break;
}
}
}
服务端在接收到客户端的SYN报文之后,首先将本端的MSS钳制值初始化为默认的TCP_MSS_DEFAULT(536),之后使用函数tcp_parse_options解析客户端的TCP选项信息。参见以上的结束,如果客户端MSS选项中的MSS值小于用户设置的值user_mss,使用客户端发送的MSS值作为钳制值。如果用户未设置MSS值user_mss,直接使用客户端发送的MSS为钳制值。
int tcp_conn_request(struct request_sock_ops *rsk_ops, const struct tcp_request_sock_ops *af_ops, struct sock *sk, struct sk_buff *skb)
{
tcp_rsk(req)->af_specific = af_ops;
tcp_clear_options(&tmp_opt);
tmp_opt.mss_clamp = af_ops->mss_clamp;
tmp_opt.user_mss = tp->rx_opt.user_mss;
tcp_parse_options(sock_net(sk), skb, &tmp_opt, 0, want_cookie ? NULL : &foc);
tcp_openreq_init(req, &tmp_opt, skb, sk);
}
static const struct tcp_request_sock_ops tcp_request_sock_ipv4_ops = {
.mss_clamp = TCP_MSS_DEFAULT,
}
在接下来的tcp_openreq_init函数中,将得到的MSS钳制值赋予新建的请求套接口成员mss。在TCP的三次握手完成之后,服务端创建子套接口,将请求套接口保存的MSS钳制值赋予新建的子套接口rx_opt->mss_clamp。
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->mss = rx_opt->mss_clamp;
}
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->rx_opt.mss_clamp = req->mss;
}
SYN请求报文检查正常处理之后,服务端回复SYN+ACK报文,如下函数tcp_make_synack,创建报文过程中使用tcp_synack_options函数初始化MSS选项通告的MSS值。其由函数tcp_mss_clamp获得。
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)
{
mss = tcp_mss_clamp(tp, dst_metric_advmss(dst));
tcp_header_size = tcp_synack_options(sk, req, mss, skb, &opts, md5, foc) + sizeof(*th);
}
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, const struct tcp_md5sig_key *md5, struct tcp_fastopen_cookie *foc)
{
struct inet_request_sock *ireq = inet_rsk(req);
opts->mss = mss;
}
如上所述,函数tcp_mss_clamp首先考虑用户通过setsockopt选项TCP_MAXSEG设置的MSS值,如果其小于通过路由系统获得的值,使用TCP_MAXSEG的设置值。反之,如果用户未设置套接口的TCP_MAXSEG值,或者其值大于路由系统获得的MSS值,使用路由系统的MSS值。
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;
}
最后,当接收到客户端回复的第三个ACK握手报文后,进入服务端的状态机处理程序tcp_rcv_state_process,这里检查一下TCP选项timestamp是否存在,存在的话将本地监听套接口的MSS通告值advmss减去此选项的长度TCPOLEN_TSTAMP_ALIGNED。
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_sync_mss将路由系统的路径MTU值转换到新的子套接口的缓存MSS值mss_cache,并且初始化子套接口的通告advmss值,设置方法与回复SYN+ACK报文时相同。
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)
{
newsk = tcp_create_openreq_child(sk, req, skb);
tcp_sync_mss(newsk, dst_mtu(dst));
newtp->advmss = tcp_mss_clamp(tcp_sk(sk), dst_metric_advmss(dst));
}
以下函数tcp_sync_mss负责将路径MTU值推算为MSS值,其中涉及TCP的MTU探测功能的部分可参考:https://blog.csdn.net/sinat_20184565/article/details/89163458。首先tcp_mtu_to_mss函数,将路径MTU减去IP与TCP标准头部,以及除SACK选项以外的其它所有选项而得到仅包含数据长度的MSS值,其次tcp_bound_to_half_wnd再将其值缩小到窗口的一半大小,即得到最终的通告MSS值,将其赋予mss_cache变量。同时,保存当前的路径MTU到变量icsk_pmtu_cookie中。
unsigned int tcp_sync_mss(struct sock *sk, u32 pmtu)
{
if (icsk->icsk_mtup.search_high > pmtu)
icsk->icsk_mtup.search_high = pmtu;
mss_now = tcp_mtu_to_mss(sk, pmtu);
mss_now = tcp_bound_to_half_wnd(tp, mss_now);
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));
tp->mss_cache = mss_now;
return mss_now;
}
服务端在调用tcp_v4_syn_recv_sock函数之前,先要解析客户端发送而来的TCP选项信息,如以下函数tcp_check_req所示,tcp_parse_options函数可见以上的介绍,如果客户端通过TCP选项发送来的MSS值小于服务器本地用户设置的值user_mss,采用客户端发送的MSS值作为服务端的MSS钳制值mss_clamp。
struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb, struct request_sock *req, bool fastopen)
{
struct tcp_options_received tmp_opt;
const struct tcphdr *th = tcp_hdr(skb);
tmp_opt.saw_tstamp = 0;
if (th->doff > (sizeof(struct tcphdr)>>2)) {
tcp_parse_options(sock_net(sk), skb, &tmp_opt, 0, NULL);
}
child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL, req, &own_req);
}
在TCP的三次握手过程中,客户端与服务端协商了MSS的钳制值mss_clamp,并且由函数tcp_sync_mss更新了MSS缓存值mss_cache和路径MTU缓存值icsk_pmtu_cookie。但是mss_cache值未考虑SACK选项的长度,在数据发送时,使用的MSS值由函数tcp_current_mss确定。此函数不仅考虑了SACK选项,而且考虑到了可能的PMTU更新等,得到实际发送数据需要的MSS值。
使用dst_mtu检查PMTU是否已更新,使用函数tcp_established_options计算连接建立之后的TCP选项长度。
unsigned int tcp_current_mss(struct sock *sk)
{
const struct dst_entry *dst = __sk_dst_get(sk);
struct tcp_out_options opts;
mss_now = tp->mss_cache;
if (dst) {
u32 mtu = dst_mtu(dst);
if (mtu != inet_csk(sk)->icsk_pmtu_cookie)
mss_now = tcp_sync_mss(sk, mtu);
}
header_len = tcp_established_options(sk, NULL, &opts, &md5) + sizeof(struct tcphdr);
if (header_len != tp->tcp_header_len) {
int delta = (int) header_len - tp->tcp_header_len;
mss_now -= delta;
}
return mss_now;
}
在TCP数据发送路径中,使用如下函数tcp_send_mss得到当前的MSS值,即tcp_current_mss函数的结果。另外,如果支持GSO特性(Generic Segmentation Offload),函数tcp_xmit_size_goal将获得一个无需按照MSS值分段的最大数据包长度size_goal,否则,size_goal等于MSS,在发送函数如tcp_sendmsg_locked中执行分段操作。如果支持GSO,并且物理发送设备支持TSO(TCP Segmentation Offload)功能,分段工作将由物理硬件完成。
static int tcp_send_mss(struct sock *sk, int *size_goal, int flags)
{
mss_now = tcp_current_mss(sk);
*size_goal = tcp_xmit_size_goal(sk, mss_now, !(flags & MSG_OOB));
return mss_now;
}
另外一个需要注意的函数为select_size,其根据当前的MSS值mss_cache,得到分配一个新的skb结构所需要的数据缓存的长度值。如果不支持NETIF_F_SG特性,sg为零,新分配的skb数据缓存长度等于当前MSS缓存值mss_cache。反之,如果支持NETIF_F_SG特性,并且支持GSO的话,skb缓存长度值由函数linear_payload_sz确定。如果当前发送队列与重传队列都为空,此为第一个要发送的数据包,缓存长度设置为2048减去MAX_TCP_HEADER再减去skb_shared_info结构长度之后的值,否则缓存长度为零。
如果不支持GSO的话,尝试分配一个页面空间。如果当前MSS缓存值加上最大的TCP头部长度MAX_TCP_HEADER超出一个页面空间,但是小于页面除TCP头部之外的剩余空间与最大skb分段数减一和页面长度的乘积,意味着一个skb结构加上frags空间足以存放MSS缓存mss_cache大小的数据,将页面剩余空间作为需要分配的缓存空间返回。
static int select_size(const struct sock *sk, bool sg, bool first_skb)
{
int tmp = tp->mss_cache;
if (sg) {
if (sk_can_gso(sk)) {
tmp = linear_payload_sz(first_skb);
} else {
int pgbreak = SKB_MAX_HEAD(MAX_TCP_HEADER);
if (tmp >= pgbreak &&
tmp <= pgbreak + (MAX_SKB_FRAGS - 1) * PAGE_SIZE)
tmp = pgbreak;
}
}
return tmp;
}
#define SKB_MAX_ORDER(X, ORDER) SKB_WITH_OVERHEAD((PAGE_SIZE << (ORDER)) - (X))
#define SKB_MAX_HEAD(X) (SKB_MAX_ORDER((X), 0))
static int linear_payload_sz(bool first_skb)
{
if (first_skb)
return SKB_WITH_OVERHEAD(2048 - MAX_TCP_HEADER);
return 0;
}
#define SKB_WITH_OVERHEAD(X) ((X) - SKB_DATA_ALIGN(sizeof(struct skb_shared_info)))
以发送函数tcp_sendmsg_locked为例看一下MSS的使用。如下tcp_send_mss得到两个值,一个分段用的MSS和一个GSO使用的size_goal。如果发送队列尾部的数据包skb的IP校验设置为CHECKSUM_NONE,导致网卡的TSO功能不可用(TSO要求必须同时支持校验和卸载),禁用GSO的size_goal值,按照MSS进行分段。
int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size)
{
restart:
mss_now = tcp_send_mss(sk, &size_goal, flags);
while (msg_data_left(msg)) {
int max = size_goal;
skb = tcp_write_queue_tail(sk);
if (skb) {
if (skb->ip_summed == CHECKSUM_NONE)
max = mss_now;
copy = max - skb->len;
}
如果队列尾部的数据包已经没有空间(copy<=0),需要分配新的skb以存储用户层数据,新的skb的数据空间大小由以上介绍的函数select_size决定。之后,拷贝用户层数据到新分配的skb缓存中。
if (copy <= 0 || !tcp_skb_can_collapse_to(skb)) {
skb = sk_stream_alloc_skb(sk, select_size(sk, sg, first_skb), sk->sk_allocation, first_skb);
if (!skb) goto wait_for_memory;
/* Check whether we can use HW checksum. */
if (sk_check_csum_caps(sk))
skb->ip_summed = CHECKSUM_PARTIAL;
skb_entail(sk, skb);
copy = size_goal;
max = size_goal;
}
之后,如果需要强制push数据包或者此数据包为发送队列的第一个数据包,使用函数__tcp_push_pending_frames或者tcp_push_one执行数据包发送。反之,如果两个条件都不成立,继续拷贝用户层数据,向接收队列中添加。如果遇到TCP缓存不足的情况,进入缓存等待状态,在此之前由函数tcp_push发送队列中的数据。
if (forced_push(tp)) {
tcp_mark_push(tp, skb);
__tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH);
} else if (skb == tcp_send_head(sk))
tcp_push_one(sk, mss_now);
continue;
wait_for_sndbuf:
set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);
wait_for_memory:
if (copied)
tcp_push(sk, flags & ~MSG_MORE, mss_now, TCP_NAGLE_PUSH, size_goal);
err = sk_stream_wait_memory(sk, &timeo);
if (err != 0)
goto do_error;
mss_now = tcp_send_mss(sk, &size_goal, flags);
}
}
发送函数__tcp_push_pending_frames、tcp_push_one和tcp_push都是最终调用tcp_write_xmit函数发送数据。其通过函数tcp_init_tso_segs计算以当前mss_now为长度的TSO分段的个数,以及tcp_gso_size最终分段的长度。确保数据包在GSO或者TSO分段时的长度按照MSS的值进行。
static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle, int push_one, gfp_t gfp)
{
max_segs = tcp_tso_segs(sk, mss_now);
while ((skb = tcp_send_head(sk))) {
tso_segs = tcp_init_tso_segs(skb, mss_now);
limit = mss_now;
if (tso_segs > 1 && !tcp_urg_mode(tp))
limit = tcp_mss_split_point(sk, skb, mss_now, min_t(unsigned int, cwnd_quota, max_segs), nonagle);
if (skb->len > limit && unlikely(tso_fragment(sk, TCP_FRAG_IN_WRITE_QUEUE, skb, limit, mss_now, gfp)))
break;
if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp)))
break;
}
}
static int tcp_init_tso_segs(struct sk_buff *skb, unsigned int mss_now)
{
int tso_segs = tcp_skb_pcount(skb);
if (!tso_segs || (tso_segs > 1 && tcp_skb_mss(skb) != mss_now)) {
tcp_set_skb_tso_segs(skb, mss_now);
tso_segs = tcp_skb_pcount(skb);
}
return tso_segs;
}
static void tcp_set_skb_tso_segs(struct sk_buff *skb, unsigned int mss_now)
{
if (skb->len <= mss_now || skb->ip_summed == CHECKSUM_NONE) {
tcp_skb_pcount_set(skb, 1);
TCP_SKB_CB(skb)->tcp_gso_size = 0;
} else {
tcp_skb_pcount_set(skb, DIV_ROUND_UP(skb->len, mss_now));
TCP_SKB_CB(skb)->tcp_gso_size = mss_now;
}
}
内核版本 4.15