本文以 linux 2.6.27.62 中 UDP 发包过程中重要的一个 IP 层的函数来分析 IP 层是如何分片的。
科普一下,什么是 IP 包分片,在某一个链路上,比如在以太网链路上,每次所能发送最大的包是有限制的,叫做 MTU,也就是 IP 层要想发包,每次包大小必须不大于 MTU,见上一篇文章,但传输层很有可能发送大于这个值的数据,此时 IP 层会对这些数据(可以称为 IP 包)进行分片,然后在收到时,在 IP 层再进行重组,形成一个 IP 包,交给传输层。
代码如下:
int ip_append_data(struct sock *sk,
int getfrag(void *from, char *to, int offset, int len,
int odd, struct sk_buff *skb),
void *from, int length, int transhdrlen,
struct ipcm_cookie *ipc, struct rtable *rt,
unsigned int flags)
{
struct inet_sock *inet = inet_sk(sk);
struct sk_buff *skb;
struct ip_options *opt = NULL;
int hh_len;
int exthdrlen;
int mtu;
int copy;
int err;
int offset = 0;
unsigned int maxfraglen, fragheaderlen;
int csummode = CHECKSUM_NONE;
// 如果只是为了探测,则不发包,直接返回
if (flags&MSG_PROBE)
return 0;
// 检查发送队列是否为空,如果为空,则表示这是 IP 包的第一个分片
if (skb_queue_empty(&sk->sk_write_queue)) {
/*
* setup for corking.
*/
opt = ipc->opt;
if (opt) {
if (inet->cork.opt == NULL) {
inet->cork.opt = kmalloc(sizeof(struct ip_options) + 40, sk->sk_allocation);
if (unlikely(inet->cork.opt == NULL))
return -ENOBUFS;
}
memcpy(inet->cork.opt, opt, sizeof(struct ip_options)+opt->optlen);
inet->cork.flags |= IPCORK_OPT;
inet->cork.addr = ipc->addr;
}
dst_hold(&rt->u.dst);
inet->cork.fragsize = mtu = inet->pmtudisc == IP_PMTUDISC_PROBE ?
rt->u.dst.dev->mtu :
dst_mtu(rt->u.dst.path);
inet->cork.dst = &rt->u.dst;
inet->cork.length = 0;
sk->sk_sndmsg_page = NULL;
sk->sk_sndmsg_off = 0;
if ((exthdrlen = rt->u.dst.header_len) != 0) {
length += exthdrlen;
transhdrlen += exthdrlen;
}
} else {
rt = (struct rtable *)inet->cork.dst;
if (inet->cork.flags & IPCORK_OPT)
opt = inet->cork.opt;
transhdrlen = 0; // 如果该 IP 包里还有分片,那么就会忽略掉此次的传输层头信息,直接添加到上一个 IP 包
exthdrlen = 0;
mtu = inet->cork.fragsize;
}
// 计算 L2 层头部长度,即链路层,以太网为 1500
hh_len = LL_RESERVED_SPACE(rt->u.dst.dev);
// 计算该层,即 IP 层头部长度
fragheaderlen = sizeof(struct iphdr) + (opt ? opt->optlen : 0);
// 计算该分片,如果不是最后一片,那么它的载荷最大为多少,8 字节对齐的原因,见上一篇《IP层分析》一文
// maxfraglen 表示如果不是最后一个分片的分片的最大长度
maxfraglen = ((mtu - fragheaderlen) & ~7) + fragheaderlen;
// 该 IP 包的载荷是否超过了最大限制,总大小为什么是 0xFFFF,见上一文
if (inet->cork.length + length > 0xFFFF - fragheaderlen) {
ip_local_error(sk, EMSGSIZE, rt->rt_dst, inet->dport, mtu-exthdrlen);
return -EMSGSIZE;
}
/*
* transhdrlen > 0 means that this is the first fragment and we wish
* it won't be fragmented in the future.
*/
if (transhdrlen &&
length + fragheaderlen <= mtu &&
rt->u.dst.dev->features & NETIF_F_V4_CSUM &&
!exthdrlen)
csummode = CHECKSUM_PARTIAL; // 让硬件,即网卡计算校验和
// 更新该 IP 包已累积的数据的长度,cork 相当于软木塞,使小的数据包可以积累成为一个大的 IP 包
inet->cork.length += length;
if (((length> mtu) || !skb_queue_empty(&sk->sk_write_queue)) &&
(sk->sk_protocol == IPPROTO_UDP) &&
(rt->u.dst.dev->features & NETIF_F_UFO)) {
err = ip_ufo_append_data(sk, getfrag, from, length, hh_len,
fragheaderlen, transhdrlen, mtu,
flags);
if (err)
goto error;
return 0;
}
/* So, what's going on in the loop below?
*
* We use calculated fragment length to generate chained skb,
* each of segments is IP fragment ready for sending to network after
* adding appropriate IP header.
*/
// 取出该 IP 包的最后一个 sk_buff,即最后一个分片
if ((skb = skb_peek_tail(&sk->sk_write_queue)) == NULL)
goto alloc_new_skb;
// 下面开始 IP 分片的主逻辑
while (length > 0) {
/* Check if the remaining data fits into current packet. */
// 检查最后一个分片的剩余空间是否可以满足当前的包,最后一个分片的大小因为不需要满足 8 字节对齐
// 所以它的大小有可能,大于 maxfraglen,但肯定小于 mtu. 所以如果 copy 如果可以满足 length,那么
// 就不用申请新的分片,直接填充到最后一个分片中。但如果 copy 不能满足 length (copy < length),
// 那么就需要新和分片,此时上次的最后一个分片的大小就需要做 8 字节对齐处理。所以 copy 记录了能够从
// length 中拷贝的数据的大小
copy = mtu - skb->len;
if (copy < length)
copy = maxfraglen - skb->len;
// 如果最后一个分片不能满足此次请求,并且 skb->len >= maxfraglen时,此时 copy <= 0, 也就是最后一个
// 分片有可能需要作处理,移动最后没有 8 字节对齐的部分
if (copy <= 0) {
char *data;
unsigned int datalen;
unsigned int fraglen;
unsigned int fraggap;
unsigned int alloclen;
struct sk_buff *skb_prev;
alloc_new_skb:
// 取出上一个分片,因为上一个分片在处理时有可能被当作最后一个分片处理,长度可能不是 8 的倍数,此处要处理这种情况
skb_prev = skb;
if (skb_prev)
fraggap = skb_prev->len - maxfraglen; // 计算最后一个分片是否需要做字节对齐处理
else
fraggap = 0;
/*
* If remaining data exceeds the mtu,
* we know we need more fragment(s).
*/
// 计算需要拷贝到新的分片中的数据长度
datalen = length + fraggap;
// 如果不能当作最后一个分片全部处理掉,那么说明还需要更多的分片,此时将要新申请的分片就需要做对齐处理了
if (datalen > mtu - fragheaderlen)
datalen = maxfraglen - fragheaderlen;
// 将要填充的分片的长度
fraglen = datalen + fragheaderlen;
if ((flags & MSG_MORE) &&
!(rt->u.dst.dev->features&NETIF_F_SG))
alloclen = mtu;
else
alloclen = datalen + fragheaderlen;
/* The last fragment gets additional space at tail.
* Note, with MSG_MORE we overallocate on fragments,
* because we have no idea what fragment will be
* the last.
*/
// 如果可以在新的分片中全部处理掉,即不需要更多的分片,将作为最后一个分片处理
if (datalen == length + fraggap)
alloclen += rt->u.dst.trailer_len;
// 如果是第一个分片
if (transhdrlen) {
skb = sock_alloc_send_skb(sk,
alloclen + hh_len + 15,
(flags & MSG_DONTWAIT), &err);
} else {
skb = NULL;
if (atomic_read(&sk->sk_wmem_alloc) <=
2 * sk->sk_sndbuf)
skb = sock_wmalloc(sk,
alloclen + hh_len + 15, 1,
sk->sk_allocation);
if (unlikely(skb == NULL))
err = -ENOBUFS;
}
if (skb == NULL)
goto error;
/*
* Fill in the control structures
*/
skb->ip_summed = csummode;
skb->csum = 0;
// 保留 L2 层,即链路层长度,该保留动作不会影响 skb->len, skb->len 只记录了 IP 层数据的长度,包括 IP 头信息
skb_reserve(skb, hh_len);
/*
* Find where to start putting bytes.
*/
data = skb_put(skb, fraglen);
skb_set_network_header(skb, exthdrlen);
skb->transport_header = (skb->network_header +
fragheaderlen);
data += fragheaderlen;
// 处理上次最后一个分片中需要字节对齐的部分
if (fraggap) {
skb->csum = skb_copy_and_csum_bits(
skb_prev, maxfraglen,
data + transhdrlen, fraggap, 0);
skb_prev->csum = csum_sub(skb_prev->csum,
skb->csum);
data += fraggap;
pskb_trim_unique(skb_prev, maxfraglen);
}
// 计算能够从用户数据中拷贝的字节数,如果是第一个分片,传进来的载荷其实是包含传输层头大小的
copy = datalen - transhdrlen - fraggap;
// 拷贝到新的分片中
if (copy > 0 && getfrag(from, data + transhdrlen, offset, copy, fraggap, skb) < 0) {
err = -EFAULT;
kfree_skb(skb);
goto error;
}
// 计算偏移
offset += copy;
// 计算此次处理掉的用户数据的字节数,datalen 是可能包含传输头信息的,传输头也相当于被处理掉了
length -= datalen - fraggap;
transhdrlen = 0;
exthdrlen = 0;
csummode = CHECKSUM_NONE;
/*
* Put the packet on the pending queue.
*/
__skb_queue_tail(&sk->sk_write_queue, skb);
continue;
}
// 如果最后一个分片能够满足请求
if (copy > length)
copy = length;
// 如果不支持离散聚合 I/O
if (!(rt->u.dst.dev->features&NETIF_F_SG)) {
unsigned int off;
// 拷贝传输层的数据到分片中
off = skb->len;
if (getfrag(from, skb_put(skb, copy),
offset, copy, off, skb) < 0) {
__skb_trim(skb, off);
err = -EFAULT;
goto error;
}
} else {
int i = skb_shinfo(skb)->nr_frags;
skb_frag_t *frag = &skb_shinfo(skb)->frags[i-1];
struct page *page = sk->sk_sndmsg_page;
int off = sk->sk_sndmsg_off;
unsigned int left;
if (page && (left = PAGE_SIZE - off) > 0) {
if (copy >= left)
copy = left;
if (page != frag->page) {
if (i == MAX_SKB_FRAGS) {
err = -EMSGSIZE;
goto error;
}
get_page(page);
skb_fill_page_desc(skb, i, page, sk->sk_sndmsg_off, 0);
frag = &skb_shinfo(skb)->frags[i];
}
} else if (i < MAX_SKB_FRAGS) {
if (copy > PAGE_SIZE)
copy = PAGE_SIZE;
page = alloc_pages(sk->sk_allocation, 0);
if (page == NULL) {
err = -ENOMEM;
goto error;
}
sk->sk_sndmsg_page = page;
sk->sk_sndmsg_off = 0;
skb_fill_page_desc(skb, i, page, 0, 0);
frag = &skb_shinfo(skb)->frags[i];
} else {
err = -EMSGSIZE;
goto error;
}
if (getfrag(from, page_address(frag->page)+frag->page_offset+frag->size, offset, copy, skb->len, skb) < 0) {
err = -EFAULT;
goto error;
}
sk->sk_sndmsg_off += copy;
frag->size += copy;
skb->len += copy;
skb->data_len += copy;
skb->truesize += copy;
atomic_add(copy, &sk->sk_wmem_alloc);
}
offset += copy;
length -= copy;
}
return 0;
error:
inet->cork.length -= length;
IP_INC_STATS(sock_net(sk), IPSTATS_MIB_OUTDISCARDS);
return err;
}
通过代码分析,我们不难发现,在使用 UDP 传输时,由于有 cork 操作的存在(该操作可由用户控制),当频繁发送小数据时,会累积成一个 IP 包,当发送大数据时,如果不超过 IP 层所能接受的最大长度,则 IP 层会对它进行分片,并且很有可能与上一个 IP 包粘连。
Remark: IP 层的一些特性,为什么中间分片需要 8 字节对齐,可参见上一篇中 <<IP层分析>>,最后一个分片是不需要 8 字节对齐的,在代码中为了处理这些情况,有非常多的逻辑。还有,此次请求的最后一个分片,很有可能在下次请求时,为了利用上一次的 IP 包,从而使得本来的最后分片,成为了中间分片,进而必须处理掉分片中的字节对齐的情况。 sk_buff 是个重要的结构,它是用来描述 IP 包中的 IP 分片的信息的。因为对 IP 层来讲,它只关心自己的头信息和载荷,但一般能组成一个 UDP 包的载荷其实只需要一个 UDP 头,所以一般只有第一个分片中带有 UDP 头,其它分片中不用传输层的头了,也就相当于把多个传输层的包合并了。
这也相当于在网络层去关心传输层的一个特例了吧。