GSO和TSO

  网络设备一次能够传输的最大数据量就是MTU,即IP传递给网络设备的每一个数据包不能超过MTU个字节,IP层的分段和重组功能就是为了适配网络设备的MTU而存在的。从理论上来讲,TCP可以不关心MTU的限定,只需要按照自己的意愿随意的将数据包丢给IP,是否需要分段可以由IP透明的处理,但是由于TCP是可靠性的流传输,如果是在IP层负责传输那么由于仅有首片的IP报文中含有TCP,后面的TCP报文如果在传输过程中丢失,通信的双方是无法感知的,基于此TCP在实现时总是会基于MTU设定自己的发包大小,尽量避免让数据包在IP层分片,也就是说TCP会保证一个TCP段经过IP封装后传给网络设备时,数据包的大小不会超过网络设备的MTU。

  TCP的这种实现会使得其必须对用户空间传入的数据进行分段,这种工作很固定,但是会耗费CPU时间,所以在高速网络中就想优化这种操作。优化的思路就是TCP将大块数据(远超MTU)传给网络设备,由网络设备按照MTU来分段,从而释放CPU资源,这就是TSO(TCP Segmentation Offload)的设计思想。

  显然,TSO需要网络设备硬件支持。更近一步,TSO实际上是一种延迟分段技术,延迟分段会减少发送路径上的数据拷贝操作,所以即使网络设备不支持TSO,只要能够延迟分段也是有收益的,而且也不仅仅限于TCP,对于其它L4协议也是可以的,这就衍生出了GSO(Generic Segmentation Offload)。这种技术是指尽可能的延迟分段,最好是在设备驱动程序中进行分段处理,但是这样一来就需要修改所有的网络设备驱动,不太现实,所以在提前一点,在将数据递交给网络设备的入口处由软件进行分段:比如 在ip_finish_output 将报文传输给dev_queue_xmit 之前 也就是在封装二层mac 前处理分段 

 

http://www.cnhalo.net/2016/09/13/linux-tcp-gso-tso/

TSO(TCP Segmentation Offload):

  是一种利用网卡来对大数据包进行自动分段,降低CPU负载的技术。 其主要是延迟分段
GSO(Generic Segmentation Offload):

  GSO是协议栈是否推迟分段,在发送到网卡之前判断网卡是否支持TSO,如果网卡支持TSO则让网卡分段,否则协议栈分完段再交给驱动。 如果TSO开启,GSO会自动开启

  • GSO开启, TSO开启: 协议栈推迟分段,并直接传递大数据包到网卡,让网卡自动分段
  • GSO开启, TSO关闭: 协议栈推迟分段,在最后发送到网卡前才执行分段
  • GSO关闭, TSO开启: 同GSO开启, TSO开启
  • GSO关闭, TSO关闭: 不推迟分段,在tcp_sendmsg中直接发送MSS大小的数据包

驱动程序在注册网卡设备的时候默认开启GSO: NETIF_F_GSO

驱动程序会根据网卡硬件是否支持来设置TSO: NETIF_F_TSO

可以通过ethtool -K来开关GSO/TSO

