linux内核协议栈 TCP层数据发送之TSO/GSO
目錄
1?基本概念
2?TCP延遲分段判定
2.1 客戶端初始化
2.2 服務器端初始化
2.3 sk_setup_caps()
3?整體結構
4. TCP發送路徑TSO處理
4.1 tcp_sendmsg()
4.1.1?tcp_current_mss
4.2 tcp_write_xmit()
4.2.1 tcp_init_tso_segs()
4.2.2?tso_fragment()
TSO相關的內容充斥著TCP的整個發送過程,弄明白其機制對理解TCP的發送過程至關重要。
1?基本概念
我們知道,網絡設備一次能夠傳輸的最大數據量就是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)。這種技術是指盡可能的延遲分段,最好是在設備驅動程序中進行分段處理,但是這樣一來就需要修改所有的網絡設備驅動,不太現實,所以再提前一點,在將數據遞交給網絡設備的入口處由軟件進行分段(見dev_queue_xmit()),這正是Linux內核的實現方式。
注:類似的一些概念如LSO、UFO等,可以類比理解,這里不再敘述。
2?TCP延遲分段判定
對于TCP來講,無論最終延遲分段是由TSO(網絡設備)實現,還是由軟件來實現(GSO),TCP的處理都是一樣的。下面來看看TCP到底是如何判斷自己是否可以延遲分段的。
static inline int sk_can_gso(const struct sock *sk) {//實際上檢查的就是sk->sk_route_caps是否設定了sk->sk_gso_type能力標記return net_gso_ok(sk->sk_route_caps, sk->sk_gso_type); }static inline int net_gso_ok(int features, int gso_type) {int feature = gso_type << NETIF_F_GSO_SHIFT;return (features & feature) == feature; }sk_route_caps字段代表的是路由能力;sk_gso_type表示的是L4協議期望底層支持的GSO技術。這兩個字段都是在三次握手過程中設定的,客戶端和服務器端的初始化分別如下。
2.1 客戶端初始化
客戶端是在tcp_v4_connect()中完成的,相關代碼如下:
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); ... }2.2 服務器端初始化
服務器端是在收到客戶端傳來的ACK后即三次握手的最后一步,會新建一個sock并進行的初始化,相關代碼如下:
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); ... }2.3 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。還需要對一些特殊場景判斷是否真的可以使用GSOif (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;} }上述代碼中涉及到的幾個能力的含義如下表所示:
| NETIF_F_GSO | 0x0000 0800 | 如果軟件實現的GSO打開,設置該標記。在高版本內核中,該值在register_netdevice()中強制打開的 |
| NETIF_F_TSO | 0x0001 0000 | 網絡設備如果支持TSO over IP,設置該標記 |
| NETIF_F_TSO_ECN | 0x0008 0000 | 網絡設備如果支持設置了ECE標記的TSO,設置該標記 |
| NETIF_F_TSO6 | 0x0010 0000 | 網絡設備如果支持TSO over IPv6,設置該標記 |
3?整體結構
TSO的處理會影響整個數據包發送路徑,不僅僅是TCP層,下面先看一個整體的結構圖,然后分析下TCP層發送路徑上對TSO的處理,其它協議層的處理待后續補充。
注:圖片來源于:https://www.cnblogs.com/lvyilong316/p/6818231.html
如上圖所示,在TCP的發送路徑上,有如下幾個點設計TSO的處理:
4. TCP發送路徑TSO處理
4.1 tcp_sendmsg()
首先是 tcp_sendmsg(),該函數負責將用戶空間的數據封裝成一個個的skb,所以它需要要知道每個skb應該要容納多少的數據量,這是通過tcp_current_mss()設定的,代碼如下:
int tcp_sendmsg(struct kiocb *iocb, struct socket *sock, struct msghdr *msg,size_t size) { ...//tcp_current_mss()中會設置tp->xmit_size_goalmss_now = tcp_current_mss(sk, !(flags&MSG_OOB));//size_goal就是本次發送每個skb可以容納的數據量,它是mss_now的整數倍,//后面tcp_sendmsg()在組織skb時,就以size_goal為上界填充數據size_goal = tp->xmit_size_goal; ... }4.1.1?tcp_current_mss
//在"TCP選項之MSS"筆記中已經分析過該函數確定發送MSS的部分,這里重點關注tp->xmit_size_goal的部分 unsigned int tcp_current_mss(struct sock *sk, int large_allowed) {struct tcp_sock *tp = tcp_sk(sk);struct dst_entry *dst = __sk_dst_get(sk);u32 mss_now;u16 xmit_size_goal;int doing_tso = 0;mss_now = tp->mss_cache;//不考慮MSG_OOB相關,從前面的介紹中我們可以知道都是支持GSO的if (large_allowed && sk_can_gso(sk) && !tp->urg_mode)doing_tso = 1;//下面三個分支是MSS相關if (dst) {u32 mtu = dst_mtu(dst);if (mtu != inet_csk(sk)->icsk_pmtu_cookie)mss_now = tcp_sync_mss(sk, mtu);}if (tp->rx_opt.eff_sacks)mss_now -= (TCPOLEN_SACK_BASE_ALIGNED +(tp->rx_opt.eff_sacks * TCPOLEN_SACK_PERBLOCK)); #ifdef CONFIG_TCP_MD5SIGif (tp->af_specific->md5_lookup(sk, sk))mss_now -= TCPOLEN_MD5SIG_ALIGNED; #endif//xmit_size_goal初始化為MSSxmit_size_goal = mss_now;//如果支持TSO,則xmit_size_goal可以更大if (doing_tso) {//65535減去協議層的頭部,包括選項部分xmit_size_goal = (65535 -inet_csk(sk)->icsk_af_ops->net_header_len -inet_csk(sk)->icsk_ext_hdr_len -tp->tcp_header_len);//調整xmit_size_goal不能超過對端接收窗口的一半xmit_size_goal = tcp_bound_to_half_wnd(tp, xmit_size_goal);//調整xmit_size_goal為MSS的整數倍xmit_size_goal -= (xmit_size_goal % mss_now);}//將確定的xmit_size_goal記錄到TCB中tp->xmit_size_goal = xmit_size_goal;return mss_now; }/* Bound MSS / TSO packet size with the half of the window */ static int tcp_bound_to_half_wnd(struct tcp_sock *tp, int pktsize) {//max_window為當前已知接收方所擁有的最大窗口值,這里如果參數pktsize超過//了接收窗口的一半,則調整其大小最大為接收窗口的一半if (tp->max_window && pktsize > (tp->max_window >> 1))return max(tp->max_window >> 1, 68U - tp->tcp_header_len);else//其余情況不做調整return pktsize; }4.2 tcp_write_xmit()
static int tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle) { ...unsigned int tso_segs;while ((skb = tcp_send_head(sk))) { ...//用MSS初始化skb中的gso字段,返回本skb將會被分割成幾個TSO段傳輸tso_segs = tcp_init_tso_segs(sk, skb, mss_now);BUG_ON(!tso_segs); ...if (tso_segs == 1) {//Nagle算法檢測,如果已經有小數據段沒有被確認,則本次發送嘗試失敗if (unlikely(!tcp_nagle_test(tp, skb, mss_now, (tcp_skb_is_last(sk, skb) ? nonagle : TCP_NAGLE_PUSH)))) {break;}} else {if (tcp_tso_should_defer(sk, skb))break;}//limit是本次能夠發送的字節數,如果skb的大小超過了limit,那么需要將其切割limit = mss_now;if (tso_segs > 1)limit = tcp_mss_split_point(sk, skb, mss_now, cwnd_quota);if (skb->len > limit && unlikely(tso_fragment(sk, skb, limit, mss_now)))break; ...} ... }4.2.1 tcp_init_tso_segs()
該函數設置skb中的GSO相關字段信息,并且返回
/* This must be invoked the first time we consider transmitting* SKB onto the wire.*/ static int tcp_init_tso_segs(struct sock *sk, struct sk_buff *skb, unsigned int mss_now) {int tso_segs = tcp_skb_pcount(skb);//cond1: tso_segs為0表示該skb的GSO信息還沒有被初始化過//cond2: MSS發生了變化,需要重新計算GSO信息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; }/* Due to TSO, an SKB can be composed of multiple actual* packets. To keep these tracked properly, we use this.*/ static inline int tcp_skb_pcount(const struct sk_buff *skb) {//gso_segs記錄了網卡在傳輸當前skb時應該將其分割成多少個包進行return skb_shinfo(skb)->gso_segs; }/* This is valid iff tcp_skb_pcount() > 1. */ static inline int tcp_skb_mss(const struct sk_buff *skb) {//gso_size記錄了該skb應該按照多大的段被切割,即上次的MSSreturn skb_shinfo(skb)->gso_size; }//設置skb中的GSO信息,所謂GSO信息,就是指skb_shared_info中的 //gso_segs、gso_size、gso_type三個字段 static void tcp_set_skb_tso_segs(struct sock *sk, struct sk_buff *skb, unsigned int mss_now) {//如果該skb數據量不足一個MSS,或者根本就不支持GSO,那么就是一個段if (skb->len <= mss_now || !sk_can_gso(sk)) {/* Avoid the costly divide in the normal non-TSO case.*///只需設置gso_segs為1,另外兩個字段在這種情況下無意義skb_shinfo(skb)->gso_segs = 1;skb_shinfo(skb)->gso_size = 0;skb_shinfo(skb)->gso_type = 0;} else {//計算要切割的段數,就是skb->len除以MSS,結果向上取整skb_shinfo(skb)->gso_segs = DIV_ROUND_UP(skb->len, mss_now);skb_shinfo(skb)->gso_size = mss_now;//gso_type來自于TCB,該字段的初始化見上文skb_shinfo(skb)->gso_type = sk->sk_gso_type;} }4.2.2?tso_fragment()
tso_fragment() 對數據包進行分段,見《linux內核協議棧 TCP層數據發送之發送新數》。
總結
以上是生活随笔為你收集整理的linux内核协议栈 TCP层数据发送之TSO/GSO的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: d : 无法将“d”项识别为 cmdle
- 下一篇: 碰撞检测经典解决方案