日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 运维知识 > linux >内容正文

linux

tcp/ip 协议栈Linux源码分析一 IPv4分片报文重组分析一

發(fā)布時間:2025/4/5 linux 24 豆豆
生活随笔 收集整理的這篇文章主要介紹了 tcp/ip 协议栈Linux源码分析一 IPv4分片报文重组分析一 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

內(nèi)核版本:3.4.39

之前因工作原因接觸到了IPv4 報文重組這個話題,一直以來對這個重組流程不是很清楚,所以很多功能的實現(xiàn)都避開了分片報文的處理,一方面是因為重組比較復(fù)雜,另一方面是經(jīng)驗不多無從下手,最近幾周抽空詳細(xì)看了下內(nèi)核源碼關(guān)于IPv4重組的流程,這里簡要說明下,有描述不對的地方還請指出。

先簡單描述下ipv4重組的流程:內(nèi)核在傳輸層(L3層)收到分片報文后在傳遞給L4(TCP/UDP)之前會將分片報文重組,重組之前有一系列的操作,首次是檢查分片報文隊列所占內(nèi)核空間是否超過閾值,超過的話就把舊的分片隊列釋放到閾值一下,然后根據(jù)分片報文的五元組(IP源地址、目的地址、協(xié)議類型、ID和user)得到一個hash值,然后去分片hash表中查找對應(yīng)的hash分片隊列,如果分片隊列不存在或者不匹配就新建一個新的,得到分片隊列指針后根據(jù)報文的偏移值將報文插入到分片隊列中合適的位置,這個過程中可能需要處理分片重疊問題。

分片隊列的結(jié)構(gòu)圖如下, ip4_frags是一個全局變量,hash是一個hash數(shù)組,里面掛著hash隊列,隊列里的元素是ipq(分片隊列),分片隊列之間通過鏈表鏈接起來,fragment是skb指針,分片報文就掛在這里。lru_list指針指向一個lru(Least Recently Used,最近最少使用)隊列,每當(dāng)分片隊列收到一個報文都會重新刷新自己在lru隊列位置(插入到尾部),這樣當(dāng)內(nèi)核分片占用空間過大的時候,直接釋放lru隊列排在前面的元素就可以了。

Linux IPv4分片隊列組織圖

?

接下來就一步步分析重組的整個流程,有點長,但是很完整,哈哈。

/** Deliver IP Packets to the higher protocol layers.* IP層傳遞給L4層(TCP/UDP)的入口函數(shù)*/ int ip_local_deliver(struct sk_buff *skb) {/** Reassemble IP fragments.*//* 如果是分片報文,就調(diào)用ip_defrag 處理分片,這個函數(shù)如果重組成功* 就返回0和重組好的報文,然后繼續(xù)往下走,最終調(diào)用ip_local_deliver_finish, 如果重組* 沒有完成或者重組失敗報文被丟棄則直接返回。*/if (ip_is_fragment(ip_hdr(skb))) {if (ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER))return 0;}return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN, skb, skb->dev, NULL,ip_local_deliver_finish); }

分片報文根據(jù)IP頭域的不同有三種,分別是第一個分片,最后一個分片以及中間的部分。

第一個分片它的MF標(biāo)志位為1并且片偏移為0,因為是第一個分片,起始偏移位置為0.

最后一個分片,MF標(biāo)志位為0并且片偏移不為0,MF為0表示沒有后續(xù)分片了。

中間的分片MF標(biāo)志位為1并且片偏移不為0.

ip_is_fragment就是判斷如果IP頭中分片標(biāo)志位MF和片偏移有一個不為0就當(dāng)作分片報文。

static inline bool ip_is_fragment(const struct iphdr *iph) {return (iph->frag_off & htons(IP_MF | IP_OFFSET)) != 0; }

ip_defrag的第二個參數(shù)這里填寫的是IP_DEFRAG_LOCAL_DELIVER,表示是由IP層重組的,因為內(nèi)核里需要對報文進(jìn)行重組的地方不止IP層,其它諸如netfilter也會重組報文,可選的值如下

/* 重組的用戶(user),定義在ip.h */ enum ip_defrag_users {IP_DEFRAG_LOCAL_DELIVER,IP_DEFRAG_CALL_RA_CHAIN,IP_DEFRAG_CONNTRACK_IN,__IP_DEFRAG_CONNTRACK_IN_END = IP_DEFRAG_CONNTRACK_IN + USHRT_MAX,IP_DEFRAG_CONNTRACK_OUT,__IP_DEFRAG_CONNTRACK_OUT_END = IP_DEFRAG_CONNTRACK_OUT + USHRT_MAX,IP_DEFRAG_CONNTRACK_BRIDGE_IN,__IP_DEFRAG_CONNTRACK_BRIDGE_IN = IP_DEFRAG_CONNTRACK_BRIDGE_IN + USHRT_MAX,IP_DEFRAG_VS_IN,IP_DEFRAG_VS_OUT,IP_DEFRAG_VS_FWD,IP_DEFRAG_AF_PACKET,IP_DEFRAG_MACVLAN, };

