IP协议栈在输出数据包到下层协议栈前会检查IP报文的长度,若是IP报文超过了MTU就会对报文执行分段,接下来的篇幅中就来分析IP分段函数。
不管是本地输出的还是转发的IPv4数据包,最终都会被送入ip_output
函数,ip_output
函数大致内容如下,在报文经过Netfilter IPv4 POSTROUTING
回调点之后就会进行IP分段,分段完成后再输出到邻居子系统。
ip_output
NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING,..., ip_finish_output, !(IPCB(skb)->flags & IPSKB_REROUTED)); // Netfilter IPv4 POSTROUTING
ip_finish_output
__ip_finish_output
if(skb->len > mtu) // 报文长度超过MTU则需要调用ip_fragment函数来分段,分段完成后调用 ip_finish_output2来输出报文到邻居子系统
ip_fragment
ip_do_fragment
ip_finish_output2
else // 报文长度不超过MTU时直接调用 ip_finish_output2来输出报文到邻居子系统
ip_finish_output2
IP分段由ip_fragment
函数完成,ip_fragment
函数会按照MTU值分配新的sk_buff
,然后将原sk_buff
携带的报文拷贝到到新分配的sk_buff
并为新的报文构造IP头部。
ip_fragment函数
ip_fragment
会调用ip_do_fragment
函数,主要的分段工作都是在ip_do_fragment
中完成的。ip_do_fragment
函数在处理报文时分两个分支:快速路径和慢速路径。
经过1 中对ip_append_data
函数分析后,可以知道本地输出的sk_buff
到达ip_fragment
函数时,组织结构可能是这样的(以frag_list
组织起来的sk_buff
, 整个链表上的sk_buff
装的都是同一个ip报文的数据):
ip_do_fragment
函数中的快速路径就是专门处理这种经历过预分段的sk_buff
,经过ip_append_data
函数的预分段,所有sk_buff
中携带的报文长度已经满足MTU,并且为ip头部和二层头部预留了足够空间,对于这类结构的sk_buff
,ip_do_fragment
函数只需要构造ip头部即可,不需要再新分配sk_buff
和拷贝ip报文。
其实不止ip_append_data
函数,在ip组包的过程中也会产生这样的结构(以frag_list
组织起来的sk_buff
),这些sk_buff
进入转发流程后最终也会被送入ip_do_fragment
函数中的快速路径来执行分段。
ip_do_fragment
函数中的慢速路径则是专门处那些没有经历过预分段的sk_buff
。
快速路径
快速路径上会先对所有sk_buff
进行合法性校验,校验通过后会为所有sk_buff
构造好ip头部再逐个发送,快速路径上将直接使用预分段过程中分配的sk_buff
,不会分配新的sk_buff
也不进行额外的数据拷贝。
int ip_do_fragment(struct net *net, struct sock *sk, struct sk_buff *skb,
int (*output)(struct net *, struct sock *, struct sk_buff *))
{
struct iphdr *iph;
struct sk_buff *skb2;
struct rtable *rt = skb_rtable(skb);
unsigned int mtu, hlen, ll_rs;
struct ip_fraglist_iter iter;
ktime_t tstamp = skb->tstamp;
struct ip_frag_state state;
......
// iph指针指向ip头部
iph = ip_hdr(skb);
// 获取MTU
mtu = ip_skb_dst_mtu(sk, skb);
if (IPCB(skb)->frag_max_size && IPCB(skb)->frag_max_size < mtu)
mtu = IPCB(skb)->frag_max_size;
// hlen代表IP 头部长度
hlen = iph->ihl * 4;
// mtu自减了hlen, 它此时代表着IP荷载最大长度
mtu = mtu - hlen; /* Size of data space */
IPCB(skb)->flags |= IPSKB_FRAG_COMPLETE;
// ll_rs是应当为L2头部预留的空间
ll_rs = LL_RESERVED_SPACE(rt->dst.dev);
// skb_has_frag_list(skb)会判断skb_shinfo(skb)->frag_list是否不为NULL,若不为NULL,则走快速路径
if (skb_has_frag_list(skb)) {
/*************************************************************************
* 1 校验frag_list上sk_buff的合法性
*************************************************************************/
struct sk_buff *frag, *frag2;
// 1.1 确认首个`sk_buff`的合法性
// first_len是首个`sk_buff`的数据长度,也就是首个sk_buff所携带的IP报文长度
unsigned int first_len = skb_pagelen(skb);
// 即便快速路径上的sk_buff之前经过了预分段,但仍然会对它们进行一些检查,符合以下任一情况,则转入慢速路径进行处理:
if (first_len - hlen > mtu || // IP报文长度大于MTU
((first_len - hlen) & 7) || // IP数据长度不为8字节倍数
ip_is_fragment(iph) || // 该sk_buff携带的IP数据是一个IP片段
skb_cloned(skb) || // skb是克隆的
skb_headroom(skb) < ll_rs) // 预留的L2头部空间不够
goto slow_path; // 转入慢速路径
// 1.2 确认frag_list上剩余sk_buff的合法性
skb_walk_frags(skb, frag) {
if (frag->len > mtu || // IP片段长度大于MTU
((frag->len & 7) && frag->next) || // 不是最后一个IP片段且IP数据长度不为8字节倍数
skb_headroom(frag) < hlen + ll_rs) // 没有预留足够的L2头部空间和IP头部空间
goto slow_path_clean; // 转入慢速路径
// sk_buff引用计数不为1, 则可能其它模块也在使用该sk_buff,则走慢速路径执行拷贝动作更为合适
if (skb_shared(frag))
goto slow_path_clean; // 转入慢速路径
BUG_ON(frag->sk);
if (skb->sk) {
frag->sk = skb->sk;
frag->destructor = sock_wfree;
}
// 1.3 还原首个sk_buff的truesize
// sk_buff的truesize本代表着一个sk_buff所占用的空间总大小,但首个sk_buff它代表的是整个IP报文,它的truesize其实是自身的size加上frag_list上所有sk_buff的truesize之和
// 这里减去frag_list上sk_buff的 truesize,还原首个sk_buff的 truesize
skb->truesize -= frag->truesize;
}
/****************************************************************
* 2 接下来的将在循环中逐个发送所有sk_buff
* 除了首个sk_buff外,frag_list上的sk_buff其实是没有构造IP头部的, 所以在接下来循环中,
* 会为它们构造IP头部。
* 整个循环会使用一个struct ip_fraglist_iter类型的结构体来控制
*************************************************************/
// 2.1 还原首个sk_buff的len,datalen等字段
// 首个sk_buff它代表着整个IP报文,它的truesize是自身的size加上frag_list上所有sk_buff的truesize之和,
// datalen是自身原始len加上frag_list上所有sk_buff的len之和, 它的datalen是自身原始datalen加上frag_list上所有sk_buff的len之和
// ip_fraglist_init会还原首个sk_buff的len,datalen等值,更新其IP头部中的报文长度,分段标志等信息,然后重新计算IP校验值,初始化iter这个控制变量
ip_fraglist_init(skb, iph, hlen, &iter);
for (;;) {
// 2.2 每当循环开始时,skb指针指向本轮循环要发送的sk_buff,iter.frag指向下一个要发送的sk_buff,
// iter.iph指向当前sk_buff的IP头部,iter.offset代表当前sk_buff携带的IP片段的在总IP报文中的偏移
// 2.3 下一个sk_buff需要根据当前sk_buff才能完成IP头部构造,所以发送当前sk_buff时需要先为下个sk_buff构造IP头部
if (iter.frag) {
// IP报文分段后,有的IP选项只存在于第一个IP片段中,其它的的IP片段不需要携带,比如LSRR/SSRR等选项,ip_fraglist_ipcb_prepare会将第二个sk_buff的IP选项中不需要携带的ip_option配置为 'No Operation'
ip_fraglist_ipcb_prepare(skb, &iter);
// 将当前sk_buff的IP头部以及一些控制信息拷贝给下个sk_buff (其实这里让我感到疑惑,ip_fraglist_ipcb_prepare为第二个sk_buff处理了IP选项,但ip_fraglist_prepare又会将第一个sk_buff的IP选项拷贝给第二个sk_buff,进而会导致所有的sk_buff都携带了完整的IP选项)
ip_fraglist_prepare(skb, &iter);
// 经过上面两个函数后,iter.iph将指向下个sk_buff的IP头部,iter.offset将代表下个sk_buff中IP片段的在总IP报文中的偏移
}
skb->tstamp = tstamp;
// 2.4 输出sk_buff到邻居子系统,即调用ip_finish_output2函数
err = output(net, sk, skb);
if (!err)
IP_INC_STATS(net, IPSTATS_MIB_FRAGCREATES);
if (err || !iter.frag)
break;
// 2.5 ip_fraglist_next会返回下个sk_buff的指针,并将iter.frag指向下下个要发送的sk_buff
skb = ip_fraglist_next(&iter);
}
......
return err;
......
}
slow_path:
......
}
接下来详细看看ip_fraglist_init
,ip_fraglist_ipcb_prepare
,ip_fraglist_prepare
,ip_fraglist_next
四个函数的处理过程
ip_fraglist_init函数
ip_fraglist_init
用于初始化iter,并配置首个sk_buff
的IP头部
void ip_fraglist_init(struct sk_buff *skb, struct iphdr *iph,
unsigned int hlen, struct ip_fraglist_iter *iter)
{
unsigned int first_len = skb_pagelen(skb);
iter->frag = skb_shinfo(skb)->frag_list; // iter->frag 指向第二个sk_buff
skb_frag_list_init(skb);
iter->offset = 0; // 首个sk_buff的偏移为0
iter->iph = iph; // iter->iph 指向首个sk_buff的IP头部
iter->hlen = hlen; // iter->iph代表IP头部长度
skb->data_len = first_len - skb_headlen(skb); // 还原首个sk_buff的data_len, 此时skb->data_len代表首个sk_buff paged buffer携带的数据长度
skb->len = first_len; // 还原首个sk_buff的len, 此时skb->len代表首个sk_buff 所携带的IP报文的长度(包含头部和数据部分)
iph->tot_len = htons(first_len);
iph->frag_off = htons(IP_MF); // 配置MORE FLAG标志
ip_send_check(iph); // 计算IP头部校验值
}
ip_fraglist_ipcb_prepare函数
ip_fraglist_ipcb_prepare
会将第二个sk_buff
的IP选项中不需要携带的ip_option配置为 ‘No Operation’
static void ip_fraglist_ipcb_prepare(struct sk_buff *skb,
struct ip_fraglist_iter *iter)
{
struct sk_buff *to = iter->frag; // to指向下一个sk_buff,ip_fraglist_ipcb_prepare第一次调用时则它指向第二个sk_buff
/* Copy the flags to each fragment. */
IPCB(to)->flags = IPCB(skb)->flags;
if (iter->offset == 0) // iter->offset == 0表示to指向第二个sk_buff。为什么后续sk_buff不需要调用ip_options_fragment呢,因为后续的sk_buff的IP头部拷贝自前一个sk_buff
ip_options_fragment(to); // 将第二个`sk_buff`的IP选项中不需要携带的ip_option配置为 'No Operation'
}
ip_fraglist_prepare函数
ip_fraglist_prepare
为下一个sk_buff
构造IP头部
void ip_fraglist_prepare(struct sk_buff *skb, struct ip_fraglist_iter *iter)
{
unsigned int hlen = iter->hlen;
struct iphdr *iph = iter->iph;
struct sk_buff *frag;
// frag指向下一个sk_buff,也就是需要构造IP头部的那个sk_buff
frag = iter->frag;
frag->ip_summed = CHECKSUM_NONE;
skb_reset_transport_header(frag); // 记录传输层头部所处位置(skb->transport_header),除了第一个sk_buff,其它sk_buff其实没有携带传输层头部
__skb_push(frag, hlen); // 将 frag->data指针指向IP头部位置
skb_reset_network_header(frag); // 记录网络层头部所处位置(skb->network_header)
memcpy(skb_network_header(frag), iph, hlen); // 将当前sk_buff (skb)的IP头部拷贝给下一个sk_buff(frag)
iter->iph = ip_hdr(frag); // iter->iph指向下一个sk_buff的IP头部
iph = iter->iph;
iph->tot_len = htons(frag->len);
ip_copy_metadata(frag, skb); // 拷贝sk_buff中的控制信息,比如sk_buff::priority,sk_buff::protocol.sk_buff::dev,路由信息,连接信息等
iter->offset += skb->len - hlen; // iter->offset此时代表下一个sk_buff的偏移
iph->frag_off = htons(iter->offset >> 3);
if (frag->next)
iph->frag_off |= htons(IP_MF); // 配置MORE FLAG标志
/* Ready, complete checksum */
ip_send_check(iph); // 计算IP头部校验值
}
ip_fraglist_next
当前sk_buff
发送出去后,ip_fraglist_next
函数将iter->frag
指向下下个要发送的sk_buff
,并返回下个要发送的sk_buff
。
static inline struct sk_buff *ip_fraglist_next(struct ip_fraglist_iter *iter)
{
struct sk_buff *skb = iter->frag;
iter->frag = skb->next; // 更新iter->frag
skb_mark_not_on_list(skb);
return skb;
}
慢速路径
慢速路径上会按照IP分段的原则将原报文数据部分划分为几个片段并将他们拷贝到新分配的sk_buff
中,接下来看看慢速路径的流程
int ip_do_fragment(struct net *net, struct sock *sk, struct sk_buff *skb,
int (*output)(struct net *, struct sock *, struct sk_buff *))
{
struct iphdr *iph;
struct sk_buff *skb2;
struct rtable *rt = skb_rtable(skb);
unsigned int mtu, hlen, ll_rs;
struct ip_fraglist_iter iter;
ktime_t tstamp = skb->tstamp;
struct ip_frag_state state;
......
// iph指针指向ip头部
iph = ip_hdr(skb);
// 获取MTU
mtu = ip_skb_dst_mtu(sk, skb);
if (IPCB(skb)->frag_max_size && IPCB(skb)->frag_max_size < mtu)
mtu = IPCB(skb)->frag_max_size;
// hlen代表IP 头部长度
hlen = iph->ihl * 4;
// mtu自减了hlen, 它此时代表着IP荷载最大长度
mtu = mtu - hlen; /* Size of data space */
IPCB(skb)->flags |= IPSKB_FRAG_COMPLETE;
// ll_rs是应当为L2头部预留的空间
ll_rs = LL_RESERVED_SPACE(rt->dst.dev);
...... // 此处省略快速路径
slow_path:
/**************************************************************
* 慢速路径中,将循环的创建新sk_buff, 并从原始sk_buff拷贝数据到新sk_buff
* skb指针指向原始sk_buff, skb2指向新分配的sk_buff, state用于控制整个循环,记录分段状态
***************************************************************/
// 1 ip_frag_init初始化用于控制整个循环的state
ip_frag_init(skb, hlen, ll_rs, mtu, IPCB(skb)->flags & IPSKB_FRAG_PMTU,
&state);
// state.left代表剩余的需处理数据长度,这个循环中skb始终指向原始的sk_buff,skb2是新分配的sk_buff
while (state.left > 0) {
// first_frag
bool first_frag = (state.offset == 0);
// 2 ip_frag_next将创建新的sk_buff, 并从原始sk_buff拷贝数据、IP头部以及一些必要的控制信息(比如sk_buff::priority,sk_buff::protocol.sk_buff::dev,路由信息,连接信息等)
skb2 = ip_frag_next(skb, &state);
if (IS_ERR(skb2)) {
err = PTR_ERR(skb2);
goto fail;
}
// 3 当ip_frag_next函数创建第一个IP片段对应的sk_buff后,ip_frag_ipcb函数会对原始skb的IP option做一些处理,因为除了第一个IP片段,其它IP片段都不需要携带所有的ip选项(比如LSRR/SSRR等选项),所以ip_frag_ipcb会把不需要携带的option配置为 'No Operation'。
// 后续循环中,创建新的sk_buff时拷贝到的的IP头部已经不携带LSRR/SSRR等选项
ip_frag_ipcb(skb, skb2, first_frag, &state);
// 4 输出skb2到下层(接下来是邻居子系统),即调用ip_finish_output2函数
skb2->tstamp = tstamp;
err = output(net, sk, skb2);
if (err)
goto fail;
}
consume_skb(skb);
return err;
fail:
kfree_skb(skb);
return err;
}
接下来看看慢速路径中调用的三个函数:ip_frag_init
,ip_frag_next
,ip_frag_ipcb
ip_frag_init函数
ip_frag_init
负责初始化用于控制整个分段流程的控制变量。
void ip_frag_init(struct sk_buff *skb, unsigned int hlen,
unsigned int ll_rs, unsigned int mtu, bool DF,
struct ip_frag_state *state) // 初始化state中的变量,用于控制整个分段流程
{
struct iphdr *iph = ip_hdr(skb);
state->DF = DF; // 设置禁止分段标志
state->hlen = hlen; // ip头部长度
state->ll_rs = ll_rs; // 链路层预留长度
state->mtu = mtu; // mtu,这里其实是指ip数据的最大长度
state->left = skb->len - hlen; // state->left记录着还有多少数据待分段,随着IP片段的创建发送会逐步减少
state->ptr = hlen; // state->ptr初始化为原始skb IP数据的起始位置,严格来讲,它不是一个指针,它是一个数据指针的偏移值,它用于指示该从原始skb的哪里开始拷贝数据,稍后创建新sk_buff时会从state->ptr所指示的位置处拷贝数据,每拷贝数据后,它会增加对应的长度
state->offset = (ntohs(iph->frag_off) & IP_OFFSET) << 3; // 我们可以从skb携带的IP头部得知该skb是否本身就是是个IP片段,state->offset代表着新分配sk_buff在整个IP报文中的偏移,每创建一个分段后,它会增加
state->not_last_frag = iph->frag_off & htons(IP_MF); // 我们可以从skb携带的IP头部得知该skb是否本身不是某个ip报文的最后一个分段,若不是,则not_last_frag为true, 则从它产生的分段也应该带上IP_MF
}
ip_frag_next函数
ip_frag_next
负责分配新sk_buff
并从原始sk_buff
拷贝数据,ip头部以及以及一些控制信息
struct sk_buff *ip_frag_next(struct sk_buff *skb, struct ip_frag_state *state)
{
unsigned int len = state->left;
// skb2将指向新分配的sk_buff
struct sk_buff *skb2;
// iph将指向新分配的sk_buff的IP头部
struct iphdr *iph;
// 1 这里会检查剩余的数据是否能放进一个分段中
len = state->left;
if (len > state->mtu)
len = state->mtu;
// 2 剩余的数据不能放进一个分段中,len取值state->mtu &= ~7, 因为中间的分段长度只能是8的倍数
if (len < state->left) {
len &= ~7;
}
// 3 经过上述两步计算,len值代表了需要拷贝的IP数据长度
// len加上需要预留的ip头部和链路层头部长度,分配sk_buff
skb2 = alloc_skb(len + state->hlen + state->ll_rs, GFP_ATOMIC);
if (!skb2)
return ERR_PTR(-ENOMEM);
// 4 从原始skb拷贝sk_buff结构体需要携带的控制信息以及路由信息
ip_copy_metadata(skb2, skb);
// 5 预留二层头部
skb_reserve(skb2, state->ll_rs);
// 6 修改新分配的sk_buff中记录报头的指针和偏移值
// 修改skb2->tail,skb2->len指针,此时的skb2->len代表着该sk_buf所携带IP片段的总长度(头部+数据)
skb_put(skb2, len + state->hlen);
// 记录网络层头部位置(即skb2->network_header)
skb_reset_network_header(skb2);
// 记录传输层头部所处位置(skb2->transport_header)
skb2->transport_header = skb2->network_header + state->hlen;
// 7 从原始skb拷贝ip头部到新sk_buff
skb_copy_from_linear_data(skb, skb_network_header(skb2), state->hlen);
// 8 从原始skb拷贝数据,skb_copy_bits的代码比较长,但其原理易懂,它会依次从原始skb的linear buffer区、paged buffer区(skb_shinfo(skb)->frags[])、frag_list上的sk_buff(从快速路径转过来的非法的sk_buff是携带了frag_list的)来拷贝数据,直到拷贝足量数据或者发生错误才返回
if (skb_copy_bits(skb, state->ptr, skb_transport_header(skb2), len))
BUG();
// 9 state->left自减len, 代表剩余数据减少
state->left -= len;
// 10 修改新sk_buff的IP头部
iph = ip_hdr(skb2);
// 将IP片段的offset值计算出来, 放入头部对应位置
iph->frag_off = htons((state->offset >> 3));
if (state->DF)
iph->frag_off |= htons(IP_DF); // 设置该IP片段是否允许被进一步分段的标志
// 若不是最后一个IP片段,则设置IP_MF
if (state->left > 0 || state->not_last_frag)
iph->frag_off |= htons(IP_MF);
// 11 更新state的state->ptr和state->offset,以便下一轮循环使用
state->ptr += len;
state->offset += len;
// 12 计算IP片段总长度
iph->tot_len = htons(len + state->hlen);
// 13 重新计算ip头部的校验值
ip_send_check(iph);
return skb2;
}
ip_frag_ipcb
ip_frag_ipcb
在ip_frag_next
分配完第一个sk_buff
后修改原始sk_buff
中携带的IP选项,将ip片段不需要携带的ip选项修改为“No Operation”
static void ip_frag_ipcb(struct sk_buff *from, struct sk_buff *to,
bool first_frag, struct ip_frag_state *state)
{
IPCB(to)->flags = IPCB(from)->flags;
// 这里和快速路径的处理是类似的,第一个分段拷贝了原始的头部后,
// 原始头部的option中有些只需在首个分段存在的option会被清除,以IPOPT_NOOP替代
if (first_frag)
ip_options_fragment(from);
}
参考文档
- https://www.rfc-editor.org/rfc/rfc791
- 《深入理解linux网络技术内幕》