#define NETIF_F_SOFT_FEATURES    (NETIF_F_GSO | NETIF_F_GRO)
int register_netdevice(struct net_device *dev)
{
    -----------------------------------------------

    /* Transfer changeable features to wanted_features and enable
     * software offloads (GSO and GRO).
     */
    dev->hw_features |= NETIF_F_SOFT_FEATURES;
    dev->features |= NETIF_F_SOFT_FEATURES;//默认开启GRO/GSO
    dev->wanted_features = dev->features & dev->hw_features;

    if (!(dev->flags & IFF_LOOPBACK)) {
        dev->hw_features |= NETIF_F_NOCACHE_COPY;
    }

    /* Make NETIF_F_HIGHDMA inheritable to VLAN devices.
     */
    dev->vlan_features |= NETIF_F_HIGHDMA;

    /* Make NETIF_F_SG inheritable to tunnel devices.
     */
    dev->hw_enc_features |= NETIF_F_SG;

    /* Make NETIF_F_SG inheritable to MPLS.
     */
    dev->mpls_features |= NETIF_F_SG;

GSO/TSO是否开启是保存在dev->features中,而设备和路由关联,当我们查询到路由后就可以把配置保存在sock中
比如在tcp_v4_connect和tcp_v4_syn_recv_sock都会调用sk_setup_caps来设置GSO/TSO配置

/* This will initiate an outgoing connection. */
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
    --------------------------------
    orig_sport = inet->inet_sport;
    orig_dport = usin->sin_port;
    fl4 = &inet->cork.fl.u.ip4;
    rt = ip_route_connect(fl4, nexthop, inet->inet_saddr,
                  RT_CONN_FLAGS(sk), sk->sk_bound_dev_if,
                  IPPROTO_TCP,
                  orig_sport, orig_dport, sk);
    --------------------------------------
//调用sk_setup_caps来设置GSO/TSO配置
    /* OK, now commit destination to socket.  */
    sk->sk_gso_type = SKB_GSO_TCPV4;
    sk_setup_caps(sk, &rt->dst);

    -----------------------------------------

    err = tcp_connect(sk);

    rt = NULL;
    if (err)

 

void sk_setup_caps(struct sock *sk, struct dst_entry *dst)
{
    /* List of features with software fallbacks. */
#define NETIF_F_GSO_SOFTWARE    (NETIF_F_TSO | NETIF_F_TSO_ECN | \
                     NETIF_F_TSO6 | NETIF_F_UFO)
    u32 max_segs = 1;

    sk_dst_set(sk, dst);
    sk->sk_route_caps = dst->dev->features;
    if (sk->sk_route_caps & NETIF_F_GSO)//软件GSO,默认开启
        sk->sk_route_caps |= NETIF_F_GSO_SOFTWARE;//开启延时gso延时选项,包括NETIF_F_TSO
    sk->sk_route_caps &= ~sk->sk_route_nocaps;
    if (sk_can_gso(sk)) {
        if (dst->header_len) {
            sk->sk_route_caps &= ~NETIF_F_GSO_MASK;
        } else {
            sk->sk_route_caps |= NETIF_F_SG | NETIF_F_HW_CSUM;// 开启gso后,设置sg和校验
            sk->sk_gso_max_size = dst->dev->gso_max_size;//GSO_MAX_SIZE=65536
            max_segs = max_t(u32, dst->dev->gso_max_segs, 1);
        }
    }
    sk->sk_gso_max_segs = max_segs;
}
//判断GSO或TSO是否开启
static inline bool sk_can_gso(const struct sock *sk)
{
    return net_gso_ok(sk->sk_route_caps, sk->sk_gso_type);
}
static inline bool net_gso_ok(netdev_features_t features, int gso_type)
{
    netdev_features_t feature = gso_type << NETIF_F_GSO_SHIFT;
    //对于tcp4, 判断NETIF_F_TSO是否被设置, 即使硬件不支持TSO,开启GSO的情况下也会被设置

    /* check flags correspondence */
    BUILD_BUG_ON(SKB_GSO_TCPV4   != (NETIF_F_TSO >> NETIF_F_GSO_SHIFT));
    BUILD_BUG_ON(SKB_GSO_UDP     != (NETIF_F_UFO >> NETIF_F_GSO_SHIFT));
    BUILD_BUG_ON(SKB_GSO_DODGY   != (NETIF_F_GSO_ROBUST >> NETIF_F_GSO_SHIFT));
    BUILD_BUG_ON(SKB_GSO_TCP_ECN != (NETIF_F_TSO_ECN >> NETIF_F_GSO_SHIFT));
    BUILD_BUG_ON(SKB_GSO_TCPV6   != (NETIF_F_TSO6 >> NETIF_F_GSO_SHIFT));
    BUILD_BUG_ON(SKB_GSO_FCOE    != (NETIF_F_FSO >> NETIF_F_GSO_SHIFT));
    BUILD_BUG_ON(SKB_GSO_GRE     != (NETIF_F_GSO_GRE >> NETIF_F_GSO_SHIFT));
    BUILD_BUG_ON(SKB_GSO_GRE_CSUM != (NETIF_F_GSO_GRE_CSUM >> NETIF_F_GSO_SHIFT));
    BUILD_BUG_ON(SKB_GSO_IPIP    != (NETIF_F_GSO_IPIP >> NETIF_F_GSO_SHIFT));
    BUILD_BUG_ON(SKB_GSO_SIT     != (NETIF_F_GSO_SIT >> NETIF_F_GSO_SHIFT));
    BUILD_BUG_ON(SKB_GSO_UDP_TUNNEL != (NETIF_F_GSO_UDP_TUNNEL >> NETIF_F_GSO_SHIFT));
    BUILD_BUG_ON(SKB_GSO_UDP_TUNNEL_CSUM != (NETIF_F_GSO_UDP_TUNNEL_CSUM >> NETIF_F_GSO_SHIFT));
    BUILD_BUG_ON(SKB_GSO_TUNNEL_REMCSUM != (NETIF_F_GSO_TUNNEL_REMCSUM >> NETIF_F_GSO_SHIFT));

    return (features & feature) == feature;
}

