tcp/ip 协议栈Linux源码分析一 IPv4分片报文重组分析一
內(nèi)核版本:3.4.39
之前因工作原因接觸到了IPv4 報(bào)文重組這個(gè)話題,一直以來對這個(gè)重組流程不是很清楚,所以很多功能的實(shí)現(xiàn)都避開了分片報(bào)文的處理,一方面是因?yàn)橹亟M比較復(fù)雜,另一方面是經(jīng)驗(yàn)不多無從下手,最近幾周抽空詳細(xì)看了下內(nèi)核源碼關(guān)于IPv4重組的流程,這里簡要說明下,有描述不對的地方還請指出。
先簡單描述下ipv4重組的流程:內(nèi)核在傳輸層(L3層)收到分片報(bào)文后在傳遞給L4(TCP/UDP)之前會(huì)將分片報(bào)文重組,重組之前有一系列的操作,首次是檢查分片報(bào)文隊(duì)列所占內(nèi)核空間是否超過閾值,超過的話就把舊的分片隊(duì)列釋放到閾值一下,然后根據(jù)分片報(bào)文的五元組(IP源地址、目的地址、協(xié)議類型、ID和user)得到一個(gè)hash值,然后去分片hash表中查找對應(yīng)的hash分片隊(duì)列,如果分片隊(duì)列不存在或者不匹配就新建一個(gè)新的,得到分片隊(duì)列指針后根據(jù)報(bào)文的偏移值將報(bào)文插入到分片隊(duì)列中合適的位置,這個(gè)過程中可能需要處理分片重疊問題。
分片隊(duì)列的結(jié)構(gòu)圖如下, ip4_frags是一個(gè)全局變量,hash是一個(gè)hash數(shù)組,里面掛著hash隊(duì)列,隊(duì)列里的元素是ipq(分片隊(duì)列),分片隊(duì)列之間通過鏈表鏈接起來,fragment是skb指針,分片報(bào)文就掛在這里。lru_list指針指向一個(gè)lru(Least Recently Used,最近最少使用)隊(duì)列,每當(dāng)分片隊(duì)列收到一個(gè)報(bào)文都會(huì)重新刷新自己在lru隊(duì)列位置(插入到尾部),這樣當(dāng)內(nèi)核分片占用空間過大的時(shí)候,直接釋放lru隊(duì)列排在前面的元素就可以了。
Linux IPv4分片隊(duì)列組織圖?
接下來就一步步分析重組的整個(gè)流程,有點(diǎn)長,但是很完整,哈哈。
/** Deliver IP Packets to the higher protocol layers.* IP層傳遞給L4層(TCP/UDP)的入口函數(shù)*/ int ip_local_deliver(struct sk_buff *skb) {/** Reassemble IP fragments.*//* 如果是分片報(bào)文,就調(diào)用ip_defrag 處理分片,這個(gè)函數(shù)如果重組成功* 就返回0和重組好的報(bào)文,然后繼續(xù)往下走,最終調(diào)用ip_local_deliver_finish, 如果重組* 沒有完成或者重組失敗報(bào)文被丟棄則直接返回。*/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); }分片報(bào)文根據(jù)IP頭域的不同有三種,分別是第一個(gè)分片,最后一個(gè)分片以及中間的部分。
第一個(gè)分片它的MF標(biāo)志位為1并且片偏移為0,因?yàn)槭堑谝粋€(gè)分片,起始偏移位置為0.
最后一個(gè)分片,MF標(biāo)志位為0并且片偏移不為0,MF為0表示沒有后續(xù)分片了。
中間的分片MF標(biāo)志位為1并且片偏移不為0.
ip_is_fragment就是判斷如果IP頭中分片標(biāo)志位MF和片偏移有一個(gè)不為0就當(dāng)作分片報(bào)文。
static inline bool ip_is_fragment(const struct iphdr *iph) {return (iph->frag_off & htons(IP_MF | IP_OFFSET)) != 0; }ip_defrag的第二個(gè)參數(shù)這里填寫的是IP_DEFRAG_LOCAL_DELIVER,表示是由IP層重組的,因?yàn)閮?nèi)核里需要對報(bào)文進(jìn)行重組的地方不止IP層,其它諸如netfilter也會(huì)重組報(bào)文,可選的值如下
/* 重組的用戶(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ù)是個(gè)包裹函數(shù),本身不處理分片,它接收一個(gè)分片skb緩存和user字段,然后調(diào)用具體的分片處理函數(shù)去處理,重組成功返回0和重組好的skb,沒有重組成功或者重組失敗就返回一個(gè)非零值。
/* 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)計(jì)數(shù)據(jù) */IP_INC_STATS_BH(net, IPSTATS_MIB_REASMREQDS);/* Start by cleaning up the memory. *//* 首先判斷當(dāng)前分片隊(duì)列所占內(nèi)存是否超過閾值,如果超過的話* 需要主動(dòng)去釋放一些分片,因?yàn)閮?nèi)存有限,分片報(bào)文在重組好之前* 是一直放在內(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)去查找分片隊(duì)列* ip_find函數(shù)查找成功就返回對應(yīng)的分片隊(duì)列,查找失敗就新建一個(gè)分片隊(duì)列,* 如果分配失敗的話就返回NULL;*/if ((qp = ip_find(net, ip_hdr(skb), user)) != NULL) {int ret;spin_lock(&qp->q.lock);/* 這里是分片隊(duì)列排隊(duì)的地方,報(bào)文的排隊(duì),重組都在這里執(zhí)行,下面* 再來分析該函數(shù)。*/ret = ip_frag_queue(qp, skb);spin_unlock(&qp->q.lock);/* 這是一個(gè)包裹函數(shù),減少分片隊(duì)列的引用計(jì)數(shù),如果沒人引用該* 隊(duì)列就調(diào)用inet_frag_destroy釋放隊(duì)列所占資源。*/ipq_put(qp);return ret;}IP_INC_STATS_BH(net, IPSTATS_MIB_REASMFAILS);/* 創(chuàng)建分片隊(duì)列失敗,釋放掉skb并返回ENOMEM */kfree_skb(skb);return -ENOMEM; } EXPORT_SYMBOL(ip_defrag);我們首先來看下ip_evictor(net)這個(gè)函數(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ù)主要用來釋放分片隊(duì)列所占用空間:
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)前所占空間總額減去低閾值得到,這個(gè)值可以通過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;}/* 增加分片隊(duì)列引用計(jì)數(shù),釋放分片哈希表讀鎖 */q = list_first_entry(&nf->lru_list,struct inet_frag_queue, lru_list);atomic_inc(&q->refcnt);read_unlock(&f->lock);/* 占用分片隊(duì)列鎖,如果還沒有設(shè)置frag_complete標(biāo)志位的話,* 調(diào)用inet_frag_kill去設(shè)置,該函數(shù)主要是將當(dāng)前分片隊(duì)列從分片哈希表中* 移除并且從lru鏈表中移除,這樣就不會(huì)在使用了。*/spin_lock(&q->lock);if (!(q->last_in & INET_FRAG_COMPLETE))inet_frag_kill(q, f);spin_unlock(&q->lock);/* 如果分片隊(duì)列這時(shí)無人引用的話,調(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ù),這個(gè)函數(shù)主要做些資源回收前的收尾工作:
void inet_frag_kill(struct inet_frag_queue *fq, struct inet_frags *f) {/* 停止分片隊(duì)列定時(shí)器,這個(gè)定時(shí)器用來防止長時(shí)間占用內(nèi)存 */if (del_timer(&fq->timer))atomic_dec(&fq->refcnt);/* frag_complete一般是重組完成的時(shí)候或者釋放分片隊(duì)列的時(shí)候去設(shè)置,* 這里判斷如果沒有設(shè)置的話,就設(shè)置該標(biāo)志位同時(shí)調(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);/* 從哈希分片隊(duì)列中移除 */hlist_del(&fq->list);/* 從lru鏈表中移除 */list_del(&fq->lru_list);/* 減少排隊(duì)的分片隊(duì)列個(gè)數(shù) */fq->net->nqueues--;write_unlock(&f->lock); }再來看下實(shí)際的分片隊(duì)列資源回收處理函數(shù) inet_frag_destroy,看這名字就知道
/* 釋放分片隊(duì)列所占資源 */ void inet_frag_destroy(struct inet_frag_queue *q, struct inet_frags *f,int *work) {struct sk_buff *fp;struct netns_frags *nf;/* 正常情況下刪除分片隊(duì)列前都會(huì)置上該標(biāo)志位并且分片隊(duì)列的定時(shí)器* 應(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í)際的釋放函數(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);/* 分片隊(duì)列釋放的回調(diào)處理函數(shù)* ipv4 這個(gè)函數(shù)是 ip4_frag_free,ipfrag_init中初始化。*/if (f->destructor)f->destructor(q);/* 最后釋放分片隊(duì)列所占內(nèi)存 */kfree(q); } EXPORT_SYMBOL(inet_frag_destroy);實(shí)際的skb釋放函數(shù)由frag_kfree_skb完成,這個(gè)函數(shù)就是釋放分片skb緩存,然后從當(dāng)前所占的內(nèi)存空減去釋放的大小
/* 釋放分片隊(duì)列的skb buffer */ static inline void frag_kfree_skb(struct netns_frags *nf, struct inet_frags *f,struct sk_buff *skb, int *work) {/* 一種情況下是分片隊(duì)列已經(jīng)重組完成,這時(shí)候需要釋放,work 指針為空 * 還有一種情況是當(dāng)內(nèi)核分片隊(duì)列所占內(nèi)存空間過大,這時(shí)候內(nèi)核需要主動(dòng)* 釋放一些舊的分片隊(duì)列,這時(shí)候work指針就表示需要釋放的空間大小*/if (work)*work -= skb->truesize;/* 從分片所占用的總的內(nèi)存數(shù)量中減去當(dāng)前釋放的skb緩存大小 */atomic_sub(skb->truesize, &nf->mem);/* 如果存在私有的釋放回調(diào)函數(shù)的話,這時(shí)候調(diào)用,* ip4_frags 這個(gè)指針為空*/if (f->skb_free)f->skb_free(skb); /* 最后調(diào)用kfree_skb釋放 skb buffer */ kfree_skb(skb); }至此,分片處理的第一步已經(jīng)完成,即保持分片所占用內(nèi)存空間不超過閾值,再往下則是真正的處理過程,包括分片隊(duì)列的查找、插入和重組。這個(gè)過程的分析放在下篇博客里。
總結(jié)
以上是生活随笔為你收集整理的tcp/ip 协议栈Linux源码分析一 IPv4分片报文重组分析一的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 富瀚微和华为海思哪个好 海思仍占市场主要
- 下一篇: tcp/ip 协议栈Linux源码分析二