歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux綜合 >> Linux內核 >> Linux內核分析 - 網絡[八補]:IP協議補充

Linux內核分析 - 網絡[八補]:IP協議補充

日期:2017/3/3 16:38:16   编辑:Linux內核

內核版本: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() 向下層傳輸報文,直到最後真正的網卡設備將數據發送到物理介質上,完成報文的發送。其循環調用的圖示如下:

Copyright © Linux教程網 All Rights Reserved