对紧急数据包或GSO/TSO都不开启的情况,才不会推迟发送, 默认使用当前MSS
开启GSO后,tcp_send_mss返回mss和单个skb的GSO大小,为mss的整数倍

 

int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{
------------------------------------------------------------
    /* This should be in poll */
    sk_clear_bit(SOCKWQ_ASYNC_NOSPACE, sk);

    mss_now = tcp_send_mss(sk, &size_goal, flags);/* size_goal表示GSO支持的大小,为mss的整数倍,不支持GSO时则和mss相等 */
}

 

static int tcp_send_mss(struct sock *sk, int *size_goal, int flags)
{
    int mss_now;
    mss_now = tcp_current_mss(sk);/*通过ip option,SACKs及pmtu确定当前的mss*/
*size_goal = tcp_xmit_size_goal(sk, mss_now, !(flags &MSG_OOB));
 return mss_now; 
}

 应用程序send()数据后,会在tcp_sendmsg中尝试在同一个skb,保存size_goal大小的数据,然后再通过tcp_push把这些包通过tcp_write_xmit发出去

int tcp_sendmsg(struct kiocb *iocb, struct socket *sock, struct msghdr *msg, size_t size)
{
    struct sock *sk = sock->sk;
    struct iovec *iov;
    struct tcp_sock *tp = tcp_sk(sk);
    struct sk_buff *skb;
    int iovlen, flags;
    int mss_now, size_goal;
    int err, copied;
    long timeo;

    lock_sock(sk);
    TCP_CHECK_TIMER(sk);

    flags = msg->msg_flags;
    timeo = sock_sndtimeo(sk, flags & MSG_DONTWAIT);/* 如果send_msg是阻塞操作的话,获取阻塞的时间 */

    /* Wait for a connection to finish. 
    发送用户数据应该处于ESTABLISHED状态或者是CLOSE_WAIT状态, 
    如果不在这两种状态则调用sk_stream_wait_connnect 等连接建立完成,如果超时的话就跳转到out_err**/
    if ((1 << sk->sk_state) & ~(TCPF_ESTABLISHED | TCPF_CLOSE_WAIT))
        if ((err = sk_stream_wait_connect(sk, &timeo)) != 0)
            goto out_err;

    /* This should be in poll */
    clear_bit(SOCK_ASYNC_NOSPACE, &sk->sk_socket->flags);
    /* size_goal表示GSO支持的大小,为mss的整数倍,不支持GSO时则和mss相等
 获取当前的MSS, 并将MSG_OOB清零,因为OOB带外数据不支持GSO*/
    mss_now = tcp_send_mss(sk, &size_goal, flags);/*返回值mss_now为真实mss*/

    /* Ok commence sending. */
     /* 待发数据块的块数 以及 数据起始地址*/
    iovlen = msg->msg_iovlen;
    iov = msg->msg_iov;
    copied = 0;//copied表示有多少个数据块已经从用户空间复制到内核空间

    err = -EPIPE;/* 先把错误谁-EPIPE,EPIPE表示本地已经关闭socket连接了*/
    if (sk->sk_err || (sk->sk_shutdown & SEND_SHUTDOWN))
        goto out_err;

    while (--iovlen >= 0) {/* 如果还有待拷贝的数据块,这个循环用于控制拷贝所有的用户数据块到内核空间*/
        size_t seglen = iov->iov_len;
        unsigned char __user *from = iov->iov_base;

        iov++;

        while (seglen > 0) {/*这个数据块是不是全部都拷贝完了,用于控制每一个数据块的拷贝*/
            int copy = 0;
            int max = size_goal; /*每个skb中填充的数据长度初始化为size_goal*/
            /* 从sk->sk_write_queue中取出队尾的skb,因为这个skb可能还没有被填满 
            发送队列的最末尾的一个skb, sk_write_queue指向发送队列的头结点,发送队列是一个双向环链表,所以这里是链表的尾节点 */    
            skb = tcp_write_queue_tail(sk);
            /*如果sk_send_head == NULL 表示所有发送队列上的SKB都已经发送过了,*/
            if (tcp_send_head(sk)) { /*sk->sk_send_head != NULL 如果之前还有未发送的数据*/
                if (skb->ip_summed == CHECKSUM_NONE)  /*比如路由变更,之前的不支持TSO,现在的支持了*/
                    max = mss_now; /*上一个不支持GSO的skb,继续不支持*/
                copy = max - skb->len; /*copy为每次想skb中拷贝的数据长度*/
            }
            
           /*copy<=0表示不能合并到之前skb做GSO 也就是最后一个SKB的长度已经到达SKB的最大长度了,
           说明不能再往这个SKB上添加数据了,需要分配一个新的SKB */

            if (copy <= 0) {
new_segment:
                /* Allocate new segment. If the interface is SG,
                 * allocate skb fitting to single page.
                 */
                 /* 内存不足,需要等待---->
                 --->判断sk->sk_wmem_queued 是否小于sk->sk_sndbuf, 即发送队列中段数据的总长度是否小于发送缓冲区的大小 */
                if (!sk_stream_memory_free(sk))
                    goto wait_for_sndbuf;
                /* 分配新的skb */
                skb = sk_stream_alloc_skb(sk, select_size(sk),
                        sk->sk_allocation);
                if (!skb)
                    goto wait_for_memory;

                /*
                 * Check whether we can use HW checksum.
                 */
                /*如果硬件支持checksum,则将skb->ip_summed设置为CHECKSUM_PARTIAL,表示由硬件计算校验和*/
                if (sk->sk_route_caps & NETIF_F_ALL_CSUM)
                    skb->ip_summed = CHECKSUM_PARTIAL;
                /*将skb加入sk->sk_write_queue队尾, 同时去掉skb的TCP_NAGLE_PUSH标记*/
                skb_entail(sk, skb);
                copy = size_goal;  /*这里将每次copy的大小设置为size_goal,即GSO支持的大小*/
                max = size_goal; /*对于新的SKB, 可以拷贝的数据长度就等于size_goal */
            }
            /* sk_send_head != NULL && (copy = size_goal - skb->len > 0), 表示这个SKB没有发送过,
            并且还没到size_goal那么大,所以可以往最后一个SKB上添加数据 */
            
/* 如果这个SKB剩余的空间大于这个数据块的大小,那么把要拷贝的长度置为要拷贝的大小,copy = min(copy, seglen)*/
            /* Try to append data to the end of skb. */
            if (copy > seglen)
                copy = seglen;

            /* Where to copy to? */
    /* 接下来确定拷贝到哪里去,看看是这个SKB的线性存储区还是聚合分散IO分段 */
            if (skb_tailroom(skb) > 0) { /*如果skb的线性区还有空间,则先填充skb的线性区*/
                /* We have some space in skb head. Superb! */
                if (copy > skb_tailroom(skb))
                    copy = skb_tailroom(skb); /* 这就是最终这次要拷贝的数据长度了 */
                if ((err = skb_add_data(skb, from, copy)) != 0) /*copy用户态数据到skb线性区*/
                    goto do_fault;
            } else {  /*否则 这个SKB的线性存储区已经没有空间了,那就要把数据复制到支持分散聚合I/O的页中 */
                int merge = 0;
                int i = skb_shinfo(skb)->nr_frags;/*获得这个SKB用了多少个分散的片段*/
                struct page *page = TCP_PAGE(sk);/* 获得上次用于拷贝的页面地址,sk_sndmsg_page*/
                int off = TCP_OFF(sk);/*已有数据在上一次用的页中的偏移*/

                if (skb_can_coalesce(skb, i, page, off) &&
                    off != PAGE_SIZE) {/*pfrag->page和frags[i-1]是否使用相同页,并且page_offset相同 也就是看看能不能往最后一个页中追加数据,如果可以的话merge赋值为1*/
                    /* We can extend the last page
                     * fragment. */
                    merge = 1; /*说明和之前frags中是同一个page,需要merge*/
                } else if (i == MAX_SKB_FRAGS ||
                       (!i && !(sk->sk_route_caps & NETIF_F_SG))) {
                    /* Need to add new fragment and cannot
                     * do this because interface is non-SG,
                     * or because all the page slots are
                     * busy. */ /*如果网络设备是不只是SG的或者分页片段已经达到上限了,那就不能再往这个SKB中添加数据了,而要分配新的SKB*/
                     /*如果设备不支持SG,或者非线性区frags已经达到最大,则创建新的skb分段*/
                    tcp_mark_push(tp, skb); /*标记push flag*/
                    goto new_segment;
                } else if (page) { /* 最后一个页的数据已经满了 */
                    if (off == PAGE_SIZE) {
                        put_page(page); /*增加page引用计数*/
                        TCP_PAGE(sk) = page = NULL;
                        off = 0;
                    }
                } else  {/* 最后一种情况,不用分配新的SKB,但是最后一个页也不能添加数据,所以要新开一个页,从这个页的起始处开始写数据,所以off要设为0 */
                    off = 0;
                }
                if (copy > PAGE_SIZE - off)
                    copy = PAGE_SIZE - off;//看看这个页还有多少剩余空间

                if (!sk_wmem_schedule(sk, copy))
                    goto wait_for_memory;

                if (!page) {/*如果page = NULL, 一般是新开了一个SKB或者聚合分散IO的最后一个页已经用完了,那么要开辟一个新的页 */
                    /* Allocate new cache page. */
                    if (!(page = sk_stream_alloc_page(sk)))
                        goto wait_for_memory;
                }
                /* 终于分配好了内存,可以开始往页上复制数据了 */
                /* Time to copy data. We are close to
                 * the end! */
                err = skb_copy_to_page(sk, from, skb, page, off, copy); /*拷贝数据到page中*/
                if (err) {
                    /* If this page was new, give it to the
                     * socket so it does not get leaked.
                     *//* 如果拷贝失败了,要记录下sk_sndmsg_page = page, sk_sndmsg_off = 0,用以记录下来以备释放或者下一次拷贝时使用 */
                    if (!TCP_PAGE(sk)) {
                        TCP_PAGE(sk) = page;
                        TCP_OFF(sk) = 0;
                    }
                    goto do_error;
                }

                /* Update the skb. */
                if (merge) { /*pfrag和frags[i - 1]是相同的----如果是在原来SKB的最后一个页中添加数据的话,需要更新这个页面的实际使用长度 */
                    skb_shinfo(skb)->frags[i - 1].size += copy;
                } else {/*如果是将数据拷贝到一个新的页中*/
                    skb_fill_page_desc(skb, i, page, off, copy);
                    if (TCP_PAGE(sk)) {/* 如果sk_sndmsg_page != NULL, 表示用的是上次分配的页面,需要增加这个页的引用计数*/
                        get_page(page);
                    } else if (off + copy < PAGE_SIZE) { /* 否则sk_sndmsg_page == NULL,说明用的是最近新分配的页,并且这个页还没有用完*/
                        get_page(page);
                        TCP_PAGE(sk) = page;/*还需要修改sk_sndmsg_page为这个页,表示下次还可以接着用这个页*/
                    }
                }

                TCP_OFF(sk) = off + copy;/* 完成了一次数据拷贝 */
            }

            if (!copied)
                TCP_SKB_CB(skb)->flags &= ~TCPCB_FLAG_PSH;

            tp->write_seq += copy;/* 更新发送队列中的最后一个序列号write_seq */
            TCP_SKB_CB(skb)->end_seq += copy;/* 更新这个SKB的最后序列号,因为我们把往这个SKB中添加了新的数据 */
            skb_shinfo(skb)->gso_segs = 0; /*清零tso分段数,让tcp_write_xmit去计算*/

            from += copy;
            copied += copy;
            if ((seglen -= copy) == 0 && iovlen == 0)/*如果用户复制全部完了,那就跳到out,跳出两层while循环*/
                goto out;
            /* 还有数据没copy,并且没有达到最大可拷贝的大小(注意这里max之前被赋值为size_goal,即GSO支持的大小), 尝试往该skb继续添加数据*/
            if (skb->len < max || (flags & MSG_OOB))//如果是带外数据,也继续复制数据
                continue;
            /*下面的逻辑就是:还有数据没copy,但是当前skb已经满了,所以可以发送了(但不是一定要发送)*/
            if (forced_push(tp)) { /*超过最大窗口的一半没有设置push了*/
                tcp_mark_push(tp, skb); /*设置push标记,更新pushed_seq*/
                __tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH); /*调用tcp_write_xmit马上发送*/
            } else if (skb == tcp_send_head(sk)) /*第一个包,直接发送*/
                tcp_push_one(sk, mss_now);
            continue; /*说明发送队列前面还有skb等待发送,且距离之前push的包还不是非常久*/

wait_for_sndbuf:
            set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);
wait_for_memory:
            if (copied)/*先把copied的发出去再等内存*/
                tcp_push(sk, flags & ~MSG_MORE, mss_now, TCP_NAGLE_PUSH);
            /*阻塞等待内存*/
            if ((err = sk_stream_wait_memory(sk, &timeo)) != 0)
                goto do_error;

            mss_now = tcp_send_mss(sk, &size_goal, flags);
        }
    }
