前面有講到過在函數ip_append_data中實現了對IP資料報的分區,這個講法是錯誤的,需要糾正一下,ip_append_data的主要任務只是建立發送網路資料的通訊端緩衝區(skb),它根據輸出路由查詢得到的輸出網路裝置介面的MTU,把超過MTU長度的應用資料分割開,並建立了多個skb,放入通訊端的發送緩衝隊列(sk_write_queue),但它並沒有為任何一個skb資料加上網路層首部,並且,隨後在ip_push_pending_frames函數中,又把發送緩衝隊列中的所有的skb,以一個鏈表的形式追加到第一個skb的end成員後面的struct
skb_shared_info結構體中的frag_list上,並只為第一個skb加上了網路層首部,所以,實際上,整個應用資料還只是在一個skb中,ip_append_data這樣做只是為接下來的真正的IP資料的分區作好準備。
ip_push_pending_frames在完成了skb的組裝後,把它交給了函數ip_output,ip_output又調用了函數ip_finish_output,該函數對skb的長度再次進行判斷,如果長度超過輸出裝置的mtu的值,並且符合其它分區條件,則調用ip_fragment進行資料報的分區,否則直接調用ip_finish_output2輸出到資料連結層。
IP資料的分區涉及到IP首部中的兩個欄位,即結構體struct iphdr的成員frag_off,其高三位是三個標誌位,第二位是不允許分區標誌,置該位,表示該IP資料報不允許被分區,如果發送這樣的資料報,並且資料報本身長度已經超出MTU的很制,則向發送方發一個icmp出錯報文,報文類型為目的不可達(3),代碼為需要進行分區但被設定了不允許分區的位(4);第三位如果置1,表示後面還有分區,置0表示本分區是一個完整的IP資料報的最後一個分區。frag_off的低13位表示本分區的第一個位元組在整個IP資料報中的位移量,單位是位元組數除以8,所以需要把這13位左移3位,才是真正的位移位元組數。
有了先前ip_append_data的工作,ip_fragment的分區工作相對簡單很多。struct sk_buff的成員cb在inet域被存入了結構體struct inet_skb_parm,其定義如下:
struct inet_skb_parm
{
struct ip_options opt;
unsigned char flags;
#define IPSKB_FORWARDED 1
#define IPSKB_XFRM_TUNNEL_SIZE 2
#define IPSKB_XFRM_TRANSFORMED 4
#define IPSKB_FRAG_COMPLETE 8
#define IPSKB_REROUTED 16
};
分區完成的一個IP資料報,它的每一個skb的cb->flags被置上IPSKB_FRAG_COMPLETE標誌。ip_fragment首先為frag_list列表中的每個skb的成員sk和destructor賦上跟第一個skb同樣的值,使它們成為正常的skb。然後,為每個skb從第一個skb中拷貝中繼資料和網路層首部,並設定正確的iphdr->frag_off的值,並把它們一一輸出到資料連結層。至此,網路層的資料發送工作全部完成。
被分區後的IP資料報,其每一個分區都在網路中獨立傳輸,所以,它們到達目的主機一般是不會同時的,並有可能亂序的。並且,它們在中間的傳輸路徑上有可能被組裝,也有可能被再次分區。由於網路層首部中frag_off的存在,使得重新正確組裝成為可能。下面看看IP資料分區到達目的主機後,是如何被重新組裝起來的。
協議棧收到一個IP資料報,進入網路層的第一個函數是ip_rcv,ip_rcv對資料報進行一些正確性檢查後,交給ip_rcv_finish,ip_rcv_finish查詢輸入路由後,交給dst_input,dst_input調用skb->dst->input,如果是本地接收的IP資料報,該函數即ip_local_deliver,ip_local_deliver一開始就檢查該資料報的IP首部的frag_off成員,如果發現其低13位不為0,或者高三位中的第三位被置1,則表示這是一個IP分區(第一個分區的低13位為0,但高三位中的第三位被置1,表示後面還有分區,最後一個分區標誌位置0,但低13位不為0,中間的分區,兩者都不為0)。對於IP分區,該函數調用ip_defrag把它與已經收到的IP分區重組,並等待後來的IP分區,直至形成一個完整的IP資料報。
一個完整IP資料報的全部IP分區組織存放在一個結構體struct ipq中,該結構體儲存有足夠的資訊,等最後一個分區到達後,把它們還原成一個IP資料報。下面是struct ipq的完整定義:
struct ipq {
struct hlist_node list;
struct list_head lru_list;
u32 user;
u32 saddr;
u32 daddr;
u16 id;
u8 protocol;
u8 last_in;
#define COMPLETE 4
#define FIRST_IN 2
#define LAST_IN 1
struct sk_buff *fragments;
int len;
int meat;
spinlock_t lock;
atomic_t refcnt;
struct timer_list timer;
struct timeval stamp;
int iif;
unsigned int rid;
struct inet_peer *peer;
};
user是一個標誌,用於標識該IP分區組的來源,協議棧收到的來自網路其它主機或本地環回介面的IP分區,該標識值是IP_DEFRAG_LOCAL_DELIVER,saddr,daddr,id,protocol的值都來源於IP首部,用於確定這些IP分區確實是來自唯一的一個IP資料報。因為一台主機的協議棧可能同時跟網路中多台主機在進行通訊,所以,某一時刻,協議棧一般總有多組IP分區等待被重組,也就是說會有多個struct
ipq結構的執行個體,多個struct ipq被組織在一個雜湊表ipq_hash中,當收到一個IP分區時,首先用IP首部的相應欄位計算一個哈項值,找到雜湊表ipq_hash中的一項,然後去匹配上述欄位完全符合的一個struct ipq,把分區加入到該ipq中即可。如果在雜湊表中找不到跟當前的IP分區首部完全符合的項,則需要重新建立一個struct ipq的執行個體,並加到雜湊表中。新建立的ipq都帶有一個定時器(timer成員),逾時時間預設為IP_FRAG_TIME(30秒),如果30秒後,某一個IP資料報的分區還沒有全部被收到,則這個ipq逾時,逾時處理函數被執行,逾時處理函數會刪除這個ipq,並向接收端發送一個icmp出錯報文,該報文類型為逾時(11),代碼為在資料報組裝期間存留時間為0(1)。
得到了一個ipq後,開始把收到的分區的skb放到這個ipq中,首先檢查ipq的last_in,如果它的值為COMPLETE,則表示這個分區組已經完整了,新收到的IP分區是錯誤的,直接扔掉。再檢查新收到的skb的cb->flags,因為在發送資料報進行分區時,每一個分區的flag會被置上IPSKB_FRAG_COMPLETE標誌。
接下來檢查收到的IP分區的IP_MF(frag_off的高三位中的第三位),如果為0,表示這已經是最後一個分區了,置ipq的last_in為LAST_IN,len始終被更新為當前收到的IP分區中位移最大的那個分區的位移值加上長度,最後如果正確,則為整個IP資料報的長度。然後把這個skb剝離網路層首部後,加入到fragments鏈表中,該鏈表以frag_off中的位移量為順序組織,也就是真正的IP分區的順序,如果當前收到的這個分區的位移量為0,則置last_in的值為FIRST_IN,meat始終被更新為當前收到的IP分區的總長,最後,如果正確,meat應該等於len。
IP分區添加完畢後,如果meat確定等於len了,可以考慮進行重組了,函數ip_frag_reasm完成重組,它取得fragments鏈表,把第二個skb開始的鏈表又重新放到第一個skb的end後面的struct skb_shared_info結構體的frag_list鏈表上,並重設IP首部,一個完整的IP資料報就被重組完成了。返回前,還要釋放ipq。