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);
}

至此,分片处理的第一步已经完成,即保持分片所占用内存空间不超过阈值,再往下则是真正的处理过程,包括分片队列的查找、插入和重组。这个过程的分析放在下篇博客里。

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