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

本文深入解析Linux内核3.4.39版本中IPv4报文重组的详细流程,涵盖分片报文处理、内核内存管理策略、哈希表查找与分片队列操作等关键环节,旨在揭示内核如何高效处理分片重组任务。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

内核版本: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);
}

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

<think>嗯,用户想要了解Linux内核5.4.0中ip_list_rcv的相关逻辑。首先,我需要回忆下这个函数的作用。ip_list_rcv应该是处理IP分片重组后的数据包接收函数吧?可能属于网络层的部分,负责处理接收到的IP分片列表。 接下来,我应该先确认这个函数在内核源码中的位置。通常,IP相关的处理代码可能在net/ipv4目录下,具体可能在ip_input.c或者ip_fragment.c文件中。需要检查下5.4.0版本的源码结构。 假设找到了ip_list_rcv函数,它的主要逻辑可能包括检查数据包的合法性,比如校验和是否正确,是否属于分片重组后的数据包。然后可能会调用上层协议的处理函数,比如TCP或UDP的接收函数。 另外,分片重组后的数据包可能需要经过Netfilter钩子点,比如NF_INET_LOCAL_IN,所以ip_list_rcv可能会涉及到这些钩子的处理。此外,还要考虑路由查找,确定数据包是发给本机的还是需要转发的。 还要注意,处理过程中可能会更新些统计信息,比如接收的报文数量或字节数。错误处理也是关键部分,比如无效的分片或校验和错误时,如何丢弃数据包并记录日志。 可能还需要查看调用ip_list_rcv的上层函数,比如ip_local_deliver函数,这样能更好地理解整个流程。此外,是否有相关的锁机制或并发处理需要考虑,确保多核环境下的正确性。 用户可能对分片重组的具体过程感兴趣,但问题主要集中在ip_list_rcv的逻辑上。需要明确这个函数在重组后的处理步骤,而重组本身可能由ip_defrag函数完成,然后交给ip_list_rcv处理完整的报文。 最后,应该整理这些思路,分步骤解释函数的逻辑,包括参数、主要处理步骤、调用的子函数、错误处理等,确保用户能够清晰地理解整个过程。</think>在Linux内核5.4.0版本中,`ip_list_rcv`函数是IPv4协议栈中处理重组后的IP分片数据包的核心函数。以下是其关键逻辑分析: --- ### 、函数定位 代码路径:`net/ipv4/ip_input.c` ```c static int ip_list_rcv(struct list_head *head, struct packet_type *pt, struct net_device *orig_dev) ``` --- ### 二、核心逻辑步骤 #### 1. 参数解析 - `head`: 重组完成的IP数据包链表 - `pt`: 协议类型描述符(标识IP协议) - `orig_dev`: 原始接收网络设备 #### 2. 遍历处理链表 通过`list_for_each_entry_safe`宏遍历每个skb: ```c list_for_each_entry_safe(skb, next, head, list) { list_del(&skb->list); /* 处理逻辑 */ } ``` - 使用安全遍历方式避免链表修改导致的冲突 - 从链表中摘除skb后独立处理 #### 3. 有效性校验 ```c iph = ip_hdr(skb); if (ip_hdr(skb)->ihl < 5 || ip_hdrlen(skb) < sizeof(*iph)) goto inhdr_error; ``` - 检查IP头长度合法性(ihl字段必须≥5) - 验证IP头长度不小于标准IPv4头长度 #### 4. Netfilter钩子处理 ```c NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN, dev_net(skb->dev), NULL, skb, skb->dev, NULL, ip_rcv_finish); ``` - 触发`NF_INET_LOCAL_IN`钩子点(本地接收流量) - 最终调用`ip_rcv_finish`完成接收处理 #### 5. 错误处理 ```c inhdr_error: IP_INC_STATS_BH(dev_net(skb->dev), IPSTATS_MIB_INHDRERRORS); kfree_skb(skb); ``` - 统计错误计数器`IPSTATS_MIB_INHDRERRORS` - 释放无效skb避免内存泄漏 --- ### 三、关联调用流程 ``` ip_local_deliver() └── ip_local_deliver_finish() └── ip_defrag() // 分片重组 └── ip_frag_queue() // 分片队列处理 └── ip_frag_reasm() // 重组完成后 └── ip_list_rcv() // 重组报文处理入口 ``` --- ### 四、关键设计特点 1. **链表批处理**:次性处理多个重组完成的skb,提升效率 2. **无锁设计**:由IP分片重组层保证链表操作的原子性 3. **统计集成**:通过`IP_INC_STATS_BH`更新内核统计计数器 4. **错误隔离**:单个skb错误不影响整个链表处理 --- ### 五、典型应用场景 - 处理经过分片重组后的UDP大数据包 - 处理分片IPsec加密流量 - 处理分片的ICMP差错报文 建议结合`ip_defrag.c`中的分片重组逻辑分析完整处理流程。实际调试时可通过`sysctl -a | grep net.ipv4.ipfrag`查看相关参数调整重组行为。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值