網絡設備一次能夠傳輸的最大數據量就是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; } }