/*正常情况下,数据都复制完了,如果有复制数据,那就把这些数据都发送出去*/
out:
    if (copied) /*所有数据都放到发送队列中了,调用tcp_push发送*/
        tcp_push(sk, flags, mss_now, tp->nonagle);
    TCP_CHECK_TIMER(sk);
    release_sock(sk);
    return copied;/*返回从用户空间拷贝了多少数据到内核空间*/

do_fault:
    if (!skb->len) {/*如果SKB的长度为0,说明这个SKB是新分配的*/
        tcp_unlink_write_queue(skb, sk);
        /* It is the one place in all of TCP, except connection
         * reset, where we can be unlinking the send_head.
         */
        tcp_check_send_head(sk, skb);
        sk_wmem_free_skb(sk, skb); /* 释放这个SKB */
    }

do_error:
    if (copied)
        goto out;
out_err:/* 完全没有复制任何数据,那只能返回错误码给用户了 */
    err = sk_stream_error(sk, flags, err);
    TCP_CHECK_TIMER(sk);
    release_sock(sk);
    return err;
}
tcp_sendmsg()做了以下事情:

1. 如果使用了TCP Fast Open,则会在发送SYN包的同时携带上数据。

2. 如果连接尚未建立好,不处于ESTABLISHED或者CLOSE_WAIT状态,

    那么进程进行睡眠,等待三次握手的完成。