接下來就看下ip_defrag函數(shù),該函數(shù)是個包裹函數(shù),本身不處理分片,它接收一個分片skb緩存和user字段,然后調(diào)用具體的分片處理函數(shù)去處理,重組成功返回0和重組好的skb,沒有重組成功或者重組失敗就返回一個非零值。

/* Process an incoming IP datagram fragment. */ 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(skb)->dev);/* snmp mib 統(tǒng)計數(shù)據(jù) */IP_INC_STATS_BH(net, IPSTATS_MIB_REASMREQDS);/* Start by cleaning up the memory. *//* 首先判斷當(dāng)前分片隊列所占內(nèi)存是否超過閾值,如果超過的話* 需要主動去釋放一些分片,因為內(nèi)存有限,分片報文在重組好之前* 是一直放在內(nèi)存里,不能無限度的存放。*/if (atomic_read(&net->ipv4.frags.mem) > net->ipv4.frags.high_thresh)ip_evictor(net);/* Lookup (or create) queue header *//* 這里根據(jù)分片五元組(源地址、目的地址、IP ID,protocol, user)去查找分片隊列* ip_find函數(shù)查找成功就返回對應(yīng)的分片隊列,查找失敗就新建一個分片隊列,* 如果分配失敗的話就返回NULL;*/if ((qp = ip_find(net, ip_hdr(skb), user)) != NULL) {int ret;spin_lock(&qp->q.lock);/* 這里是分片隊列排隊的地方,報文的排隊,重組都在這里執(zhí)行,下面* 再來分析該函數(shù)。*/ret = ip_frag_queue(qp, skb);spin_unlock(&qp->q.lock);/* 這是一個包裹函數(shù),減少分片隊列的引用計數(shù),如果沒人引用該* 隊列就調(diào)用inet_frag_destroy釋放隊列所占資源。*/ipq_put(qp);return ret;}IP_INC_STATS_BH(net, IPSTATS_MIB_REASMFAILS);/* 創(chuàng)建分片隊列失敗,釋放掉skb并返回ENOMEM */kfree_skb(skb);return -ENOMEM; } EXPORT_SYMBOL(ip_defrag);

我們首先來看下ip_evictor(net)這個函數(shù),

/* Memory limiting on fragments. Evictor trashes the oldest* fragment queue until we are back under the threshold.* 分片內(nèi)存限制處理,將分片所占用空間保持到低閾值一下,* 主要調(diào)用inet_frag_evicor來處理*/ static void ip_evictor(struct net *net) {int evicted;evicted = inet_frag_evictor(&net->ipv4.frags, &ip4_frags);if (evicted)IP_ADD_STATS_BH(net, IPSTATS_MIB_REASMFAILS, evicted); }

繼續(xù)分析inet_frag_evictor函數(shù),該函數(shù)主要用來釋放分片隊列所占用空間:

int inet_frag_evictor(struct netns_frags *nf, struct inet_frags *f) {struct inet_frag_queue *q;int work, evicted = 0;/* 首先得到需要釋放的內(nèi)存空間大小,* 用當(dāng)前所占空間總額減去低閾值得到,這個值可以通過proc文件系統(tǒng)配置。*/work = atomic_read(&nf->mem) - nf->low_thresh;while (work > 0) {/* 先獲取分片哈希表的讀鎖,如果lru鏈表為空就跳出 */read_lock(&f->lock);if (list_empty(&nf->lru_list)) {read_unlock(&f->lock);break;}/* 增加分片隊列引用計數(shù),釋放分片哈希表讀鎖 */q = list_first_entry(&nf->lru_list,struct inet_frag_queue, lru_list);atomic_inc(&q->refcnt);read_unlock(&f->lock);/* 占用分片隊列鎖,如果還沒有設(shè)置frag_complete標(biāo)志位的話,* 調(diào)用inet_frag_kill去設(shè)置,該函數(shù)主要是將當(dāng)前分片隊列從分片哈希表中* 移除并且從lru鏈表中移除,這樣就不會在使用了。*/spin_lock(&q->lock);if (!(q->last_in & INET_FRAG_COMPLETE))inet_frag_kill(q, f);spin_unlock(&q->lock);/* 如果分片隊列這時無人引用的話,調(diào)用inet_frag_destroy 釋放分片緩存* 所占用空間,下面再分析該函數(shù) 。*/if (atomic_dec_and_test(&q->refcnt))inet_frag_destroy(q, f, &work);evicted++;}return evicted; } EXPORT_SYMBOL(inet_frag_evictor);

