首先來看一下分片好的幀的一些概念:
1 第一個幀的offset位非0並且MF位爲1
2 所有的在第一個幀和最後一個幀之間的幀都擁有長度大於0的域
3 最後一個幀MF位爲0 並且offset位非0。(這樣就能判斷是否是最後一個幀了).
這裏要注意在linux中,ip頭的frag_off域包含了 rfcip頭的定義中的nf,df,以及offset域,因此我們每次需要按位與來取得相應的域的值,看下面
ip_local_deliver的代碼片段就清楚了:
- ///取出mf位和offset域,從而決定是否要組包。
- if (ip_hdr(skb)->frag_off & htons(IP_MF | IP_OFFSET)) {
- if (ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER))
- return 0;
- }
而fragmentation/defragmentation 子系統的初始化是通過ipfrag_init來實現了,而它是被inet_init來調用的。它主要做的是註冊sys文件系統節點
,並開啓一個定時器,以及初始化一些相關的變量.這個函數的初始化以及相關的數據結構的詳細介紹,我們會在後面的組包小節中介紹。現在我們先
來看切片的處理。
相對於組包,切片邏輯什麼的都比較簡單。切片的主要函數是ip_fragment.它的輸入包包括下面幾種:
1 要被轉發的包(沒有切片的)。
2 要被轉發的包(已經被路由器或者源主機切片了的).
3 被本地函數所創建的buffer,簡而言之也就是本地所要傳輸的數據包(還未加包頭),但是需要被切片的。
而ip_fragment所必須處理下面幾種情況:
1 一大塊數據需要被分割爲更小的部分。
2 一堆數據片段(我的上篇blog有介紹,也就是ip_append_data已經切好的數據包,或者tcp已經切好的數據包)不需要再被切片。
上面的兩種情況其實就是看高層(4層)協議有沒有做切片工作(按照PMTU)了。如果已經被切片(其實也算不上切片(4層不能處理ip頭),只能說i4層爲
了ip層更好的處理數據包,從而幫ip層做了一部分工作),則ip層所做的很簡單,就是給每個包加上ip頭就可以了。
切片分爲兩種類型,一種是fast (或者說 efficient)切片,這種也就是4層已經切好片,這裏只需要加上ip頭就可以了,一種是slow切片,也就是需
要現在切片。
下來來看切片的主要任務:
1 將數據包切片爲MTU大小(通過ptmu).
2 初始化每一個fragment的ip 頭。還要判斷一些option的copy位,因爲並不是每一種option都要放在所有已切片的fragment 的ip頭中的。
3 計算ip層的校驗值。
4 通過netfilter過濾。
5 update 一些kernel 域以及snmp 統計值。
接下來來看ip_fragment的具體實現:
- int ip_fragment(struct sk_buff *skb, int (*output)(struct sk_buff*))
第一個參數skb表示將要被切片的ip包,第二個參數是一個傳輸切片的輸出函數(切片完畢後就交給這個函數處理)。比如ip_finish_output2類似的。
這個函數我們來分段看,首先來看它進行切片前的一些準備工作:
- ///先是取出了一些下面將要使用的變量。
- struct iphdr *iph;
- int raw = 0;
- int ptr;
- struct net_device *dev;
- struct sk_buff *skb2;
- unsigned int mtu, hlen, left, len, ll_rs, pad;
- int offset;
- __be16 not_last_frag;
- ///路由表
- struct rtable *rt = skb->rtable;
- int err = 0;
- ///網絡設備
- dev = rt->u.dst.dev;
- ///ip頭
- iph = ip_hdr(skb);
- ///判斷DF位,我們知道如果df位被設置了話就表示不要被切片,這時ip_fragment將會發送一個icmp豹紋返回到源主機。這裏主要是爲forward數據所判斷。
- 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;
- }
- ///得到ip頭的長度
- hlen = iph->ihl * 4;
- ///得到mtu的大小。這裏要注意,他的大小減去了hlen,也就是ip頭的大小。
- mtu = dst_mtu(&rt->u.dst) - hlen; /* Size of data space */
- IPCB(skb)->flags |= IPSKB_FRAG_COMPLETE;
不管是slow還是fast 被切片的任何一個幀如果傳輸失敗,ip_fragment都會立即返回一個錯誤給4層,並且緊跟着的幀也不會再被傳輸,然後將處理方法交給4層去做。
接下來我們來看fast 切片。 一般用fast切片的都是經由4層的ip_append_data和ip_push_pending函數(udp)將數據包已經切片好的,或者是tcp層已經切片好的數據包,纔會用fast切片.
這裏要主要幾個問題:
1 每一個切片的大小都不能超過PMTU。
2 只有最後一個切片纔會有3層的整個數據包的大小。
3 每一個切片都必須有足夠的大小來允許2層加上自己的頭。
我們先看一下skb_pagelen這個函數(下面的處理會用到),這個函數用來得到當前skb的len,首先我們要知道(我前面的blog有介紹)在sk_write_queue
的sk_buff隊列中,每一個sk_buff的len = x(也就是麼一個第一個切片的包的l4 payload的長度) + S1 (這裏表示所有的frags域的數據的總大小,也
就是data_len的長度)。可以先看下面的圖:
很容易一目瞭然。
- static inline int skb_pagelen(const struct sk_buff *skb)
- {
- int i, len = 0;
- ///我們知道如果設備支持S/G IO的話,nr_frags會包含一些L4 payload,因此我們需要先遍歷nr_frags.然後加入它的長度。
- for (i = (int)skb_shinfo(skb)->nr_frags - 1; i >= 0; i--)
- len += skb_shinfo(skb)->frags[i].size;
- ///最後加上skb_headlen,而skb_headlen = skb->len - skb->data_len;因此這裏就會返回這個數據包的len。
- return len + skb_headlen(skb);
- }
- ///通過上一篇blog我們知道,如果4層將數據包分片了,那麼就會把這些數據包放到skb的frag_list鏈表中,因此我們這裏首先先判斷frag_list鏈表是否爲空,爲空的話我們將會進行slow 切片。
- if (skb_shinfo(skb)->frag_list) {
- struct sk_buff *frag;
- ///取得第一個數據報的len.我們知道當sk_write_queue隊列被flush後,除了第一個切好包的另外的包都會加入到frag_list中,而這裏我們我們需要得到的第一個包(也就是本身這個sk_buff)的長度。
- int first_len = skb_pagelen(skb);
- int truesizes = 0;
- ///接下來的判斷都是爲了確定我們能進行fast切片。切片不能被共享,這是因爲在fast path 中,我們需要加給每個切片不同的ip頭(而並不會複製每個切片)。因此在fast path中是不可接受的。而在slow path中,就算有共享也無所謂,因爲他會複製每一個切片,使用一個新的buff。
- ///判斷第一個包長度是否符合一些限制(包括mtu,mf位等一些限制).如果第一個數據報的len沒有包含mtu的大小這裏之所以要把第一個切好片的數據包單獨拿出來檢測,是因爲一些域是第一個包所獨有的(比如IP_MF要爲1)。這裏由於這個mtu是不包括hlen的mtu,因此我們需要減去一個hlen。
- if (first_len - hlen > mtu ||
- ((first_len - hlen) & 7) ||
- (iph->frag_off & htons(IP_MF|IP_OFFSET)) ||
- skb_cloned(skb))
- goto slow_path;
- ///遍歷剩餘的frag。
- for (frag = skb_shinfo(skb)->frag_list; frag; frag = frag->next) {
- /* Correct geometry. */
- ///判斷每個幀的mtu,以及相關的東西,如果不符合條件則要進行slow path,基本和上面的第一個skb的判斷類似。
- if (frag->len > mtu ||
- ((frag->len & 7) && frag->next) ||
- skb_headroom(frag) < hlen)
- goto slow_path;
- ///判斷是否共享。
- /* Partially cloned skb? */
- if (skb_shared(frag))
- goto slow_path;
- BUG_ON(frag->sk);
- ///進行socket的一些操作。
- if (skb->sk) {
- sock_hold(skb->sk);
- frag->sk = skb->sk;
- frag->destructor = sock_wfree;
- truesizes += frag->truesize;
- }
- }
- ///通過上面的檢測,都通過了,因此我們可以進行fast path切片了。
- ///先是設置一些將要處理的變量的值。
- err = 0;
- offset = 0;
- ///取得frag_list列表
- frag = skb_shinfo(skb)->frag_list;
- skb_shinfo(skb)->frag_list = NULL;
- ///得到數據(不包括頭)的大小。
- skb->data_len = first_len - skb_headlen(skb);
- skb->truesize -= truesizes;
- ///得到
- skb->len = first_len;
- iph->tot_len = htons(first_len);
- ///設置mf位
- iph->frag_off = htons(IP_MF);
- ///執行校驗
- ip_send_check(iph);
- for (;;) {
- ///開始進行發送。
- if (frag) {
- ///設置校驗位
- frag->ip_summed = CHECKSUM_NONE;
- ///設置相應的頭部。
- skb_reset_transport_header(frag);
- __skb_push(frag, hlen);
- skb_reset_network_header(frag);
- ///複製ip頭。
- memcpy(skb_network_header(frag), iph, hlen);
- ///修改每個切片的ip頭的一些屬性。
- iph = ip_hdr(frag);
- iph->tot_len = htons(frag->len);
- ///將當前skb的一些屬性付給將要傳遞的切片好的幀。
- ip_copy_metadata(frag, skb);
- if (offset == 0)
- ///處理ip_option
- 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);
- if (!err)
- IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGCREATES);
- if (err || !frag)
- break;
- ///處理鏈表中下一個buf。
- skb = frag;
- frag = skb->next;
- skb->next = NULL;
- }
- if (err == 0) {
- IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGOKS);
- return 0;
- }
- ///釋放內存。
- while (frag) {
- skb = frag->next;
- kfree_skb(frag);
- frag = skb;
- }
- IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGFAILS);
- return err;
- }
再接下來我們來看slow fragmentation:
- ///切片開始的位移
- left = skb->len - hlen; /* Space per frame */
- ///而ptr就是切片開始的指針。
- 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
- */
- ///處理橋接的相關操作。
- pad = nf_bridge_pad(skb);
- ll_rs = LL_RESERVED_SPACE_EXTRA(rt->u.dst.dev, pad);
- mtu -= pad;
- ///其實也就是取出取出ip offset域。
- offset = (ntohs(iph->frag_off) & IP_OFFSET) << 3;
- ///not_last_frag,顧名思義,其實也就是表明這個幀是否是最後一個切片。
- not_last_frag = iph->frag_off & htons(IP_MF);
- ///開始爲循環處理,每一個切片創建一個skb buffer。
- while (left > 0) {
- len = left;
- ///如果len大於mtu,我們設置當前的將要切片的數據大小爲mtu。
- if (len > mtu)
- len = mtu;
- ///長度也必須位對齊。
- if (len < left) {
- len &= ~7;
- }
- ///malloc一個新的buff。它的大小包括ip payload,ip head,以及L2 head.
- 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;
- }
- ///調用ip_copy_metadata複製一些相同的值的域。
- ip_copy_metadata(skb2, skb);
- ///進行skb的相關操作。爲了加上ip頭。
- skb_reserve(skb2, ll_rs);
- skb_put(skb2, len + hlen);
- skb_reset_network_header(skb2);
- skb2->transport_header = skb2->network_header + hlen;
- ///將每一個分片的ip包都關聯到源包的socket上。
- if (skb->sk)
- skb_set_owner_w(skb2, skb->sk);
- ///開始填充新的ip包的數據。
- ///先拷貝包頭。
- skb_copy_from_linear_data(skb, skb_network_header(skb2), hlen);
- ///拷貝數據部分,這個函數實現的比較複雜。
- if (skb_copy_bits(skb, ptr, skb_transport_header(skb2), len))
- BUG();
- left -= len;
- ///填充相應的ip頭。
- iph = ip_hdr(skb2);
- iph->frag_off = htons((offset >> 3));
- ///第一個包,因此進行ip_option處理。
- if (offset == 0)
- ip_options_fragment(skb);
- ///不是最後一個包,因此設置mf位。
- if (left > 0 || not_last_frag)
- iph->frag_off |= htons(IP_MF);
- ///移動指針以及更改位移大小。
- ptr += len;
- offset += len;
- ///update包頭的大小。
- iph->tot_len = htons(len + hlen);
- ///重新計算校驗。
- ip_send_check(iph);
- //最終輸出。
- err = output(skb2);
- if (err)
- goto fail;
- IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGCREATES);
- }
- kfree_skb(skb);
- IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGOKS);
- return err;
接下來來看ip組包的實現。首先要知道每一個切片(屬於同一個源包的)的ip包 id都是相同的。
首先來看相應的數據結構。在內核中,每一個ip包(切片好的)都是一個struct ipq鏈表。而不同的數據包(這裏指不是屬於同一個源包的數據包)都保
存在一個hash表中。也就是ip4_frags這個變量:
- static struct inet_frags ip4_frags;
- #define INETFRAGS_HASHSZ 64
- struct inet_frags {
- struct hlist_head hash[INETFRAGS_HASHSZ];
- rwlock_t lock;
- ///隨機值,它被用在計算hash值上面,下面會介紹到,過一段時間,內核就會更新這個值。
- u32 rnd;
- int qsize;
- int secret_interval;
- struct timer_list secret_timer;
- ///hash函數
- unsigned int (*hashfn)(struct inet_frag_queue *);
- void (*constructor)(struct inet_frag_queue *q,
- void *arg);
- void (*destructor)(struct inet_frag_queue *);
- void (*skb_free)(struct sk_buff *);
- int (*match)(struct inet_frag_queue *q,
- void *arg);
- void (*frag_expire)(unsigned long data);
- };
- struct ipq {
- struct inet_frag_queue q;
- u32 user;
- ///都是ip頭相關的一些域。
- __be32 saddr;
- __be32 daddr;
- __be16 id;
- u8 protocol;
- int iif;
- unsigned int rid;
- struct inet_peer *peer;
- };
- struct inet_frag_queue {
- struct hlist_node list;
- struct netns_frags *net;
- ///基於LRU算法,主要用在GC上。
- struct list_head lru_list; /* lru list member */
- spinlock_t lock;
- atomic_t refcnt;
- ///屬於同一個源的數據包的定時器,當定時器到期,切片還沒到達,此時就會drop掉所有的數據切片。
- struct timer_list timer; /* when will this queue expire? */
- ///保存有所有的切片鏈表(從屬於同一個ip包)
- struct sk_buff *fragments; /* list of received fragments */
- ktime_t stamp;
- int len; /* total length of orig datagram */
- ///表示從源ip包已經接收的字節數。
- int meat;
- ///這個域主要可以設置爲下面的3種值。
- __u8 last_in; /* first/last segment arrived? */
- ///完成,第一個幀以及最後一個幀。
- #define INET_FRAG_COMPLETE 4
- #define INET_FRAG_FIRST_IN 2
- #define INET_FRAG_LAST_IN 1
- };
看下面的圖就一目瞭然了:
首先來看組包要解決的一些問題:
1 fragment必須存儲在內存中,知道他們全部都被網絡子系統處理。纔會釋放,因此內存會是個巨大的浪費。
2 這裏雖然使用了hash表,可是假設惡意攻擊者得到散列算法並且僞造數據包來嘗試着降低一些hash表中的元素的比重,從而使執行變得緩慢。這裏linux使用一個定時器通過製造的隨機數來使hash值的生成不可預測。
這個定時器的初始化是通過ipfrag_init(它會初始化上面提到的ip4_frags全局變量)調用inet_frags_init進行的:
- void inet_frags_init(struct inet_frags *f)
- {
- int i;
- for (i = 0; i < INETFRAGS_HASHSZ; i++)
- INIT_HLIST_HEAD(&f->hash[i]);
- rwlock_init(&f->lock);
- f->rnd = (u32) ((num_physpages ^ (num_physpages>>7)) ^
- (jiffies ^ (jiffies >> 6)));
- ///安裝定時器,當定時器到期就會調用inet_frag_secret_rebuild方法。
- setup_timer(&f->secret_timer, inet_frag_secret_rebuild,
- (unsigned long)f);
- f->secret_timer.expires = jiffies + f->secret_interval;
- add_timer(&f->secret_timer);
- }
- static void inet_frag_secret_rebuild(unsigned long dummy)
- {
- ................................................
- write_lock(&f->lock);
- ///得到隨機值
- get_random_bytes(&f->rnd, sizeof(u32));
- ///然後通過這個隨機值重新計算整個hash表的hash值。
- for (i = 0; i < INETFRAGS_HASHSZ; i++) {
- struct inet_frag_queue *q;
- struct hlist_node *p, *n;
- hlist_for_each_entry_safe(q, p, n, &f->hash[i], list) {
- unsigned int hval = f->hashfn(q);
- if (hval != i) {
- hlist_del(&q->list);
- /* Relink to new hash chain. */
- hlist_add_head(&q->list, &f->hash[hval]);
- }
- }
- }
- ..............................................
- }
3 ip協議是不可靠的,因此切片有可能被丟失。內核處理這個,是使用了一個定時器(每個數據包(也就是這個切片從屬於的那個數據包)).當定時器到期,而切片沒有到達,就會丟棄這個包。
4 由於ip協議是無連接的,因此當高層決定重傳數據包的時候,組包時有可能會出現多個重複分片的情況。這是因爲ip包是由4個域來判斷的,源和目的地址,包id以及4層的協議類型。而最主要的是包id。可是包id只有16位,因此一個gigabit網卡幾乎在半秒時間就能用完這個id一次。而第二次重傳的數據包有可能走的和第一個第一次時不同的路徑,因此內核必須每個切片都要檢測和前面接受的切片的重疊情況的發生。
先來看ip_defrag用到的幾個函數:
inet_frag_create:
創建一個新的ipq實例
ip_evitor:
remove掉所有的未完成的數據包。它每次都會update一個LRU鏈表。每次都會把一個新的ipq數據結構加到ipq_lru_list的結尾。
ip_find:
發現切片所從屬的數據包的切片鏈表。
ip_frag_queue:
排隊一個給定的切片刀一個切片列表。這個經常和上一個方法一起使用。
ip_frag_reasm:
當所有的切片都到達後,build一個ip數據包。
ip_frag_destroy:
remove掉傳進來的ipq數據結構。包括和他有聯繫的所有的ip切片。
ipq_put:
將引用計數減一,如果爲0,則直接調用ip_frag_destroy.
- static inline void inet_frag_put(struct inet_frag_queue *q, struct inet_frags *f)
- {
- if (atomic_dec_and_test(&q->refcnt))
- inet_frag_destroy(q, f, NULL);
- }
ipq_kill
主要用在gc上,標記一個ipq數據結構可以被remove,由於一些幀沒有按時到達。
接下來來看ip_defrag的實現。
- int ip_defrag(struct sk_buff *skb, u32 user)
- {
- struct ipq *qp;
- struct net *net;
- net = skb->dev ? dev_net(skb->dev) : dev_net(skb->dst->dev);
- IP_INC_STATS_BH(net, IPSTATS_MIB_REASMREQDS);
- ///如果內存不夠,則依據lru算法進行清理。
- if (atomic_read(&net->ipv4.frags.mem) > net->ipv4.frags.high_thresh)
- ip_evictor(net);
- ///查找相應的iqp,如果不存在則會新創建一個(這些都在ip_find裏面實現)
- if ((qp = ip_find(net, ip_hdr(skb), user)) != NULL) {
- int ret;
- spin_lock(&qp->q.lock);
- ///排隊進隊列。
- ret = ip_frag_queue(qp, skb);
- spin_unlock(&qp->q.lock);
- ipq_put(qp);
- return ret;
- }
- IP_INC_STATS_BH(net, IPSTATS_MIB_REASMFAILS);
- kfree_skb(skb);
- return -ENOMEM;
- }
我們可以看到這裏最重要的一個函數其實是ip_frag_queue,它主要任務是:
1 發現輸入幀在源包的位置。
2 基於blog剛開始所描述的,判斷是否是最後一個切片。
3 插入切片到切片列表(從屬於相同的ip包)
4 update 垃圾回收所用到的ipq的一些相關域。
5 校驗l4層的校驗值(在硬件計算).
- ///其中qp是源ip包的所有切片鏈表,而skb是將要加進來切片。
- static int ip_frag_queue(struct ipq *qp, struct sk_buff *skb)
- {
- .............................
- /// INET_FRAG_COMPLETE表示所有的切片包都已經抵達,這個時侯就不需要再組包了,因此這裏就是校驗函數有沒有被錯誤的調用。
- if (qp->q.last_in & INET_FRAG_COMPLETE)
- goto err;
- .................................................
- ///將offset 8字節對齊、
- offset = ntohs(ip_hdr(skb)->frag_off);
- flags = offset & ~IP_OFFSET;
- offset &= IP_OFFSET;
- offset <<= 3; /* offset is in 8-byte chunks */
- ihl = ip_hdrlen(skb);
- ///計算這個新的切片包的結束位置。
- end = offset + skb->len - ihl;
- err = -EINVAL;
- ///MF沒有設置,表明這個幀是最後一個幀。進入相關處理。
- if ((flags & IP_MF) == 0) {
- /* If we already have some bits beyond end
- * or have different end, the segment is corrrupted.
- */
- ///設置相應的len位置,以及last_in域。
- if (end < qp->q.len ||
- ((qp->q.last_in & INET_FRAG_LAST_IN) && end != qp->q.len))
- goto err;
- qp->q.last_in |= INET_FRAG_LAST_IN;
- qp->q.len = end;
- } else {
- ///除了最後一個切片,每個切片都必須是8字節的倍數。
- if (end&7) {
- ///不是8字節的倍數,kernel截斷這個切片。此時就需要l4層的校驗重新計算,因此設置ip_summed爲 CHECKSUM_NONE
- end &= ~7;
- if (skb->ip_summed != CHECKSUM_UNNECESSARY)
- skb->ip_summed = CHECKSUM_NONE;
- }
- if (end > qp->q.len) {
- ///數據包太大,並且是最後一個包,則表明這個數據包出錯,因此drop它。
- /* Some bits beyond end -> corruption. */
- if (qp->q.last_in & INET_FRAG_LAST_IN)
- goto err;
- qp->q.len = end;
- }
- }
- ///ip頭不能被切片,因此end肯定會大於offset。
- if (end == offset)
- goto err;
- err = -ENOMEM;
- ///remove掉ip頭。
- if (pskb_pull(skb, ihl) == NULL)
- goto err;
- ///trim掉一些padding,然後重新計算checksum。
- err = pskb_trim_rcsum(skb, end - offset);
- if (err)
- goto err;
- ///接下來遍歷並將切片(爲了找出當前將要插入的切片的位置),是以offset爲基準。這裏要合租要FRAG_CB宏是用來提取sk_buff->cb域。
- prev = NULL;
- for (next = qp->q.fragments; next != NULL; next = next->next) {
- if (FRAG_CB(next)->offset >= offset)
- break; /* bingo! */
- prev = next;
- }
- ///當prev!=NULL時,說明這個切片要插入到列表當中。
- if (prev) {
- ///計算有沒有重疊。
- int i = (FRAG_CB(prev)->offset + prev->len) - offset;
- ///大於0.證明有重疊,因此進行相關處理
- if (i > 0) {
- ///將重疊部分用新的切片覆蓋。
- offset += i;
- err = -EINVAL;
- if (end <= offset)
- goto err;
- err = -ENOMEM;
- //移動i個位置。
- if (!pskb_pull(skb, i))
- goto err;
- ///需要重新計算L4的校驗。
- if (skb->ip_summed != CHECKSUM_UNNECESSARY)
- skb->ip_summed = CHECKSUM_NONE;
- }
- }
- err = -ENOMEM;
- ///
- while (next && FRAG_CB(next)->offset < end) {
- ///和上面的判斷很類似,也是先計算重疊數。這裏要注意重疊分爲兩種情況:1;一個或多個切片被新的切片完全覆蓋。2;被部分覆蓋,因此這裏我們需要分兩種情況進行處理。
- int i = end - FRAG_CB(next)->offset; /* overlap is 'i' bytes */
- if (i < next->len) {
- ///被部分覆蓋的情況。將新的切片offset移動i字節,然後remove掉老的切片中的i個字節。
- /* Eat head of the next overlapped fragment
- * and leave the loop. The next ones cannot overlap.
- */
- if (!pskb_pull(next, i))
- goto err;
- FRAG_CB(next)->offset += i;
- ///將接收到的源數據報的大小減去i,也就是remove掉不完全覆蓋的那一部分。
- qp->q.meat -= i;
- ///重新計算l4層的校驗。
- if (next->ip_summed != CHECKSUM_UNNECESSARY)
- next->ip_summed = CHECKSUM_NONE;
- break;
- } else {
- ///老的切片完全被新的切片覆蓋,此時只需要remove掉老的切片就可以了。
- struct sk_buff *free_it = next;
- next = next->next;
- if (prev)
- prev->next = next;
- else
- qp->q.fragments = next;
- ///將qp的接受字節數更新。
- qp->q.meat -= free_it->len;
- frag_kfree_skb(qp->q.net, free_it, NULL);
- }
- }
- FRAG_CB(skb)->offset = offset;
- ....................................................
- atomic_add(skb->truesize, &qp->q.net->mem);
- ///offset爲0說明是第一個切片,因此設置相應的位。
- if (offset == 0)
- qp->q.last_in |= INET_FRAG_FIRST_IN;
- if (qp->q.last_in == (INET_FRAG_FIRST_IN | INET_FRAG_LAST_IN) &&
- qp->q.meat == qp->q.len)
- ///所有條件的滿足了,就開始buildip包。
- return ip_frag_reasm(qp, prev, dev);
- write_lock(&ip4_frags.lock);
- ///從將此切片加入到lry鏈表中。
- list_move_tail(&qp->q.lru_list, &qp->q.net->lru_list);
- write_unlock(&ip4_frags.lock);
- return -EINPROGRESS;
- err:
- kfree_skb(skb);
- return err;
- }
如果網絡設備提供L4層的硬件校驗的話,輸入ip幀還會進行L4的校驗計算。當幀通過ip_frag_reasm組合好,它會進行校驗的重新計算。我們這裏通過設置skb->ip_summed到CHECKSUM_NONE,來表示需要嬌豔的標誌。
最後來看下GC。
內核爲ip切片數據包實現了兩種類型的垃圾回收。
1 系統內存使用限制。
2 組包的定時器
這裏有一個全局的ip_frag_mem變量,來表示當前被切片所佔用的內存數。每次一個新的切片被加入,這個值都會更新。而所能使用的最大內存可以在運行時改變,是通過/proc的sysctl_ipfrag_high_thresh來改變的,因此我們能看到當ip_defrag時,一開始會先判斷內存的限制:
- if (atomic_read(&net->ipv4.frags.mem) > net->ipv4.frags.high_thresh)
- ip_evictor(net);
當一個切片數據包到達後,內核會啓動一個組包定時器,他是爲了避免一個數據包占據ipq_hash太長時間,因此當定時器到期後,它就會清理掉在hash表中的相應的qp結構(也就是所有的未完成切片包).這個處理函數就是ip_expire,它的初始化是在ipfrag_init進行的。:
- static void ip_expire(unsigned long arg)
- {
- struct ipq *qp;
- struct net *net;
- ///取出相應的qp,以及net域。
- qp = container_of((struct inet_frag_queue *) arg, struct ipq, q);
- net = container_of(qp->q.net, struct net, ipv4.frags);
- spin_lock(&qp->q.lock);
- ///如果數據包已經傳輸完畢,則不進行任何處理,直接退出。
- if (qp->q.last_in & INET_FRAG_COMPLETE)
- goto out;
- ///調用ipq_kill,這個函數主要是減少qp的引用計數,並從相關鏈表(比如LRU_LIST)中移除它。
- ipq_kill(qp);
- IP_INC_STATS_BH(net, IPSTATS_MIB_REASMTIMEOUT);
- IP_INC_STATS_BH(net, IPSTATS_MIB_REASMFAILS);
- ///如果是第一個切片,則發送一個ICMP給源主機。
- if ((qp->q.last_in & INET_FRAG_FIRST_IN) && qp->q.fragments != NULL) {
- struct sk_buff *head = qp->q.fragments;
- /* Send an ICMP "Fragment Reassembly Timeout" message. */
- if ((head->dev = dev_get_by_index(net, qp->iif)) != NULL) {
- icmp_send(head, ICMP_TIME_EXCEEDED, ICMP_EXC_FRAGTIME, 0);
- dev_put(head->dev);
- }
- }
- out:
- spin_unlock(&qp->q.lock);
- ipq_put(qp);
- }