3. 获取当前的MSS、网络设备支持的最大数据长度size_goal。

    如果支持GSO,size_goal会是MSS的整数倍。

4. 遍历用户层的数据块数组:

    4.1 获取发送队列的最后一个skb,如果是尚未发送的,且长度尚未达到size_goal,

           那么可以往此skb继续追加数据。

    4.2 否则需要申请一个新的skb来装载数据。

           4.2.1 如果发送队列的总大小sk_wmem_queued大于等于发送缓存的上限sk_sndbuf,

                     或者发送缓存中尚未发送的数据量超过了用户的设置值:

                     设置同步发送时发送缓存不够的标志。

                     如果此时已有数据复制到发送队列了,就尝试立即发送。

                     等待发送缓存,直到sock有发送缓存可写事件唤醒进程,或者等待超时。

           4.2.2 申请一个skb,其线性数据区的大小为:

                     通过select_size()得到的线性数据区中TCP负荷的大小 + 最大的协议头长度。

                     如果申请skb失败了,或者虽然申请skb成功,但是从系统层面判断此次申请不合法,

                     等待可用内存,等待时间为2~202ms之间的一个随机数。

           4.2.3 如果以上两步成功了,就更新skb的TCP控制块字段,把skb加入到sock发送队列的尾部,

                     增加发送队列的大小,减小预分配缓存的大小。

    4.3 接下来就是拷贝消息头中的数据到skb中了。

           如果skb的线性数据区还有剩余空间,就复制数据到线性数据区中,同时计算校验和。

    4.4 如果skb的线性数据区已经用完了,那么就使用分页区:

           4.4.1 检查分页是否有可用空间,如果没有就申请新的page。如果申请失败,说明系统内存不足。

                     之后会设置TCP内存压力标志,减小发送缓冲区的上限,睡眠等待内存。

           4.4.2 判断能否往最后一个分页追加数据。不能追加时,检查分页数是否达到了上限、

                     或网卡不支持分散聚合。如果是的话,就为此skb设置PSH标志。

                     然后跳转到4.2处申请新的skb,来继续填装数据。

           4.4.3 从系统层面判断此次分页发送缓存的申请是否合法。

           4.4.4 拷贝用户空间的数据到skb的分页中,同时计算校验和。

                     更新skb的长度字段,更新sock的发送队列大小和预分配缓存。

           4.4.5 如果把数据追加到最后一个分页了,更新最后一个分页的数据大小。否则初始化新的分页。

    4.5 拷贝成功后更新:送队列的最后一个序号、skb的结束序号、已经拷贝到发送队列的数据量。

    4.6 尽可能的将发送队列中的skb发送出去。
————————————————
转载https://blog.csdn.net/zhangskd/article/details/48207553

  最终会调用tcp_push发送skb,而tcp_push又会调用tcp_write_xmit。tcp_sendmsg已经把数据按照GSO最大的size,放到一个个的skb中, 最终调用tcp_write_xmit发送这些GSO包。

  tcp_write_xmit会检查当前的拥塞窗口,还有nagle测试,tsq检查来决定是否能发送整个或者部分的skb,

   如果只能发送一部分,则需要调用tso_fragment做切分。最后通过tcp_transmit_skb发送, 如果发送窗口没有达到限制,skb中存放的数据将达到GSO最大值。

static int tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,
              int push_one, gfp_t gfp)
{
    struct tcp_sock *tp = tcp_sk(sk);
    struct sk_buff *skb;
    unsigned int tso_segs, sent_pkts;
    int cwnd_quota;
    int result;

    sent_pkts = 0;

    if (!push_one) {
        /* Do MTU probing. */
        result = tcp_mtu_probe(sk);
        if (!result) {
            return 0;
        } else if (result > 0) {
            sent_pkts = 1;
        }
    }
    /*遍历发送队列*/
    while ((skb = tcp_send_head(sk))) {
        unsigned int limit;

        tso_segs = tcp_init_tso_segs(sk, skb, mss_now); /*skb->len/mss,重新设置tcp_gso_segs,因为在tcp_sendmsg中被清零了*/
        BUG_ON(!tso_segs);

        cwnd_quota = tcp_cwnd_test(tp, skb);
        if (!cwnd_quota)
            break;

        if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now)))
            break;

        if (tso_segs == 1) {  /*tso_segs=1表示无需tso分段*/
            /* 根据nagle算法,计算是否需要推迟发送数据 */
            if (unlikely(!tcp_nagle_test(tp, skb, mss_now,
                             (tcp_skb_is_last(sk, skb) ? /*last skb就直接发送*/
                              nonagle : TCP_NAGLE_PUSH))))
                break;
        } else {/*有多个tso分段*/
            if (!push_one /*push所有skb*/
                && tcp_tso_should_defer(sk, skb))/*/如果发送窗口剩余不多,并且预计下一个ack将很快到来(意味着可用窗口会增加),则推迟发送*/
                break;
        }
        /*下面的逻辑是:不用推迟发送,马上发送的情况*/
        limit = mss_now;
