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;
    }
}
 
        

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM