linux核心學習筆記------ip報文的分片
阿新 • • 發佈:2019-02-10
對網路比較熟悉的童鞋都知道,當傳送的ip報文長度超出了最大的傳輸單位MTU,且允許分片的情況下,就會對ip報文進行分片。在上層要傳送資料時就會呼叫dst_output,dst_output就會呼叫ip_output,而ip_output就會呼叫ip_finish_output,在ip_finish_output把資料傳送出去之前就會判斷該報文是否進行分片。
從原始碼中可以看出,當報文的長度大於mtu,gso的長度不為0就會呼叫ip_fragment進行分片。否則就會呼叫ip_finish_output2把資料傳送出去。static int ip_finish_output(struct sk_buff *skb) { 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分片目前有兩種分片方式:1、快速分片;2、慢速分片。在快速分片中,將資料分割成片段已經由傳輸層完成,三層只需將這寫片段組成ip分片;而慢速分片則需要完成全部的工作,即對一個完整的ip資料報根據mtu值迴圈進行分片,直至完成。整個分片工作都在ip_fragment中完成。
快速分片和慢速分片主要通過skb_has_frags這個來判斷,也就是判斷該資料的第一個skb中的frag_list是否為空,如果為空就是需要進行慢速分片,否則傳輸層已經為快速分片做好了準備。上面的程式碼大部分都有註釋,需要注意一種情況int ip_fragment(struct sk_buff *skb, int (*output)(struct sk_buff *)) { ....... struct rtable *rt = skb_rtable(skb); int err = 0; dev = rt->u.dst.dev; ...... /* * 如果待分片IP資料包禁止分片,則呼叫 * icmp_send()向傳送方傳送一個原因為需要 * 分片而設定了不分片標誌的目的不可達 * ICMP報文,並丟棄報文,即設定IP狀態 * 為分片失敗,釋放skb,返回訊息過長 * 錯誤碼。 */ if (unlikely((iph->frag_off & htons(IP_DF)) && !skb->local_df)) { IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGFAILS); icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED, htonl(ip_skb_dst_mtu(skb))); kfree_skb(skb); return -EMSGSIZE; } hlen = iph->ihl * 4; mtu = dst_mtu(&rt->u.dst) - hlen; /* Size of data space */ /* * 在分片之前先給IP資料包的控制塊設定 * IPSKB_FRAG_COMPLETE標誌,標識完成分片。 */ IPCB(skb)->flags |= IPSKB_FRAG_COMPLETE; if (skb_has_frags(skb)) { /* * 獲得此IP資料包第一個分片長度,包括SG型別 * 聚合分散I/O資料區中的資料。 */ int first_len = skb_pagelen(skb); if (first_len - hlen > mtu || ((first_len - hlen) & 7) || (iph->frag_off & htons(IP_MF|IP_OFFSET)) || skb_cloned(skb)) goto slow_path; skb_walk_frags(skb, frag) { if (frag->len > mtu || ((frag->len & 7) && frag->next) || skb_headroom(frag) < hlen) goto slow_path; if (skb_shared(frag)) goto slow_path; if (skb->sk) { frag->sk = skb->sk; frag->destructor = sock_wfree; truesizes += frag->truesize; } ...... frag = skb_shinfo(skb)->frag_list; skb_frag_list_init(skb); ...... for (;;) { if (frag) { /* * 設定後一個分片skb中指向三層和四層首部 * 的指標。 */ skb_reset_transport_header(frag); __skb_push(frag, hlen); skb_reset_network_header(frag); /* * 將當前分片的IP首部複製給後一個分片, * 並修改後一個分片IP首部的總長度欄位。 */ memcpy(skb_network_header(frag), iph, hlen); iph = ip_hdr(frag); iph->tot_len = htons(frag->len); /* * 根據當前分片的skb填充後一個分片 * skb中的引數。 */ ip_copy_metadata(frag, skb); /* * 如果是在處理第一個分片,則呼叫ip_options_fragment() * 將第二個分片skb中無需複製到每個分片的IP選項都 * 填充為IPOPT_NOOP,此後所有的分片選項部分都簡單 * 地複製上一個的即可。 */ 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); err = output(skb); skb = frag; frag = skb->next; skb->next = NULL;
1、要進行快速分片還需要對傳輸層傳遞的所有的skb進行判斷:
- 有分片長度大於mtu
- 除最後一個分片外還有分片長度未與8位元組對其
- ip首部中的MF或片偏移不為0,說明不是一個完整的ip報文
- 此skb被克隆
當不能進行快速分片時就會轉到慢速分片,慢速分片其實就需要對skb資料進行復制,而快速分片就不需要此操作。
slow_path:
/*
* 獲取待分片的IP資料包的資料長度,此處減去hlen是
* 為二層首部留出空間。
*/
left = skb->len - hlen; /* Space per frame */
/*
* 獲取IP資料包中資料區指標
*/
ptr = raw + hlen; /* Where to start from */
/* for bridged IP traffic encapsulated inside f.e. a vlan header,
* we need to make room for the encapsulating header
*/
/*
* 如果是橋轉發基於VLAN的IP資料包,則需
* 獲得VLAN首部長度,在後面分配skb
* 緩衝區時留下相應的空間,同時還需
* 修改MTU值。
*/
pad = nf_bridge_pad(skb);
/*
* 獲得IP首部中的片偏移值,即每個分片
* 起始處在原始資料包中位置,該值是
* 13位的,因此要乘8.
*/
offset = (ntohs(iph->frag_off) & IP_OFFSET) << 3;
/*
* 取MF位值,MF值除最後一個分片外
* 都應該置為1,表示該分片之後還
* 有分片。
*/
not_last_frag = iph->frag_off & htons(IP_MF);
/*
* 迴圈對left長度的資料進行分片,為
* 每一個分片建立一個新的SKB。
*/
while (left > 0) {
len = left;
/* IF: it doesn't fit, use 'mtu' - the data space left */
/*
* 如果剩餘資料的長度大於MTU,則以MTU為
* 分片長度進行分片;否則就以剩餘資料
* 的長度作為分片長度,顯然後一種情況
* 只會出現在最後一個分片。
*/
if (len > mtu)
len = mtu;
/* IF: we are not sending upto and including the packet end
then align the next start on an eight byte boundary */
/*
* 除非是最後一個分節,否則分片不包括IP
* 首部的資料部分,需8位元組對齊。
*/
if (len < left) {
len &= ~7;
}
/*
* Allocate buffer.
*/
/*
* 為分片分配一個SKB,其長度為分片長、
* IP首部長,以及二層首部長之和。
*/
if ((skb2 = alloc_skb(len+hlen+ll_rs, GFP_ATOMIC)) == NULL) {
NETDEBUG(KERN_INFO "IP: frag: no memory for new fragment!\n");
err = -ENOMEM;
goto fail;
}
......
/*
* 複製分片資料,並更新原始資料包剩餘未分片資料量。
* 此處呼叫了skb_copy_bits(),是因為skb中的資料儲存有多種
* 可能性,而skb_copy_bits可以處理這些細節。
*/
if (skb_copy_bits(skb, ptr, skb_transport_header(skb2), len))
/*
* 設定分片的片偏移欄位,對於第一個分片,
* 該值即原始IP資料包的片偏移欄位值。
*/
iph = ip_hdr(skb2);
iph->frag_off = htons((offset >> 3));
if (offset == 0)
ip_options_fragment(skb);
* 如果不是最後一個分節,則設定IP首部中
* 標識欄位的MF位。
*/
if (left > 0 || not_last_frag)
iph->frag_off |= htons(IP_MF);
/*
* 更新後一個分節在整個原始資料包中的偏移量,
* 以及後一個分片在當前被分片資料包中的偏移量。
* 這兩個偏移量是有區別的,因為一個數據包在
* 傳輸過程中可能被多次分片,因此當前被分片
* 資料包也由可能是另外一個數據包的分片。
*/
ptr += len;
offset += len;
/*
* Put this fragment into the sending queue.
*/
/*
* 設定分片IP首部中總長度欄位。
*/
iph->tot_len = htons(len + hlen);
......
上述程式碼也是有註釋的,只提示兩點:
1、分片的片偏移
分段偏移用於指明分段起始點相對報文起始點的偏移,長度為13位,以8個位組為單位。若MTU=1500時,一個大小為3000位元組的資料經過該介面,會被分為端傳輸:
第一段長度為1480+20,第二段為1480,第三段為40,那麼第一段分段的偏移為0,第二段為1480/8,第三段為185+185,所以在原始碼中需要乘以8,而在設定ip首部片偏移時又除以8的原因
2、ip選項的處理要注意,有的ip選項需要體現在所有的分片中,而有的不需要。