tcp/ip 協議棧Linux源碼分析一 IPv4重組分析

內核版本:3.4.39

之前因工作原因接觸到了IPv4 報文重組這個話題,一直以來對這個重組流程不是很清楚,所以很多功能的實現都避開了分片報文的處理,一方面是因爲重組比較複雜,另一方面是經驗不多無從下手,最近幾周抽空詳細看了下內核源碼關於IPv4重組的流程,這裏簡要說明下,有描述不對的地方還請指出。

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

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

Linux IPv4分片隊列組織圖

 

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

/*
 * 	Deliver IP Packets to the higher protocol layers.
 *  IP層傳遞給L4層(TCP/UDP)的入口函數
 */
int ip_local_deliver(struct sk_buff *skb)
{
	/*
	 *	Reassemble IP fragments.
	 */
    /* 如果是分片報文,就調用ip_defrag 處理分片,這個函數如果重組成功
     * 就返回0和重組好的報文,然後繼續往下走,最終調用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);
}

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

第一個分片它的MF標誌位爲1並且片偏移爲0,因爲是第一個分片,起始偏移位置爲0.

最後一個分片,MF標誌位爲0並且片偏移不爲0,MF爲0表示沒有後續分片了。

中間的分片MF標誌位爲1並且片偏移不爲0.

ip_is_fragment就是判斷如果IP頭中分片標誌位MF和片偏移有一個不爲0就當作分片報文。

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

ip_defrag的第二個參數這裏填寫的是IP_DEFRAG_LOCAL_DELIVER,表示是由IP層重組的,因爲內核裏需要對報文進行重組的地方不止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函數,該函數是個包裹函數,本身不處理分片,它接收一個分片skb緩存和user字段,然後調用具體的分片處理函數去處理,重組成功返回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 統計數據 */
	IP_INC_STATS_BH(net, IPSTATS_MIB_REASMREQDS);

	/* Start by cleaning up the memory. */
	/* 首先判斷當前分片隊列所佔內存是否超過閾值,如果超過的話
	 * 需要主動去釋放一些分片,因爲內存有限,分片報文在重組好之前
	 * 是一直放在內存裏,不能無限度的存放。
	 */
	if (atomic_read(&net->ipv4.frags.mem) > net->ipv4.frags.high_thresh)
		ip_evictor(net);

	/* Lookup (or create) queue header */
	/* 這裏根據分片五元組(源地址、目的地址、IP ID,protocol, user)去查找分片隊列
	 * ip_find函數查找成功就返回對應的分片隊列,查找失敗就新建一個分片隊列,
	 * 如果分配失敗的話就返回NULL;
	 */
	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);

		/* 這是一個包裹函數,減少分片隊列的引用計數,如果沒人引用該
         * 隊列就調用inet_frag_destroy釋放隊列所佔資源。
		 */
		ipq_put(qp);
		return ret;
	}

	IP_INC_STATS_BH(net, IPSTATS_MIB_REASMFAILS);
	/* 創建分片隊列失敗,釋放掉skb並返回ENOMEM */
	kfree_skb(skb);
	return -ENOMEM;
}
EXPORT_SYMBOL(ip_defrag);

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

/* Memory limiting on fragments.  Evictor trashes the oldest
 * fragment queue until we are back under the threshold.
 * 分片內存限制處理,將分片所佔用空間保持到低閾值一下,
 * 主要調用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);
}

繼續分析inet_frag_evictor函數,該函數主要用來釋放分片隊列所佔用空間:

int inet_frag_evictor(struct netns_frags *nf, struct inet_frags *f)
{
	struct inet_frag_queue *q;
	int work, evicted = 0;

    /* 首先得到需要釋放的內存空間大小,
     * 用當前所佔空間總額減去低閾值得到,這個值可以通過proc文件系統配置。
     */
	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;
		}

        /* 增加分片隊列引用計數,釋放分片哈希表讀鎖 */
		q = list_first_entry(&nf->lru_list,
				struct inet_frag_queue, lru_list);
		atomic_inc(&q->refcnt);
		read_unlock(&f->lock);

        /* 佔用分片隊列鎖,如果還沒有設置frag_complete標誌位的話,
         * 調用inet_frag_kill去設置,該函數主要是將當前分片隊列從分片哈希表中
         * 移除並且從lru鏈表中移除,這樣就不會在使用了。
         */
		spin_lock(&q->lock);
		if (!(q->last_in & INET_FRAG_COMPLETE))
			inet_frag_kill(q, f);
		spin_unlock(&q->lock);

        /* 如果分片隊列這時無人引用的話,調用inet_frag_destroy 釋放分片緩存
         * 所佔用空間,下面再分析該函數 。
         */
		if (atomic_dec_and_test(&q->refcnt))
			inet_frag_destroy(q, f, &work);
		evicted++;
	}

	return evicted;
}
EXPORT_SYMBOL(inet_frag_evictor);

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

void inet_frag_kill(struct inet_frag_queue *fq, struct inet_frags *f)
{
    /* 停止分片隊列定時器,這個定時器用來防止長時間佔用內存 */
	if (del_timer(&fq->timer))
		atomic_dec(&fq->refcnt);

    /* frag_complete一般是重組完成的時候或者釋放分片隊列的時候去設置,
     * 這裏判斷如果沒有設置的話,就設置該標誌位同時調用fq_unlink函數
     * 去處理鏈表移除的事情,包括哈希表和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);

	/* 減少排隊的分片隊列個數 */
	fq->net->nqueues--;
	write_unlock(&f->lock);
}

再來看下實際的分片隊列資源回收處理函數 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;

    /* 正常情況下刪除分片隊列前都會置上該標誌位並且分片隊列的定時器
     * 應該停止,這裏檢查下,有異常就告警
     */
	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;

        /* 實際的釋放函數 */
		frag_kfree_skb(nf, f, fp, work);
		fp = xp;
	}

    /* qsize 是分片結構體 struct ipq的大小 */
	if (work)
		*work -= f->qsize;
	atomic_sub(f->qsize, &nf->mem);

    /* 分片隊列釋放的回調處理函數
     * ipv4 這個函數是 ip4_frag_free,ipfrag_init中初始化。
     */
	if (f->destructor)
		f->destructor(q);
    /* 最後釋放分片隊列所佔內存 */
	kfree(q);
}
EXPORT_SYMBOL(inet_frag_destroy);

實際的skb釋放函數由frag_kfree_skb完成,這個函數就是釋放分片skb緩存,然後從當前所佔的內存空減去釋放的大小

/* 釋放分片隊列的skb buffer */
static inline void frag_kfree_skb(struct netns_frags *nf, struct inet_frags *f,
		struct sk_buff *skb, int *work)
{
    /* 一種情況下是分片隊列已經重組完成,這時候需要釋放,work 指針爲空 
     * 還有一種情況是當內核分片隊列所佔內存空間過大,這時候內核需要主動
     * 釋放一些舊的分片隊列,這時候work指針就表示需要釋放的空間大小
     */
	if (work)
		*work -= skb->truesize;

    /* 從分片所佔用的總的內存數量中減去當前釋放的skb緩存大小 */
	atomic_sub(skb->truesize, &nf->mem);

	/* 如果存在私有的釋放回調函數的話,這時候調用,
	 * ip4_frags 這個指針爲空
	 */
	if (f->skb_free)
		f->skb_free(skb);  

	/* 最後調用kfree_skb釋放 skb buffer */	
	kfree_skb(skb);
}

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章