/*由于tso_segs被设置为skb->len/mss_now,所以开启gso时一定大于1*/
        if (tso_segs > 1 && !tcp_urg_mode(tp)) /*tso分段大于1且非urg模式*/
            limit = tcp_mss_split_point(sk, skb, mss_now, cwnd_quota);/*返回当前skb中可以发送的数据大小,通过mss和cwnd*/
        /* 当skb的长度大于限制时,需要调用tso_fragment分片,如果分段失败则暂不发送 */
        if (skb->len > limit &&
            unlikely(tso_fragment(sk, skb, limit, mss_now))) /*/按limit切割成多个skb*/
            break;

        TCP_SKB_CB(skb)->when = tcp_time_stamp;
        /*发送,如果包被qdisc丢了,则退出循环,不继续发送了*/
        if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp)))
            break;

        /* Advance the send_head.  This one is sent out.
         * This call will increment packets_out.
         */
         /*更新sk_send_head和packets_out*/
        tcp_event_new_data_sent(sk, skb);

        tcp_minshall_update(tp, mss_now, skb);
        sent_pkts++;

        if (push_one)
            break;
    }

    if (likely(sent_pkts)) {
        tcp_cwnd_validate(sk);
        return 0;
    }
    return !tp->packets_out && tcp_send_head(sk);
}

 

   其中tcp_init_tso_segs会设置skb的gso信息后文分析。我们看到tcp_write_xmit 会调用tso_fragment进行“tcp分段”。

  而分段的条件是skb->len > limit。这里的关键就是limit的值,我们看到在tso_segs > 1时,也就是开启gso的时候,limit的值是由tcp_mss_split_point得到的,

  也就是min(skb->len, window),即发送窗口允许的最大值。在没有开启gso时limit就是当前的mss。

 

 

客户端初始化

int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
...
    //设置GSO类型为TCPV4,该类型值会体现在每一个skb中,底层在
    //分段时需要根据该类型区分L4协议是哪个,以做不同的处理
    sk->sk_gso_type = SKB_GSO_TCPV4;
    //见下面
    sk_setup_caps(sk, &rt->u.dst);
...
}

 

服务器端初始化

struct sock *tcp_v4_syn_recv_sock(struct sock *sk, struct sk_buff *skb,
                  struct request_sock *req,
                  struct dst_entry *dst)
{
...
    //同上
    newsk->sk_gso_type = SKB_GSO_TCPV4;
    sk_setup_caps(newsk, dst);
...
}

 

sk_setup_caps()

设备和路由是相关的,L4协议会先查路由,所以设备的能力最终会体现在路由缓存中,sk_setup_caps()就是根据路由缓存中的设备能力初始化sk_route_caps字段。

enum {
    SKB_GSO_TCPV4 = 1 << 0,
    SKB_GSO_UDP = 1 << 1,
    /* This indicates the skb is from an untrusted source. */
    SKB_GSO_DODGY = 1 << 2,
    /* This indicates the tcp segment has CWR set. */
    SKB_GSO_TCP_ECN = 1 << 3,
    SKB_GSO_TCPV6 = 1 << 4,
};
 
#define NETIF_F_GSO_SHIFT    16
#define NETIF_F_GSO_MASK    0xffff0000
#define NETIF_F_TSO        (SKB_GSO_TCPV4 << NETIF_F_GSO_SHIFT)
#define NETIF_F_UFO        (SKB_GSO_UDP << NETIF_F_GSO_SHIFT)
#define NETIF_F_TSO_ECN        (SKB_GSO_TCP_ECN << NETIF_F_GSO_SHIFT)
#define NETIF_F_TSO6        (SKB_GSO_TCPV6 << NETIF_F_GSO_SHIFT)
 
#define NETIF_F_GSO_SOFTWARE    (NETIF_F_TSO | NETIF_F_TSO_ECN | NETIF_F_TSO6)
 
void sk_setup_caps(struct sock *sk, struct dst_entry *dst)
{
    __sk_dst_set(sk, dst);
    //初始值来源于网络设备中的features字段
    sk->sk_route_caps = dst->dev->features;
    //如果支持GSO,那么路由能力中的TSO标记也会设定,因为对于L4协议来讲,
    //延迟分段具体是用软件还是硬件来实现自己并不关心
    if (sk->sk_route_caps & NETIF_F_GSO)
        sk->sk_route_caps |= NETIF_F_GSO_SOFTWARE;
    //支持GSO时,sk_can_gso()返回非0。还需要对一些特殊场景判断是否真的可以使用GSO
    if (sk_can_gso(sk)) {
        //只有使用IPSec时,dst->header_len才不为0,这种情况下不能使用TSO特性
        if (dst->header_len)
            sk->sk_route_caps &= ~NETIF_F_GSO_MASK;
        else
            //支持GSO时,必须支持SG IO和校验功能,这是因为分段时需要单独设置每个
            //分段的校验和,这些工作L4是没有办法提前做的。此外,如果不支持SG IO,
            //那么延迟分段将失去意义,因为这时L4必须要保证skb中数据只保存在线性
            //区域,这就不可避免的在发送路径中必须做相应的数据拷贝操作
            sk->sk_route_caps |= NETIF_F_SG | NETIF_F_HW_CSUM;
    }
}

 

posted @ 2020-08-07 21:06  codestacklinuxer  阅读(1017)  评论(0编辑  收藏  举报