內核版本:2.6.34
在前一篇”IP協議”中對報文接收時IP層的處理進行了分析,本篇分析將針對報文發送時IP層的處理。
傳輸層處理完後,會調用ip_push_pending_frames()將報文傳遞給IP層:
ip_push_pending_frames() -> ip_local_out() -> __ip_local_out()
在ip_push_pending_frames()中,會設置第一個IP分片的報頭字段,tot_len和 check不會設置。
int ip_local_out(struct sk_buff *skb) { int err; err = __ip_local_out(skb); if (likely(err == 1)) err = dst_output(skb); return err; }
__ip_local_out():設置IP報頭字節總長度tot_len,校驗和check。
iph->tot_len = htons(skb- >len); ip_send_check(iph);
最後調用dst_output()發送數據給IP層,dst_output()實際調用skb_dst(skb)->output(skb) ,skb_dst(skb)就是skb所對應的路由項。skb_dst(skb)指向的是路由項dst_entry,它的input在收到報文時賦值 ip_local_deliver(),而output在發送報文時賦值ip_output()。
return nf_hook(PF_INET, NF_INET_LOCAL_OUT, skb, NULL, skb_dst(skb)->dev, dst_output);
在IP層的調用過程如下:
ip_output() -> ip_finish_output() - > ip_finish_output2() -> hh->hh_output()
在ip_output()中,設置了dev與協議號,從IP層往下,就是以dev驅 動數據傳輸了。
skb->dev = dev; skb->protocol = htons(ETH_P_IP);
在ip_finish_output()中,判斷如果報文過大,則先調用ip_fragment()進行 分片(後面會對這個函數進行分析),然後調用ip_finish_output2()發送。
if (skb->len > ip_skb_dst_mtu (skb) && !skb_is_gso(skb)) return ip_fragment(skb, ip_finish_output2); else return ip_finish_output2(skb);
情況一:ip_fragment()
ip_fragment()與ip_append_data()是IP層傳送報文很重要的 兩個函數,弄清它們之間的關系很重要。
ip_append_data()是上層構造向IP層傳送數據的skb使用的,它會根據MTU值對傳送 數據進行分片,後續分片鏈在第一個分片的frag_list上;如果設備支持SG,那麼同一個分片內容(當分片內容是多次輸入得到的 )不一定在一個線性空間上,後續輸入的分片內容存在分片的frags數組中。只有第一個分片才有frag_list,而每個分片都能擁 有frags。由ip_append_data()構造好的skb大致如下圖所示:
ip_fragments()字面 意思是分片,但實際上分片工作已經由ip_append_data()完成了,它只在上層分片出現問題時重新進行分片。它的主要作用還是 完成分片的後續工作。假設一個報文被分成了三份skb1, skb2, skb3,它們將獨立的傳遞到網絡上,但顯然ip_append_data()得 到的skb還不是獨立的,skb1包含了整個報文的信息,分片報文也鏈在frag_list上;而skb2, skb3則缺少IP報頭的信息,如分片 的偏移,分片的標識,校驗和等。ip_fragments()做的主要工作就是將skb拆分成能獨立發送的報文。由ip_fragments()處理後 的skb如圖所示:
兩張圖只列出了IP報頭tot_len字段的不同,其它諸如check, frag_list, frag_off等字段也是不同的。
先是對第 一個分片的更新,讓它脫離後續分片,成為獨立包。frag_list置為空,當然frag_list得保存下來(到frag)中,後續分片要從 frag_list中取出。更新skb_datalen和skb->len為第一個分片自身的值,在之前ip_append_data()處理後它是代表全部分片 的值。ip報頭的tot_len, frag_off和check分別設置。關於first_len的值,下面這張圖可以清晰的解釋(frags是支持SG的設備 可能會出現的,不支持的話,skb->data_len=0):
frag = skb_shinfo(skb)->frag_list; skb_frag_list_init(skb); skb->data_len = first_len - skb_headlen(skb); skb->truesize -= truesizes; skb->len = first_len; iph->tot_len = htons(first_len); iph->frag_off = htons(IP_MF); ip_send_check(iph);
下面是循環每個分片的代碼,中間省略了每個分片的處理,這部分單獨拿出來說明,frag是從 skb中取出的skb_shinfo(skb)->frag_list。
for (;;) { if (frag) { …… // 分片處理 if (err || !frag) break; skb = frag; frag = skb->next; skb->next = NULL; } }
對於後續分片,要生成它的IP報頭,設置好其中字段,這裡根據分片的排列設置了片偏移iph->frag_off,以及 偏移標識(前續分片打上IP_MF標簽)。ip_copy_metadata()從前一個分片中拷貝些數據,比如pkt_type, protocol, dev, priority, mark, flags等。ip_options_fragment()處理分片的IP選項部分,因為很多選項只要第一個分片有就可以了,後續分 片可以去除。
frag->ip_summed = CHECKSUM_NONE; skb_reset_transport_header(frag); __skb_push(frag, hlen); skb_reset_network_header(frag); memcpy(skb_network_header(frag), iph, hlen); iph = ip_hdr(frag); iph->tot_len = htons(frag->len); ip_copy_metadata(frag, skb); if (offset == 0) ip_options_fragment(frag); offset += skb->len - hlen; iph->frag_off = htons(offset>>3); if (frag->next != NULL) iph->frag_off |= htons(IP_MF); /* Ready, complete checksum */ ip_send_check(iph);
對於每一個分片,在處理完後,調用發送函數向下發送,這裡output就是ip_finish_output2() 。
err = output(skb);
情況二:ip_finish_output2()
調用相應發送函數發送給下一層。有關hh和neighbour 參考”ARP模塊”。
if (dst->hh) return neigh_hh_output(dst->hh, skb); else if (dst->neighbour) return dst->neighbour->output(skb);
在創建鄰居表項時neighbour->output()被賦值,比如收到arp報文 ,在arp_process() -> neigh_event_ns()中創建報文相應的鄰居表項,而neigh->ops和neigh->output根據情況賦予 不同的值。
if (dev->header_ops->cache) neigh->ops = &arp_hh_ops; else neigh->ops = &arp_generic_ops; if (neigh->nud_state&NUD_VALID) neigh->output = neigh->ops->connected_output; else neigh->output = neigh->ops->output;
鄰居表項創建後,相應的hh緩存項並沒有創建,當向鄰居表項中的 主機發送報文時,先調用neigh->output(),假設neigh->ops被賦值arp_generiv_ops,則neigh->output= neigh_resolve_output,而在neigh_resolve_output()函數中,會創建hh緩存項,其中hh->output= dev_queue_xmit()。
所以,無論哪種情況,hh->output還是neigh->output,最終都是調用dev_queue_xmit()向下層傳送報文的。這也是IP層 下傳送報文的統一方式-dev_queue_xmit()。雖然調用接口相同,但IP層下的各個協議模塊都是有設備的概念的,因此每個模塊 的設備都不相同,在每個模塊中都會更換skb->dev為下層的設備,而dev_queue_xmit()最終使用的是skb->dev特定的函數 進行發送的,這樣實現了各模塊的接口一致。
dev_queue_xmit() 發送函數
skb_needs_linearize()判斷是否要對報文 進行線性處理,如果需要,它返回1,由__skb_linearize()完成線性處理。線性處理就是將報文的所有內容放到線性地址空間, 不能有分片的存在。在發送報文時,ip_append_data()對過長的報文進行了分片frag_list,多次添加時使用了SG特性frags(如 果支持)。skb_needs_linearize()就是判斷設備能否處理ip_append_data()所做的分片工作。判斷條件很簡單:skb有分片即 frag_list,但設備不支持分片NETIF_F_FRAGLIST;skb應用了SG但設備不支持NETIF_F_SG或者是有一個分片在highmem中。最後 的線性化函數__skb_linearize()也很簡單,它調用__pskb_pull_tail(skb, skb->data_len),data_len就是非線性空間的長 度,__pskb_pull_taill會將這部分數據拷貝到skb->data,從而完成線性化。明顯看到,不支持分片的設備在做線性化處理 時會多一次數據拷貝操作。
if (skb_needs_linearize(skb, dev) && __skb_linearize(skb)) goto out_kfree_skb;
ip_summed==CHECKSUM_PARTIAL表示協議棧並沒有計算完校驗和,只計算了IP頭,偽頭等,將傳 輸層的數據部分留給了硬件進行計算。dev_can_checksum()判斷設備是否能計算校驗和,如果不能的話,則skb_checksum_help ()軟件的計算校驗和。
if (skb->ip_summed == CHECKSUM_PARTIAL) { skb_set_transport_header(skb, skb->csum_start - skb_headroom(skb)); if (!dev_can_checksum(dev, skb) && skb_checksum_help(skb)) goto out_kfree_skb; }
每個設備在創建時都會新建傳送隊列,dev->_tx。以B4401網卡創建為例,alloc_etherdev()創建的隊列_tx數為1 ,即單隊列的,dev_pick_tx()取出這個隊列dev->_tx[0] -> txq中。其它支持多隊列的網卡會根據skb- >sk_tx_queue_mapping來選擇_tx隊列。
txq = dev_pick_tx(dev, skb); q = rcu_dereference_bh(txq->qdisc);
支持queue discipline(隊列排序)會由q->enqueue和q->dequeue來 管理隊列,發送報文。支持的網卡設備則由其後的代碼來處理報文發送。B4401不支持,其q->enqueue為空。
if (q->enqueue) { rc = __dev_xmit_skb(skb, q, dev, txq); goto out; }
下面是不支持qdisc的網卡設備發送數據的代碼段:dev->falgs & IFF_UP判斷網卡是否UP狀態, netif_tx_queue_stopped()判斷傳送隊列是否在運行狀態。兩者滿足的話,調用dev_hard_start_xmit()向下傳輸報文。 dev_xmit_complete()檢查傳輸結果。
if (dev->flags & IFF_UP) { …… if (!netif_tx_queue_stopped(txq)) { rc = dev_hard_start_xmit(skb, dev, txq); if (dev_xmit_complete(rc)) { HARD_TX_UNLOCK(dev, txq); goto out; } } …… }
dev_hard_start_xmit()核心語句如下,ops->nod_start_xmit()調用設備skb->dev特定的發送操作將skb向下 傳送,緊接檢查發送值rc,更新發送狀態計數。如果此時dev指向vlan設備,則ops->ndo_start_xmit()指向 vlan_dev_hard_start_xmit(),它生成vlan報文,更換skb->dev,更新計數,再次調用dev_queue_xmit();如果此時dev指向 網卡設備(如b4401),則ops->ndo_start_xmit()指向b44_start_xmit(),它會將數據發送物理介質。
rc = ops- >ndo_start_xmit(skb, dev); if (rc == NETDEV_TX_OK) txq_trans_update(txq);
簡單總結下,在不支持QDISC的網卡上,從IP層向下的傳輸,循環的調用dev_queue_xmit() 向下層傳輸報文,直到最後真正的網卡設備將數據發送到物理介質上,完成報文的發送。其循環調用的圖示如下: