概念
TSO(TCP Segmentation Offload): 是一種利用網路卡來對大資料包進行自動分段,降低CPU負載的技術。 其主要是延遲分段。
GSO(Generic Segmentation Offload): GSO是協議棧是否推遲分段,在傳送到網路卡之前判斷網路卡是否支援TSO,如果網路卡支援TSO則讓網路卡分段,否則協議棧分完段再交給驅動。 如果TSO開啟,GSO會自動開啟。
以下是TSO和GSO的組合關係:
- GSO開啟, TSO開啟: 協議棧推遲分段,並直接傳遞大資料包到網路卡,讓網路卡自動分段
- GSO開啟, TSO關閉: 協議棧推遲分段,在最後傳送到網路卡前才執行分段
- GSO關閉, TSO開啟: 同GSO開啟, TSO開啟
- GSO關閉, TSO關閉: 不推遲分段,在tcp_sendmsg中直接傳送MSS大小的資料包
開啟GSO/TSO
驅動程式在註冊網路卡裝置的時候預設開啟GSO: NETIF_F_GSO
驅動程式會根據網路卡硬體是否支援來設定TSO: NETIF_F_TSO
可以通過ethtool -K來開關GSO/TSO
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#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; ... } static int ixgbe_probe(struct pci_dev *pdev, const struct pci_device_id *ent) { ... netdev->features = NETIF_F_SG | NETIF_F_TSO | NETIF_F_TSO6 | NETIF_F_RXHASH | NETIF_F_RXCSUM | NETIF_F_HW_CSUM; register_netdev(netdev); ... } |
是否推遲分段
從上面我們知道GSO/TSO是否開啟是儲存在dev->features中,而裝置和路由關聯,當我們查詢到路由後就可以把配置儲存在sock中。
比如在tcp_v4_connect和tcp_v4_syn_recv_sock都會呼叫sk_setup_caps來設定GSO/TSO配置。
需要注意的是,只要開啟了GSO,即使硬體不支援TSO,也會設定NETIF_F_TSO,使得sk_can_gso(sk)在GSO開啟或者TSO開啟的時候都返回true
l sk_setup_caps
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#define NETIF_F_GSO_SOFTWARE (NETIF_F_TSO | NETIF_F_TSO_ECN | NETIF_F_TSO6) #define NETIF_F_TSO (SKB_GSO_TCPV4 << NETIF_F_GSO_SHIFT) void sk_setup_caps(struct sock *sk, struct dst_entry *dst) { __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; /*開啟TSO*/ if (sk_can_gso(sk)) { /*對於tcp這裡會成立*/ if (dst->header_len) { sk->sk_route_caps &= ~NETIF_F_GSO_MASK; } else { sk->sk_route_caps |= NETIF_F_SG | NETIF_F_HW_CSUM; sk->sk_gso_max_size = dst->dev->gso_max_size; /*GSO_MAX_SIZE=65536*/ } } } |
從上面可以看出,如果裝置開啟了GSO,sock都會將TSO標誌開啟,但是注意這和硬體是否開啟TSO無關,硬體的TSO取決於硬體自身特性的支援。下面看下sk_can_gso的邏輯。
l sk_can_gso
1 2 3 4 5 |
static inline int sk_can_gso(const struct sock *sk) { /*對於tcp,在tcp_v4_connect中被設定:sk->sk_gso_type = SKB_GSO_TCPV4*/ return net_gso_ok(sk->sk_route_caps, sk->sk_gso_type); } |
l net_gso_ok
1 2 3 4 5 |
static inline int net_gso_ok(int features, int gso_type) { int feature = gso_type << NETIF_F_GSO_SHIFT; return (features & feature) == feature; } |
由於對於tcp 在sk_setup_caps中sk->sk_route_caps也被設定有SKB_GSO_TCPV4,所以整個sk_can_gso成立。
GSO的資料包長度
對緊急資料包或GSO/TSO都不開啟的情況,才不會推遲傳送, 預設使用當前MSS。開啟GSO後,tcp_send_mss返回mss和單個skb的GSO大小,為mss的整數倍。
l tcp_send_mss
1 2 3 4 5 6 7 8 9 |
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; } |
l tcp_xmit_size_goal
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
static unsigned int tcp_xmit_size_goal(struct sock *sk, u32 mss_now, int large_allowed) { struct tcp_sock *tp = tcp_sk(sk); u32 xmit_size_goal, old_size_goal; xmit_size_goal = mss_now; /*這裡large_allowed表示是否是緊急資料*/ if (large_allowed && sk_can_gso(sk)) { /*如果不是緊急資料且支援GSO*/ xmit_size_goal = ((sk->sk_gso_max_size - 1) - inet_csk(sk)->icsk_af_ops->net_header_len - inet_csk(sk)->icsk_ext_hdr_len - tp->tcp_header_len);/*xmit_size_goal為gso最大分段大小減去tcp和ip頭部長度*/ xmit_size_goal = tcp_bound_to_half_wnd(tp, xmit_size_goal);/*最多達到收到的最大rwnd視窗通告的一半*/ /* We try hard to avoid divides here */ old_size_goal = tp->xmit_size_goal_segs * mss_now; if (likely(old_size_goal <= xmit_size_goal && old_size_goal + mss_now > xmit_size_goal)) { xmit_size_goal = old_size_goal; /*使用老的xmit_size*/ } else { tp->xmit_size_goal_segs = xmit_size_goal / mss_now; xmit_size_goal = tp->xmit_size_goal_segs * mss_now; /*使用新的xmit_size*/ } } return max(xmit_size_goal, mss_now); } |
l tcp_sendmsg
應用程式send()資料後,會在tcp_sendmsg中嘗試在同一個skb,儲存size_goal大小的資料,然後再通過tcp_push把這些包通過tcp_write_xmit發出去
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 |
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); /* Wait for a connection to finish. */ 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_now = tcp_send_mss(sk, &size_goal, flags);/*返回值mss_now為真實mss*/ /* Ok commence sending. */ iovlen = msg->msg_iovlen; iov = msg->msg_iov; copied = 0; err = -EPIPE; 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 = tcp_write_queue_tail(sk); if (tcp_send_head(sk)) { /*如果之前還有未傳送的資料*/ if (skb->ip_summed == CHECKSUM_NONE) /*比如路由變更,之前的不支援TSO,現在的支援了*/ max = mss_now; /*上一個不支援GSO的skb,繼續不支援*/ copy = max - skb->len; /*copy為每次想skb中拷貝的資料長度*/ } /*copy<=0表示不能合併到之前skb做GSO*/ if (copy <= 0) { new_segment: /* Allocate new segment. If the interface is SG, * allocate skb fitting to single page. */ /* 記憶體不足,需要等待 */ 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; } /* Try to append data to the end of skb. */ if (copy > seglen) copy = seglen; /* Where to copy to? */ if (skb_tailroom(skb) > 0) { /*如果skb的線性區還有空間,則先填充skb的線性區*/ /* We have some space in skb head. */ if (copy > skb_tailroom(skb)) copy = skb_tailroom(skb); if ((err = skb_add_data(skb, from, copy)) != 0) /*copy使用者態資料到skb線性區*/ goto do_fault; } else { /*否則嘗試向SG的frags中拷貝*/ int merge = 0; int i = skb_shinfo(skb)->nr_frags; struct page *page = TCP_PAGE(sk); int off = TCP_OFF(sk); if (skb_can_coalesce(skb, i, page, off) && off != PAGE_SIZE) {/*pfrag->page和frags[i-1]是否使用相同頁,並且page_offset相同*/ /* 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,或者非線性區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 off = 0; if (copy > PAGE_SIZE - off) copy = PAGE_SIZE - off; if (!sk_wmem_schedule(sk, copy)) goto wait_for_memory; if (!page) { /* Allocate new cache page. */ if (!(page = sk_stream_alloc_page(sk))) goto wait_for_memory; } 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. */ 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_shinfo(skb)->frags[i - 1].size += copy; } else { skb_fill_page_desc(skb, i, page, off, copy); if (TCP_PAGE(sk)) { get_page(page); } else if (off + copy < PAGE_SIZE) { get_page(page); TCP_PAGE(sk) = page; } } TCP_OFF(sk) = off + copy; } if (!copied) TCP_SKB_CB(skb)->flags &= ~TCPCB_FLAG_PSH; tp->write_seq += copy; TCP_SKB_CB(skb)->end_seq += copy; skb_shinfo(skb)->gso_segs = 0; /*清零tso分段數,讓tcp_write_xmit去計算*/ from += copy; copied += copy; if ((seglen -= copy) == 0 && iovlen == 0) 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) { 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); } 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_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最大值。
l tcp_write_xmit
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
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。
l tcp_init_tso_segs
1 2 3 4 5 6 7 8 9 10 |
static int tcp_init_tso_segs(struct sock *sk, struct sk_buff *skb, unsigned int mss_now) { int tso_segs = tcp_skb_pcount(skb); /*skb_shinfo(skb)->gso_seg之前被初始化為0*/ if (!tso_segs || (tso_segs > 1 && tcp_skb_mss(skb) != mss_now)) { tcp_set_skb_tso_segs(sk, skb, mss_now); tso_segs = tcp_skb_pcount(skb); } return tso_segs; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
static void tcp_set_skb_tso_segs(struct sock *sk, struct sk_buff *skb, unsigned int mss_now) { /* Make sure we own this skb before messing gso_size/gso_segs */ WARN_ON_ONCE(skb_cloned(skb)); if (skb->len <= mss_now || !sk_can_gso(sk) || skb->ip_summed == CHECKSUM_NONE) {/*不支援gso的情況*/ /* Avoid the costly divide in the normal * non-TSO case. */ skb_shinfo(skb)->gso_segs = 1; skb_shinfo(skb)->gso_size = 0; skb_shinfo(skb)->gso_type = 0; } else { skb_shinfo(skb)->gso_segs = DIV_ROUND_UP(skb->len, mss_now); /*被設定為skb->len/mss_now*/ skb_shinfo(skb)->gso_size = mss_now; /*注意mss_now為真實的mss,這裡儲存以供gso分段使用*/ skb_shinfo(skb)->gso_type = sk->sk_gso_type; } } |
tcp_write_xmit最後會呼叫ip_queue_xmit傳送skb,進入ip層。
ip分片,tcp分段,GSO,TSO
之後的邏輯就是之前另一篇文章中分析的GSO邏輯了。下面我們看下整個協議棧中ip分片,tcp分段,GSO,TSO的關係。我將這個流程由下圖表示。