当前位置: 首页 > 工具软件 > MSS > 使用案例 >

TCP发送MSS值

陆沈浪
2023-12-01

涉及到通过MSS值mss_clamp,用户设置MSS值user_mss和当前使用的MSS值mss_cache。

客户端通告MSS协商

内核依照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;
        }
    }
}

服务端通告MSS协商

服务端在接收到客户端的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发送报文长度

在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

 

 类似资料: