Linux GSO邏輯分析 ——lvyilong316 (註:kernel版本linux 2.6.32) GSO用來擴展之前的TSO,目前已經併入upstream內核。TSO只能支持tcp協議,而GSO可以支持tcpv4, tcpv6, udp等協議。在GSO之前,skb_shinfo(skb)有兩 ...
Linux GSO邏輯分析
——lvyilong316
(註:kernel版本linux 2.6.32)
GSO用來擴展之前的TSO,目前已經併入upstream內核。TSO只能支持tcp協議,而GSO可以支持tcpv4, tcpv6, udp等協議。在GSO之前,skb_shinfo(skb)有兩個成員ufo_size, tso_size,分別表示udp fragmentation offloading支持的分片長度,以及tcp segmentation offloading支持的分段長度,現在都用skb_shinfo(skb)->gso_size代替。
skb_shinfo(skb)->ufo_segs, skb_shinfo(skb)->tso_segs也被替換成了skb_shinfo(skb)->gso_segs,表示分片的個數。
gso用來delay 大包的分片,所以一直到dev_hard_start_xmit函數才會調用到。
l dev_hard_start_xmit
1 int dev_hard_start_xmit(struct sk_buff *skb, struct net_device *dev, 2 3 struct netdev_queue *txq) 4 5 { 6 7 const struct net_device_ops *ops = dev->netdev_ops; 8 9 int rc; 10 11 12 13 if (likely(!skb->next)) { 14 15 if (!list_empty(&ptype_all)) 16 17 dev_queue_xmit_nit(skb, dev); 18 19 //判斷網卡是否需要協議棧負責gso 20 21 if (netif_needs_gso(dev, skb)) { 22 23 //真正負責GSO操作的函數 24 25 if (unlikely(dev_gso_segment(skb))) 26 27 goto out_kfree_skb; 28 29 if (skb->next) 30 31 goto gso; 32 33 } 34 35 //…… 36 37 gso: 38 39 do { 40 41 //指向GSO分片後的一個skb 42 43 struct sk_buff *nskb = skb->next; 44 45 skb->next = nskb->next; 46 47 nskb->next = NULL; 48 49 if (dev->priv_flags & IFF_XMIT_DST_RELEASE) 50 51 skb_dst_drop(nskb); 52 53 //將通過GSO分片後的包逐個發出 54 55 rc = ops->ndo_start_xmit(nskb, dev); 56 57 if (unlikely(rc != NETDEV_TX_OK)) { 58 59 nskb->next = skb->next; 60 61 skb->next = nskb; 62 63 return rc; 64 65 } 66 67 txq_trans_update(txq); 68 69 if (unlikely(netif_tx_queue_stopped(txq) && skb->next)) 70 71 return NETDEV_TX_BUSY; 72 73 } while (skb->next); 74 75 76 77 skb->destructor = DEV_GSO_CB(skb)->destructor; 78 79 80 81 out_kfree_skb: 82 83 kfree_skb(skb); 84 85 return NETDEV_TX_OK; 86 87 }
那是不是所有skb在發送時都要經過GSO的邏輯呢?顯然不是,只有通過netif_needs_gso判斷才會進入GSO的邏輯,下麵我們看下netif_needs_gso是如何判斷的。
1 static inline int netif_needs_gso(struct net_device *dev, struct sk_buff *skb) 2 3 { 4 5 return skb_is_gso(skb) && 6 7 (!skb_gso_ok(skb, dev->features) || 8 9 unlikely(skb->ip_summed != CHECKSUM_PARTIAL)); 10 11 }
註意這裡最後用了一個unlikely,因為如果通過前面的判斷,說明網卡是支持GSO的,而一般網卡支持GSO也就會支持CHECKSUM_PARTIAL。進入GSO處理的第一個前提是skb_is_gso函數返回真,看下skb_is_gso的邏輯:
1 static inline int skb_is_gso(const struct sk_buff *skb) 2 3 { 4 5 return skb_shinfo(skb)->gso_size; 6 7 }
skb_is_gso的邏輯很簡單,返回skb_shinfo(skb)->gso_size,所以進入GSO處理邏輯的必要條件之一是skb_shinfo(skb)->gso_size不為0,那麼這個欄位的含義是什麼呢?gso_size表示生產GSO大包時的數據包長度,一般時mss的整數倍。下麵看skb_gso_ok,如果這個函數返回False,就可以進入GSO處理邏輯。
1 static inline int skb_gso_ok(struct sk_buff *skb, int features) 2 3 { 4 5 return net_gso_ok(features, skb_shinfo(skb)->gso_type) && 6 7 (!skb_has_frags(skb) || (features & NETIF_F_FRAGLIST)); 8 9 }
skb_shinfo(skb)->gso_type包括SKB_GSO_TCPv4, SKB_GSO_UDPv4,同時NETIF_F_XXX的標誌也增加了相應的bit,標識設備是否支持TSO, GSO, e.g.
1 NETIF_F_TSO = SKB_GSO_TCPV4 << NETIF_F_GSO_SHIFT 2 3 NETIF_F_UFO = SKB_GSO_UDPV4 << NETIF_F_GSO_SHIFT 4 5 #define NETIF_F_GSO_SHIFT 16
通過以上三個函數分析,以下三個情況需要協議棧負責GSO。
下麵看GSO的協議棧處理邏輯,入口就是dev_gso_segment。
l dev_gso_segment
協議棧的GSO邏輯是在dev_gso_segment中進行的。這個函數主要完成對skb的分片,並將分片存放在原始skb的skb->next中,這也是GSO的主要工作。
1 static int dev_gso_segment(struct sk_buff *skb) 2 3 { 4 5 struct net_device *dev = skb->dev; 6 7 struct sk_buff *segs; 8 9 int features = dev->features & ~(illegal_highdma(dev, skb) ? 10 11 NETIF_F_SG : 0); 12 13 14 15 segs = skb_gso_segment(skb, features); 16 17 18 19 /* Verifying header integrity only. */ 20 21 if (!segs) 22 23 return 0; 24 25 26 27 if (IS_ERR(segs)) 28 29 return PTR_ERR(segs); 30 31 32 33 skb->next = segs; 34 35 DEV_GSO_CB(skb)->destructor = skb->destructor; 36 37 skb->destructor = dev_gso_skb_destructor; 38 39 40 41 return 0; 42 43 }
主要分片邏輯由skb_gso_segment來處理,這裡我們主要看下析構過程,此時skb經過分片之後已經是一個skb list,通過skb->next串在一起,此時把初始的skb->destructor函數存到skb->cb中,然後把skb->destructor變更為dev_gso_skb_destructor。dev_gso_skb_destructor會把skb->next一個個通過kfree_skb釋放掉,最後調用DEV_GSO_CB(skb)->destructor,即skb初始的析構函數做最後的清理。
l skb_gso_segment
這個函數將skb分片,並返回一個skb list。如果skb不需要分片則返回NULL。
1 struct sk_buff *skb_gso_segment(struct sk_buff *skb, int features) 2 3 { 4 5 struct sk_buff *segs = ERR_PTR(-EPROTONOSUPPORT); 6 7 struct packet_type *ptype; 8 9 __be16 type = skb->protocol; 10 11 int err; 12 13 14 15 skb_reset_mac_header(skb); 16 17 skb->mac_len = skb->network_header - skb->mac_header; 18 19 __skb_pull(skb, skb->mac_len); 20 21 //如果skb->ip_summed 不是 CHECKSUM_PARTIAL,那麼報個warning,因為GSO類型的skb其ip_summed一般都是CHECKSUM_PARTIAL 22 23 if (unlikely(skb->ip_summed != CHECKSUM_PARTIAL)) { 24 25 struct net_device *dev = skb->dev; 26 27 struct ethtool_drvinfo info = {}; 28 29 WARN(……); 30 31 if (skb_header_cloned(skb) && 32 33 (err = pskb_expand_head(skb, 0, 0, GFP_ATOMIC))) 34 35 return ERR_PTR(err); 36 37 } 38 39 rcu_read_lock(); 40 41 list_for_each_entry_rcu(ptype, &ptype_base[ntohs(type) & PTYPE_HASH_MASK], list) { 42 43 if (ptype->type == type && !ptype->dev && ptype->gso_segment) { 44 45 if (unlikely(skb->ip_summed != CHECKSUM_PARTIAL)) { 46 47 // 如果ip_summed != CHECKSUM_PARTIAL,則調用上層協議的gso_send_check 48 49 err = ptype->gso_send_check(skb); 50 51 segs = ERR_PTR(err); 52 53 if (err || skb_gso_ok(skb, features)) 54 55 break; 56 57 __skb_push(skb, (skb->data - 58 59 skb_network_header(skb))); 60 61 } 62 63 //把skb->data指向network header,調用上層協議的gso_segment完成分片 64 65 segs = ptype->gso_segment(skb, features); 66 67 break; 68 69 } 70 71 } 72 73 rcu_read_unlock(); 74 75 //把skb->data再次指向mac header 76 77 __skb_push(skb, skb->data - skb_mac_header(skb)); 78 79 80 81 return segs; 82 83 }
最終追調用上層協議的gso處理函數,對於IP協議,在註冊IP的packet_type時,其gso處理函數被初始化為inet_gso_segment。下麵我們看inet_gso_segment的處理流程。
l inet_gso_segment
./net/ipv4/af_inet.c
IP層GSO操作只是提供介面給鏈路層來訪問傳輸層(TCP、UDP),因此IP層實現的介面只是根據分段數據報獲取對應的傳輸層介面,並對完成GSO分段後的IP數據報重新計算校驗和。
static struct sk_buff *inet_gso_segment(struct sk_buff *skb, int features) { struct sk_buff *segs = ERR_PTR(-EINVAL); struct iphdr *iph; const struct net_protocol *ops; int proto; int ihl; int id; unsigned int offset = 0; if (!(features & NETIF_F_V4_CSUM)) features &= ~NETIF_F_SG; //校驗待軟GSO分段的的skb,其gso_tpye是否存在其他非法值 if (unlikely(skb_shinfo(skb)->gso_type & ~(SKB_GSO_TCPV4 | SKB_GSO_UDP | SKB_GSO_DODGY | SKB_GSO_TCP_ECN | 0))) goto out; //分段數據至少大於IP首部長度 if (unlikely(!pskb_may_pull(skb, sizeof(*iph)))) goto out; //檢驗首部中的長度欄位是否有效 iph = ip_hdr(skb); ihl = iph->ihl * 4; if (ihl < sizeof(*iph)) goto out; //再次通過首部中的長度欄位檢測skb長度是否有效 if (unlikely(!pskb_may_pull(skb, ihl))) goto out; //註意:這裡已經將data偏移到了傳送層頭部了,去掉了IP頭 __skb_pull(skb, ihl); skb_reset_transport_header(skb);//設置傳輸層頭部位置 iph = ip_hdr(skb); id = ntohs(iph->id);//取出首部中的id欄位 proto = iph->protocol & (MAX_INET_PROTOS - 1);//取出IP首部的協議值,用於定位與之對應的傳輸層介面(tcp還是udp) segs = ERR_PTR(-EPROTONOSUPPORT); rcu_read_lock(); ops = rcu_dereference(inet_protos[proto]);//根據協議欄位取得上層的協議介面 if (likely(ops && ops->gso_segment)) segs = ops->gso_segment(skb, features);//調用上冊協議的GSO處理函數 rcu_read_unlock(); if (!segs || IS_ERR(segs)) goto out; //開始處理分段後的skb skb = segs; do { iph = ip_hdr(skb); if (proto == IPPROTO_UDP) {//對於UDP進行的IP分片的頭部處理邏輯 iph->id = htons(id);//所有UDP的IP分片id都相同 iph->frag_off = htons(offset >> 3);//ip頭部偏移欄位單位為8位元組 if (skb->next != NULL) iph->frag_off |= htons(IP_MF);//設置分片標識 offset += (skb->len - skb->mac_len - iph->ihl * 4); } else iph->id = htons(id++);//對於TCP報,分片後IP頭部中id加1 iph->tot_len = htons(skb->len - skb->mac_len); iph->check = 0; //計算校驗和,只是IP頭部的 iph->check = ip_fast_csum(skb_network_header(skb), iph->ihl); } while ((skb = skb->next)); out: return segs; }
這裡有個問題,UDP經過GSO分片後每個分片的IP頭部id是一樣的,這個符合IP分片的邏輯,但是為什麼TCP的GSO分片,IP頭部的id會依次加1呢?原因是: tcp建立三次握手的過程中產生合適的mss(具體的處理機制參見TCP/IP詳解P257),這個mss肯定是<=網路層的最大路徑MTU,然後tcp數據封裝成ip數據包通過網路層發送,當伺服器端傳輸層接收到tcp數據之後進行tcp重組。所以正常情況下tcp產生的ip數據包在傳輸過程中是不會發生分片的!由於GSO應該保證對外透明,所以其效果應該也和在TCP層直接分片的效果是一樣的,所以這裡對UDP的處理是IP分片邏輯,但對TCP的處理是構造新的skb邏輯。
l 小結:對於GSO
UDP:所有分片ip頭部id都相同,設置IP_MF分片標誌(除最後一片) (等同於IP分片)
TCP:分片後,每個分片IP頭部中id加1, (等同於TCP分段)
下麵分別看對於TCP和UDP調用不通的GSO處理函數。對於TCP其GSO處理函數為tcp_tso_segment。
l tcp_tso_segment
./net/ipv4/tcp.c
1 struct sk_buff *tcp_tso_segment(struct sk_buff *skb, int features) 2 3 { 4 5 struct sk_buff *segs = ERR_PTR(-EINVAL); 6 7 struct tcphdr *th; 8 9 unsigned thlen; 10 11 unsigned int seq; 12 13 __be32 delta; 14 15 unsigned int oldlen; 16 17 unsigned int mss; 18 19 //檢測報文長度至少由tcp頭部長度 20 21 if (!pskb_may_pull(skb, sizeof(*th))) 22 23 goto out; 24 25 26 27 th = tcp_hdr(skb); 28 29 thlen = th->doff * 4;//TCP頭部的長度欄位單位為4位元組 30 31 if (thlen < sizeof(*th)) 32 33 goto out; 34 35 //再次通過首部中的長度欄位檢測skb長度是否有效 36 37 if (!pskb_may_pull(skb, thlen)) 38 39 goto out; 40 41 //把tcp header移到skb header里,把skb->len存到oldlen中,此時skb->len就只有ip payload的長度(包含TCP首部) 42 43 oldlen = (u16)~skb->len; 44 45 __skb_pull(skb, thlen); //data指向tcp payload 46 47 //這裡可以看出gso_size的含義就是mss 48 49 mss = skb_shinfo(skb)->gso_size; 50 51 if (unlikely(skb->len <= mss))//如果skb長度小於mss就不需要GSO分片處理了 52 53 goto out; 54 55 if (skb_gso_ok(skb, features | NETIF_F_GSO_ROBUST)) { 56 57 /* Packet is from an untrusted source, reset gso_segs. */ 58 59 int type = skb_shinfo(skb)->gso_type; 60 61 //校驗待軟GSO分段的的skb,其gso_tpye是否存在其他非法值 62 63 if (unlikely(type & 64 65 ~(SKB_GSO_TCPV4 | 66 67 SKB_GSO_DODGY | 68 69 SKB_GSO_TCP_ECN | 70 71 SKB_GSO_TCPV6 | 72 73 0) || 74 75 !(type & (SKB_GSO_TCPV4 | SKB_GSO_TCPV6)))) 76 77 goto out; 78 79 //計算出skb按照mss的長度需要分多少片,賦值給gso_segs 80 81 skb_shinfo(skb)->gso_segs = DIV_ROUND_UP(skb->len, mss); 82 83 84 85 segs = NULL; 86 87 goto out; 88 89 } 90 91 //skb_segment是真正的分段實現,後面再分析 92 93 segs = skb_segment(skb, features); 94 95 if (IS_ERR(segs)) 96 97 goto out; 98 99 100 101 delta = htonl(oldlen + (thlen + mss)); 102 103 104 105 skb = segs; 106 107 th = tcp_hdr(skb); 108 109 seq = ntohl(th->seq); 110 111 //下麵是設置每個分片的tcp頭部信息 112 113 do { 114 115 th->fin = th->psh = 0; 116 117 //計算每個分片的校驗和 118 119 th->check = ~csum_fold((__force __wsum)((__force u32)th->check + 120 121 (__force u32)delta)); 122 123 if (skb->ip_summed != CHECKSUM_PARTIAL) 124 125 th->check =csum_fold(csum_partial(skb_transport_header(skb), 126 127 thlen, skb->csum)); 128 129 //重新初始化每個分片的序列號 130 131 seq += mss; 132 133 skb = skb->next; 134 135 th = tcp_hdr(skb); 136 137 138 139 th->seq = htonl(seq); 140 141 th->cwr = 0; 142 143 } while (skb->next); 144 145 146 147 delta = htonl(oldlen + (skb->tail - skb->transport_header) + 148 149 skb->data_len); 150 151 th->check = ~csum_fold((__force __wsum)((__force u32)th->check + 152 153 (__force u32)delta)); 154 155 if (skb->ip_summed != CHECKSUM_PARTIAL) 156 157 th->check = csum_fold(csum_partial(skb_transport_header(skb), 158 159 thlen, skb->csum)); 160 161 162 163 out: 164 165 return segs; 166 167 }
從上面可以看出,每個TCP的GSO分片是包含了TCP頭部信息的,這也符合TCP層的分段邏輯。另外註意這裡傳遞給skb_segment做分段時是不帶TCP首部的。對於UDP,其GSO處理函數為udp4_ufo_fragment。
l udp4_ufo_fragment
./net/ipv4/udp.c
1 struct sk_buff *udp4_ufo_fragment(struct sk_buff *skb, int features) 2 3 { 4 5 struct sk_buff *segs = ERR_PTR(-EINVAL); 6 7 unsigned int mss; 8 9 int offset; 10 11 __wsum csum; 12 13 14 15 mss = skb_shinfo(skb)->gso_size; 16 17 if (unlikely(skb->len <= mss)) 18 19 goto out; 20 21 22 23 if (skb_gso_ok(skb, features | NETIF_F_GSO_ROBUST)) { 24 25 /* Packet is from an untrusted source, reset gso_segs. */ 26 27 int type = skb_shinfo(skb)->gso_type; 28 29 30 31 if (unlikely(type & ~(SKB_GSO_UDP | SKB_GSO_DODGY) || 32 33 !(type & (SKB_GSO_UDP)))) 34 35 goto out; 36 37 38 39 skb_shinfo(skb)->gso_segs = DIV_ROUND_UP(skb->len, mss); 40 41 42 43 segs = NULL; 44 45 goto out; 46 47 } 48 49 50 51 /* Do software UFO. Complete and fill in the UDP checksum as HW cannot 52 53 * do checksum of UDP packets sent as multiple IP fragments. 54 55 */ 56 57 //計算udp的checksum 58 59 offset = skb->csum_start - skb_headroom(skb); 60 61 csum = skb_checksum(skb, offset, skb->len - offset, 0); 62 63 offset += skb->csum_offset; 64 65 *(__sum16 *)(skb->data + offset) = csum_fold(csum); 66 67 skb->ip_summed = CHECKSUM_NONE; 68 69 //這裡傳遞給skb_segment做分片時是沒有將UDP首部去除的 70 71 segs = skb_segment(skb, features); 72 73 out: 74 75 return segs; 76 77 }
註意這裡傳遞給skb_segment 做分片是帶有udp首部的,分片將udp首部作為普通數據切分,這也意味著對於udp的GSO分片,只有第一片有UDP首部。udp的分段其實和ip的分片沒什麼區別,只是多一個計算checksum的步驟,下麵看完成分片的關鍵函數skb_segment。
l skb_segment
/net/core/skbuff.c
1 struct sk_buff *skb_segment(struct sk_buff *skb, int features) 2 3 { 4 5 struct sk_buff *segs = NULL; 6 7 struct sk_buff *tail = NULL; 8 9 struct sk_buff *fskb = skb_shinfo(skb)->frag_list; 10 11 unsigned int mss = skb_shinfo(skb)->gso_size; 12 13 unsigned int doffset = skb->data - skb_mac_header(skb);//mac頭+ip頭+tcp頭 或mac頭+ip頭(對於UDP傳入時沒有將頭部偏移過去) 14 15 unsigned int offset = doffset; 16 17 unsigned int headroom; 18 19 unsigned int len; 20 21 int sg = features & NETIF_F_SG; 22 23 int nfrags = skb_shinfo(skb)->nr_frags; 24 25 int err = -ENOMEM; 26 27 int i = 0; 28 29 int pos; 30 31 32 33 __skb_push(skb, doffset); 34 35 headroom = skb_headroom(skb); 36 37 pos = skb_headlen(skb);//pos初始化為線性區長度 38 39 40 41 do { 42 43 struct sk_buff *nskb; 44 45 skb_frag_t *frag; 46 47 int hsize; 48 49 int size; 50 51 // offset為分片已處理的長度,len為skb->len減去直到offset的部分。開始時,offset只是mac header + ip header + tcp header的長度,len即tcp payload的長度。隨著segment增加, offset每次都增加mss長度。因此len的定義是每個segment的payload長度(最後一個segment的payload可能小於一個mss長度) 52 53 len = skb->len - offset; 54 55 if (len > mss)//len為本次要創建的新分片的長度 56 57 len = mss; 58 59 // hsize為線性區部分的payload減去offset後的大小,如果hsize小於0,那麼說明payload在skb的frags或frag_list中。隨著offset一直增長,必定會有hsize一直<0的情況開始出現,除非skb是一個完全linearize化的skb 60 61 hsize = skb_headlen(skb) - offset; 62 63 //這種情況說明線性區已經沒有tcp payload的部分,需要pull數據過來 64 65 if (hsize < 0) 66 67 hsize = 0; 68 69 //如果不支持NETIF_F_SG或者hsize大於len,那麼hsize就為len(本次新分片的長度),此時說明segment的payload還在skb 線性區中 70 71 if (hsize > len || !sg) 72 73 hsize = len; 74 75 76 77 if (!hsize && i >= nfrags) {// hsize為0,表示需要從frags數組或者frag_list鏈表中拷貝出數據,i >= nfrags說明frags數組中的數據也拷貝完了,下麵需要從frag_list鏈表中拷貝數據了 78 79 BUG_ON(fskb->len != len); 80 81 82 83 pos += len; 84 85 //frag_list的數據不用真的拷貝,只需要拷貝其skb描述符,就可以復用其數據區 86 87 nskb = skb_clone(fskb, GFP_ATOMIC);//拷貝frag_list中的skb的描述符 88 89 fskb = fskb->next;//指向frag_list的下一個skb元素 90 91 92 93 if (unlikely(!nskb)) 94