tcp/ip 协议栈Linux源码分析四 IPv4分片 ip_fragment函数分析
程序员文章站
2024-03-19 22:49:22
...
内核版本:3.4.39
很多项目涉及到IP分片的时候都是绕过去了,感觉分片挺难的。但是老这么做也不行啊,抽空分析了内核的分片处理函数ip_fragment,也不是特别复杂,感觉挺简单的,看来事情只有实际去做才知道。
int ip_fragment(struct sk_buff *skb, int (*output)(struct sk_buff *))
{
struct iphdr *iph;
int ptr;
struct net_device *dev;
struct sk_buff *skb2;
unsigned int mtu, hlen, left, len, ll_rs;
int offset;
__be16 not_last_frag;
struct rtable *rt = skb_rtable(skb);
int err = 0;
/* 获取路由里面的出口设备 */
dev = rt->dst.dev;
/* 获取IP头指针 */
iph = ip_hdr(skb);
/* 如果数据包携带不分片标志并且本地开启了pmtu发现(local_df==0),则需要
* 给发送方返回一个icmp不可达报文。
*/
if (unlikely((iph->frag_off & htons(IP_DF)) && !skb->local_df)) {
IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGFAILS);
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED,
htonl(ip_skb_dst_mtu(skb)));
kfree_skb(skb);
return -EMSGSIZE;
}
/*
* Setup starting values.
*/
/* 获取IP首部长度 */
hlen = iph->ihl * 4;
/* 获取分片报文数据部分大小(mtu 最大传输单元) */
mtu = dst_mtu(&rt->dst) - hlen; /* Size of data space */
#ifdef CONFIG_BRIDGE_NETFILTER
if (skb->nf_bridge)
mtu -= nf_bridge_mtu_reduction(skb);
#endif
/* 设置标志位,ip_defrag重组函数会去判断这个标志位 */
IPCB(skb)->flags |= IPSKB_FRAG_COMPLETE;
/* When frag_list is given, use it. First, check its validity:
* some transformers could create wrong frag_list or break existing
* one, it is not prohibited. In this case fall back to copying.
*
* LATER: this step can be merged to real generation of fragments,
* we can switch to copy when see the first bad fragment.
*/
/* 传输层如果已经进行了分片 */
if (skb_has_frag_list(skb)) {
struct sk_buff *frag, *frag2;
/* 同一个页面内的报文长度,不包括其它分片 */
int first_len = skb_pagelen(skb);
/*
* 以下四种情况是不能进行快速分片的
* 1. 第一个报文长度大于mtu。
* 2. 第一个报文长度不是8的倍数。
* 3. 该报文自身就是分片报文,需要走慢速通道。
* 4. 该报文是克隆的,因为快速分片是直接修改skb的,如果是克隆的
* 则在其它地方存在引用,因此不能直接修改。
*/
if (first_len - hlen > mtu ||
((first_len - hlen) & 7) ||
ip_is_fragment(iph) ||
skb_cloned(skb))
goto slow_path;
/* 判断其它分片是否满足快速分片要求 */
skb_walk_frags(skb, frag) {
/* 如果SKB分片超过mtu则进入慢速分片。
* 如果SKB分片不是8的倍数并且不是最后一个分片也走慢速通道。
* 如果SKB分片头部空间无法塞下一个IP头
* 符合上述情况就进入慢速通道,和上面的慢速通道略有区别
*/
if (frag->len > mtu ||
((frag->len & 7) && frag->next) ||
skb_headroom(frag) < hlen)
goto slow_path_clean;
/* 如果某个地方在引用skb结构体
* 就进入慢速通道。
*/
if (skb_shared(frag))
goto slow_path_clean;
/* 这里我有点疑问,为啥分片的skb不能有sk */
BUG_ON(frag->sk);
if (skb->sk) {
/* 将所有片段都关联到同一个套接字
* 设置套接字的回调函数
*/
frag->sk = skb->sk;
frag->destructor = sock_wfree;
}
/* truesize 是skb的总长度,包括skb结构体和数据部分大小
* 这里将其长度从skb中移除,相当于分离开来
*/
skb->truesize -= frag->truesize;
}
/* Everything is OK. Generate! */
err = 0;
offset = 0;
/* 保存那些独立分片 */
frag = skb_shinfo(skb)->frag_list;
/* 清空首个报文的分片指针,该指针已经保存在frag里 */
skb_frag_list_init(skb);
/* 第一个报文非线性数据区长度 */
skb->data_len = first_len - skb_headlen(skb);
/* 设置skb数据长度,包括线性区和非线性区 */
skb->len = first_len;
/* 设置ip头域里面数据长度 */
iph->tot_len = htons(first_len);
/* 设置MF标志位 */
iph->frag_off = htons(IP_MF);
/* 设置IP头域校验和 */
ip_send_check(iph);
/* 准备其它分片 */
for (;;) {
if (frag) {
frag->ip_summed = CHECKSUM_NONE;
/* 设置传输层首部指针 */
skb_reset_transport_header(frag);
/* 插入IP首部长度 */
__skb_push(frag, hlen);
/* 设置网络层首部指针 */
skb_reset_network_header(frag);
/* 复制IP头部,因为是后续分片,所以要设置
* 偏移值,长度和分片标志位,当然,校验和要重新计算。
*/
memcpy(skb_network_header(frag), iph, hlen);
iph = ip_hdr(frag);
iph->tot_len = htons(frag->len);
/* 复制第一个分片报文的标志位,包括优先级,出口设备
* netfilter子模块使用到的标志位等等。
* 所有报文设置相同的属性
*/
ip_copy_metadata(frag, skb);
/* 处理IP扩展选项,分片情况下扩展选项处理还是比较特殊的
* 有些选项要求所有分片报文都要携带,有些选项只需要首个分片报文携带。
* 详细情况参考RFC791
*/
if (offset == 0)
ip_options_fragment(frag);
/* 分片偏移值,等于之前报文长度累积和 */
/* 这里已经假定之前的报文长度都是一样的 */
offset += skb->len - hlen;
/* 一定能被8整除? */
/* 没错,毕竟大小是之前分片的总和 */
iph->frag_off = htons(offset>>3);
/* 如果不是最后一个分片,就设置MF标识 */
if (frag->next != NULL)
iph->frag_off |= htons(IP_MF);
/* Ready, complete checksum */
/* 重新计算校验和 */
ip_send_check(iph);
}
/* 调用发送接口发送出去 */
err = output(skb);
if (!err)
IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGCREATES);
/* 如果出错并且没有多余分片 */
if (err || !frag)
break;
/* 当前skb传输完成,指向下一个分片 */
skb = frag;
frag = skb->next;
skb->next = NULL;
}
/* 传输正常,增加MIB统计计数 */
if (err == 0) {
IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGOKS);
return 0;
}
/* 当传输结束后,释放skb */
while (frag) {
skb = frag->next;
kfree_skb(frag);
frag = skb;
}
IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGFAILS);
return err;
slow_path_clean:
/* 这一步到时没有很好理解⊙﹏⊙‖∣,了解的大牛可以说下 */
skb_walk_frags(skb, frag2) {
if (frag2 == frag)
break;
frag2->sk = NULL;
frag2->destructor = NULL;
skb->truesize += frag2->truesize;
}
}
/* 慢速分片 */
slow_path:
/* 先计算纯数据总长度,这个长度不包括IP头长 */
left = skb->len - hlen; /* Space per frame */
/* 指向数据的起始位置 */
ptr = hlen; /* Where to start from */
/* for bridged IP traffic encapsulated inside f.e. a vlan header,
* we need to make room for the encapsulating header
*/
/* (链路层预留空间) */
ll_rs = LL_RESERVED_SPACE_EXTRA(rt->dst.dev, nf_bridge_pad(skb));
/*
* Fragment the datagram.
*/
/* 获取偏移值,不从0开始是因为有可能需要分片的报文自身就是一个分片报文 */
offset = (ntohs(iph->frag_off) & IP_OFFSET) << 3;
/* 可能的取值包括0和1 */
not_last_frag = iph->frag_off & htons(IP_MF);
/* 进行分片处理,left的初值就是数据总长 */
while (left > 0) {
len = left;
/* 确保每个报文长度不超过MTU */
if (len > mtu)
len = mtu;
/* 确保分片报文大小长度为8字节的整数倍,最后一个分片除外 */
if (len < left) {
len &= ~7;
}
/* 分配一个新的skb buffer
* 数据长度+IP头部长度+链路长度
*/
if ((skb2 = alloc_skb(len+hlen+ll_rs, GFP_ATOMIC)) == NULL) {
NETDEBUG(KERN_INFO "IP: frag: no memory for new fragment!\n");
err = -ENOMEM;
goto fail;
}
/* 复制一些公共数据 */
ip_copy_metadata(skb2, skb);
/* 预留链路层首部空间 */
skb_reserve(skb2, ll_rs);
/* 插入数据部分和IP头 */
skb_put(skb2, len + hlen);
/* 重置网络头指针 */
skb_reset_network_header(skb2);
/* 设置传输层头部指针 */
skb2->transport_header = skb2->network_header + hlen;
/** 将每一个分片的skb包都关联到源包的socket */
if (skb->sk)
skb_set_owner_w(skb2, skb->sk);
/*
* Copy the packet header into the new buffer.
*/
/* 复制IP报头 */
skb_copy_from_linear_data(skb, skb_network_header(skb2), hlen);
/*
* Copy a block of the IP datagram.
* 从原始报文中的ptr起复制长度为len的数据到skb2中
*/
if (skb_copy_bits(skb, ptr, skb_transport_header(skb2), len))
BUG();
/* 从总长度中减去这个分片的长度,得到剩余部分的长度 */
left -= len;
/*
* 接下来就是设置分片报文的网络层首部
*/
iph = ip_hdr(skb2);
/* 设置片偏移 */
iph->frag_off = htons((offset >> 3));
/* ANK: dirty, but effective trick. Upgrade options only if
* the segment to be fragmented was THE FIRST (otherwise,
* options are already fixed) and make it ONCE
* on the initial skb, so that all the following fragments
* will inherit fixed options.
*/
/* 处理IP选项,对于分片报文来说有些选项是需要每个报文必须携带,
* 有些选项只需第一个分片报文携带就可以了。具体的操作可以参考RFC791
*/
if (offset == 0)
ip_options_fragment(skb);
/*
* Added AC : If we are fragmenting a fragment that's not the
* last fragment then keep MF on each bit
*/
/* 设置分片标识位
* 刚才提到not_last_frag可以是0或者1,取决于它在原始分片报文的值
*/
if (left > 0 || not_last_frag)
iph->frag_off |= htons(IP_MF);
/* 更新数据指针,指向下一个分片报文的起始复制位置 */
ptr += len;
/* 更新片偏移字段 */
offset += len;
/* 更新长度字段 */
iph->tot_len = htons(len + hlen);
/* 更新校验和字段 */
ip_send_check(iph);
/* 配置完成发送 */
err = output(skb2);
if (err)
goto fail;
IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGCREATES);
}
/* 分片结束,释放原始报文
* 这个和快速分片不同,快速分片是发送原始分片
* 慢速分片是新建skb然后复制发送
*/
kfree_skb(skb);
IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGOKS);
return err;
fail:
kfree_skb(skb);
IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGFAILS);
return err;
}
EXPORT_SYMBOL(ip_fragment);
参考文档:
1. linux内核ip分片函数ip_fragment解析 https://blog.csdn.net/force_eagle/article/details/4555314
2. SKB结构体分析 http://vger.kernel.org/~davem/skb.html