看下inet_frag_kill函數(shù),這個函數(shù)主要做些資源回收前的收尾工作:

void inet_frag_kill(struct inet_frag_queue *fq, struct inet_frags *f) {/* 停止分片隊列定時器,這個定時器用來防止長時間占用內(nèi)存 */if (del_timer(&fq->timer))atomic_dec(&fq->refcnt);/* frag_complete一般是重組完成的時候或者釋放分片隊列的時候去設(shè)置,* 這里判斷如果沒有設(shè)置的話,就設(shè)置該標(biāo)志位同時調(diào)用fq_unlink函數(shù)* 去處理鏈表移除的事情,包括哈希表和lru鏈表。*/if (!(fq->last_in & INET_FRAG_COMPLETE)) {fq_unlink(fq, f);atomic_dec(&fq->refcnt);fq->last_in |= INET_FRAG_COMPLETE;} } EXPORT_SYMBOL(inet_frag_kill);

fq_unlink的原型:

static inline void fq_unlink(struct inet_frag_queue *fq, struct inet_frags *f) {write_lock(&f->lock);/* 從哈希分片隊列中移除 */hlist_del(&fq->list);/* 從lru鏈表中移除 */list_del(&fq->lru_list);/* 減少排隊的分片隊列個數(shù) */fq->net->nqueues--;write_unlock(&f->lock); }

再來看下實際的分片隊列資源回收處理函數(shù) inet_frag_destroy,看這名字就知道

/* 釋放分片隊列所占資源 */ void inet_frag_destroy(struct inet_frag_queue *q, struct inet_frags *f,int *work) {struct sk_buff *fp;struct netns_frags *nf;/* 正常情況下刪除分片隊列前都會置上該標(biāo)志位并且分片隊列的定時器* 應(yīng)該停止,這里檢查下,有異常就告警*/WARN_ON(!(q->last_in & INET_FRAG_COMPLETE));WARN_ON(del_timer(&q->timer) != 0);/* Release all fragment data. * 先釋放所有的skb分片緩存*/fp = q->fragments;nf = q->net;while (fp) {struct sk_buff *xp = fp->next;/* 實際的釋放函數(shù) */frag_kfree_skb(nf, f, fp, work);fp = xp;}/* qsize 是分片結(jié)構(gòu)體 struct ipq的大小 */if (work)*work -= f->qsize;atomic_sub(f->qsize, &nf->mem);/* 分片隊列釋放的回調(diào)處理函數(shù)* ipv4 這個函數(shù)是 ip4_frag_free,ipfrag_init中初始化。*/if (f->destructor)f->destructor(q);/* 最后釋放分片隊列所占內(nèi)存 */kfree(q); } EXPORT_SYMBOL(inet_frag_destroy);

實際的skb釋放函數(shù)由frag_kfree_skb完成,這個函數(shù)就是釋放分片skb緩存,然后從當(dāng)前所占的內(nèi)存空減去釋放的大小

/* 釋放分片隊列的skb buffer */ static inline void frag_kfree_skb(struct netns_frags *nf, struct inet_frags *f,struct sk_buff *skb, int *work) {/* 一種情況下是分片隊列已經(jīng)重組完成,這時候需要釋放,work 指針為空 * 還有一種情況是當(dāng)內(nèi)核分片隊列所占內(nèi)存空間過大,這時候內(nèi)核需要主動* 釋放一些舊的分片隊列,這時候work指針就表示需要釋放的空間大小*/if (work)*work -= skb->truesize;/* 從分片所占用的總的內(nèi)存數(shù)量中減去當(dāng)前釋放的skb緩存大小 */atomic_sub(skb->truesize, &nf->mem);/* 如果存在私有的釋放回調(diào)函數(shù)的話,這時候調(diào)用,* ip4_frags 這個指針為空*/if (f->skb_free)f->skb_free(skb); /* 最后調(diào)用kfree_skb釋放 skb buffer */ kfree_skb(skb); }

至此,分片處理的第一步已經(jīng)完成,即保持分片所占用內(nèi)存空間不超過閾值,再往下則是真正的處理過程,包括分片隊列的查找、插入和重組。這個過程的分析放在下篇博客里。

總結(jié)

以上是生活随笔為你收集整理的tcp/ip 协议栈Linux源码分析一 IPv4分片报文重组分析一的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。