tcp/ip 協議棧Linux源碼分析二 IPv4分片報文重組分析二

繼續接着上篇講,之前我們說過,收到分片報文後首先會檢查分片報文所佔內存是否過大,如果超過閾值的話就要調用ip_evictor函數去釋放一些舊的分片隊列,關於如何釋放分片隊列資源上一篇已經總結完成,接下來來看下進一步的處理,即如何查找分片隊列的,先看下代碼:

	/* 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_find()函數根據報文的五元組得到一個hash值去查找hash表找到對應的分片隊列,找到的話返回,找不到並且當前hash桶的深度不超過一定的值的話就新建一個隊列,否則就直接返回NULL。

我們看下ip_find()具體的處理流程:

/* Find the correct entry in the "incomplete datagrams" queue for
 * this IP datagram, and create new one, if nothing is found.
 * 從哈希表中找到對應的分片隊列,找不到就新建一個
 */
static inline struct ipq *ip_find(struct net *net, struct iphdr *iph, u32 user)
{
	struct inet_frag_queue *q;
	struct ip4_create_arg arg;
	unsigned int hash;

    /* arg包含了分片的五元組,源地址、目的地址、協議 
     * IP ID以及user(表示調用者,可能是協議棧也可能是netfilter )
     */
	arg.iph = iph;
	arg.user = user;

    /* 先持有哈希表的讀鎖,防止更改 */
	read_lock(&ip4_frags.lock);

	/* 根據上述五元組到一個hash值,經典的hash函數,可以拿來自用 */
	hash = ipqhashfn(iph->id, iph->saddr, iph->daddr, iph->protocol);

    /* 根據hash值查找hash表,這裏arg的作用是對分片隊列進行匹配,
     * 因爲hash值相等的分片隊列能有很多,在這個函數裏,如果找不到
     * 的話就會去新建一個分片隊列。
     */
	q = inet_frag_find(&net->ipv4.frags, &ip4_frags, &arg, hash);
	if (IS_ERR_OR_NULL(q)) {
		inet_frag_maybe_warn_overflow(q, pr_fmt());
		return NULL;
	}
	
     /* 找到了,返回ipq分片隊列指針,注意區分struct ipq 和
	 * struct inet_frag_queue的關係,兩者是包含關係,前者包含後者
	 */
	return container_of(q, struct ipq, q);
}

接着看inet_frag_find 分片隊列查找函數的實現:

/* 分片隊列查找函數 */
struct inet_frag_queue *inet_frag_find(struct netns_frags *nf,
		struct inet_frags *f, void *key, unsigned int hash)
	__releases(&f->lock)
{
	struct inet_frag_queue *q;
	struct hlist_node *n;
	int depth = 0;

    /* 遍歷hash表,即ip4_frags->hash[hash],然後調用match回調函數
     * 去和報文的五元組進行匹配,找到的話就增加該隊列的引用計數並返回其指針,
     * 找不到的話增加hash桶的深度,繼續查找下一個。
     * ip4_frags 註冊的match 回調函數是ip4_frag_match,在ip_fragment.c文件裏
     * 該函數很簡單,就是去比較五元組是否完全一樣。
     */
	hlist_for_each_entry(q, n, &f->hash[hash], list) {
		if (q->net == nf && f->match(q, key)) {
			atomic_inc(&q->refcnt);
			read_unlock(&f->lock);
			return q;
		}
		depth++;
	}
	read_unlock(&f->lock);

    /* 還是沒找到,如果hash桶深不超過限值的話就調用inet_frag_create
     * 創建一個新的分片隊列,超出的話直接返回錯誤就得了。
     * 通常收到第一個分片的時候會走到這裏。
     */
	if (depth <= INETFRAGS_MAXDEPTH)
		return inet_frag_create(nf, f, key);
	else
		return ERR_PTR(-ENOBUFS);
}
EXPORT_SYMBOL(inet_frag_find);

match對調函數,處理很簡單,就是對報文的五元組和分片隊列進行比較;

static int ip4_frag_match(struct inet_frag_queue *q, void *a)
{
	struct ipq *qp;
	struct ip4_create_arg *arg = a;

	qp = container_of(q, struct ipq, q);
	return	qp->id == arg->iph->id &&
			qp->saddr == arg->iph->saddr &&
			qp->daddr == arg->iph->daddr &&
			qp->protocol == arg->iph->protocol &&
			qp->user == arg->user;
}

這裏重點關注下 inet_frag_create 函數:

/* 創建分片隊列 */
static struct inet_frag_queue *inet_frag_create(struct netns_frags *nf,
		struct inet_frags *f, void *arg)
{
	struct inet_frag_queue *q;

    /* 創建並初始化分片隊列 */
	q = inet_frag_alloc(nf, f, arg);
	if (q == NULL)
		return NULL;

    /* 將分片隊列插入到分片哈希表中和lru鏈表尾部 */
	return inet_frag_intern(nf, q, f, arg);
}

inet_frag_alloc就是創建一個分片隊列緩存然後初始化:

static struct inet_frag_queue *inet_frag_alloc(struct netns_frags *nf,
		struct inet_frags *f, void *arg)
{
	struct inet_frag_queue *q;

    /* qsize指的是分片隊列的固定大小,等於sizeof(struct ipq) */
	q = kzalloc(f->qsize, GFP_ATOMIC);
	if (q == NULL)
		return NULL;

    /* 初始化分片隊列,將五元組賦值給分片隊列,
     * constructor是初始化回調函數是,ip4_frag_init(), 在ipfrag_init()裏設置。
     */
	f->constructor(q, arg);

	/* 增加分片所佔用的內存大小 */
	atomic_add(f->qsize, &nf->mem);

	/* 初始化該分片隊列的定時器,並設置該定時器的回調處理函數 
	 * 回調處理函數是在系統初始化的時候設置的,ip4的分片定時器
	 * 回調處理函數是ip_expire(), 該定時器的主要作用是重組超時後
	 * 釋放該分片隊列所佔資源,防止大量分片長時間佔用內存,定時器
     * 的時間也是可以通過proc文件系統去配置的等。
	 */
	setup_timer(&q->timer, f->frag_expire, (unsigned long)q);
	spin_lock_init(&q->lock);

	/* 初始化引用計數爲1 */
	atomic_set(&q->refcnt, 1);
	q->net = nf;

	return q;
}

上面分片隊列創建完成之後,還要調用inet_frag_intern()函數將分片隊列插入到hash數組中,看下這個函數的處理:

/* 分片隊列插入函數 */
static struct inet_frag_queue *inet_frag_intern(struct netns_frags *nf,
		struct inet_frag_queue *qp_in, struct inet_frags *f,
		void *arg)
{
	struct inet_frag_queue *qp;
#ifdef CONFIG_SMP
	struct hlist_node *n;
#endif
	unsigned int hash;

    /* 因爲是修改分片hash表,這裏要求寫鎖 */
	write_lock(&f->lock);
	/*
	 * While we stayed w/o the lock other CPU could update
	 * the rnd seed, so we need to re-calculate the hash
	 * chain. Fortunatelly the qp_in can be used to get one.
	 */
	 /*
	 * hashfn函數指針在ipfrag_init()裏初始化爲ip4_hashfn(),
	 * 就是一個hash函數
	 */
	hash = f->hashfn(qp_in);
#ifdef CONFIG_SMP
	/* With SMP race we have to recheck hash table, because
	 * such entry could be created on other cpu, while we
	 * promoted read lock to write lock.
	 * 
	 * 在多核處理情況下有可能其它CPU也收到同一路報文然後創建 了
	 * 分片隊列,如果出現這種情況就將我們新創建的分片隊列釋放掉,
	 * 即設置last_in標誌位,然後調用inet_frag_put()做釋放處理,
	 * 這時候把先創建的分片隊列qp返回就好了。
	 */
	 
	hlist_for_each_entry(qp, n, &f->hash[hash], list) {
		if (qp->net == nf && f->match(qp, arg)) {
			atomic_inc(&qp->refcnt);
			write_unlock(&f->lock);
			qp_in->last_in |= INET_FRAG_COMPLETE;
			inet_frag_put(qp_in, f);
			return qp;
		}
	}
#endif
	qp = qp_in;
	/* 重新初始化分片隊列超時時間 */
	if (!mod_timer(&qp->timer, jiffies + nf->timeout))
		atomic_inc(&qp->refcnt);

	atomic_inc(&qp->refcnt);

	/* 插入到分片hash表的頭部 */
	hlist_add_head(&qp->list, &f->hash[hash]);

	/* 插入到lru鏈表的尾部,當分片所佔空用過大的時候,
	 * 內核會從lru的首部順序釋放分片隊列,因爲排在前面的
	 * 都是舊的分片,新的都掛在lru尾部
	 */
	list_add_tail(&qp->lru_list, &nf->lru_list);

	/* 增加分片隊列個數 */
	nf->nqueues++;

	/* 插入結束,釋放寫鎖 */
	write_unlock(&f->lock);
	return qp;
}

在多核處理情況下可能會重複創建分片隊列,這時候後創建的分片隊列對調用inet_frag_put() 函數進行釋放,這是個靜態內聯函數,原型在inet_frag.h裏

static inline void inet_frag_put(struct inet_frag_queue *q, struct inet_frags *f)
{
    /* 當分片隊列引用計數爲0的時候,釋放分片隊列 */
	if (atomic_dec_and_test(&q->refcnt))
		inet_frag_destroy(q, f, NULL);
}

inet_frag_destroy()函數上篇博客已介紹,這裏不再重複。

ip_find()函數返回後就已經得到了該報文所對應的分片隊列,這時候再調用ip_frag_queue()進行進一步處理,當然,ip_find也有可能返回失敗,這個時候就只能釋放該報文skb緩存。

ip_frag_queue函數主要進行分片報文的排隊、重組處理,這裏需要處理多種異常情況,函數比較長,今晚就先不講了,放在下篇講。

 

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