Packet fragmentation and segmentation offload in UDP and VXLAN
skb模型
IP 数据包分片(fragment)时用到的 frag_list 模型:
分片的数据有各自的 skb 结构体,它们通过 skb->next 链接成一个单链表,表头是第一个 skb 的 shared_info 中的 frag_list。
GSO 进行分段(segmentation)用到的一种模型:
当一个大的 TCP 数据包被切割成几个 MTU 大小的数据时,它们也是通过 skb->next 链接到一起的:
与分片不同的是,相关的信息是记录在最前面一个 skb 的 skb_shared_info 里面的 gso_segs 和 gso_size
一种是网卡驱动常用的模型:
数据存放在物理页面的不同位置,skb_shared_info 里有一个数组,存放一组 (页面、偏移、大小) 的信息,用来记录这些数据
对于以太网,每个传输的数据帧的大小受限于PMTU,PMTU一般为1500字节(不包括L2 header本身)。当应用层下发的数据超过PMTU(严格来说是PMTU - L4 header - L3 header)时,就会在L4/L3进行分片,保证下发给NIC的数据不会超过PMTU。当然,对于TSO/GSO/UFO,情况又不太一样。我们先看看UDP的分片过程,从而更好的理解TSO/GSO。
UDP fragmentation
老的内核通常在IP层处理IP分段,IP层可以接收0~64KB的数据。因此,当数据IP packet大于PMTU时,就必须把数据分成多个IP分段。 较新的内核中,L4会尝试进行分段:L4不会再把超过PMTU的缓冲区直接传给IP层,而是传递一组和PMTU相匹配的缓冲区。这样,IP层只需要给每个分段增加IP报头。但是这并不意味着IP层就不做分段的工作了,一些情况下,IP层还会进行分段操作,见ip_fragment。
对于UDP,分片工作主要在ip_append_data(ip_apepend_page)中完成。
Memory allocation in skb_buff
ip_append_data会创建一个或者多个skb_buff对象,每个skb_buff表示一个IP packet。并根据下面两个因素决给skb分配的内存大小:
-
<1> MSG_MORE:如果socket设置了MSG_MORE,意味着应用层还有数据要发送,所以,可以分配大一点的缓冲区(PMTU),使得后续的数据可以合并到同一个skb。
-
<2>Scatter/Gather I/O:如果NIC支持SG,分段可以更有效的方式存储至内存页面(不用每次都分配PMTU大小缓冲区)。
if ((flags & MSG_MORE) &&
!(rt->u.dst.dev->features&NETIF_F_SG))
alloclen = mtu;
else
alloclen = datalen + fragheaderlen;
///最后一个分段,考虑trailer是否存在
if (datalen == length)
alloclen += rt->u.dst.trailer_len;
如果设置了MSG_MORE且设备不支持SG,则分配PMTU大小的skb,否则,分配只需要能够容纳当前数据大小的skb。
查看NIC是否支持SG IO:
# ethtool -k eth1|grep scatter
scatter-gather: on
tx-scatter-gather: on
tx-scatter-gather-fraglist: off [fixed]
- IP packet that does not need fragmentation, with IPsec
不需要分片的IP packet(IPsec)的skb的内存结构:
注意,对于IPsec,exthdrlen为IPsec header的长度,对于普通IP packet,exthdrlen为0。
- Fragmentation without Scatter/Gather I/O
如果设备不支持SG(Scatter/Gather I/O),会根据是否设置MSG_MORE,情况会不一样:
(1)No SG and No MSG_MORE
左下角的对象为ip_append_data需要处理的数据,length=x+y。由于长度(include L4 header)大于PMTU(严格来说是length+fraghdrlen > PMTU),会分成2个skb,第1个skb的大小为PMTU(include L3 header),第2个skb存储剩下的数据。
值得注意是第2个skb没有L4 header。
(2)No SG but MSG_MORE
与情况(1)的区别在于,由于设置了MSG_MORE,所以第2个skb仍然分配PMTU大小的空间。当再次调用ip_append_data的时候,会先将数据填充到第2个skb的剩余空间,然后再创建第3个skb(如果第2个skb空间不够)。
- Fragmentation with Scatter/Gather I/O
如果设备支持SG,skb->data指向的内存只会在SKB第一次填充数据时才会使用(skb->data指向的内存刚好能容下第一次调用ip_append_data的数据),接着调用ip_append_data的数据会写到专门分配的内存页中。当支持SG时,第二次调用ip_append_data时,数据如下存放:
当第二次调用ip_append_data时,数据(S1)会写到由frags指向的page中。S1不需要header:skb_buff实例中的所有数据分片(fragments)都属于同一个IP packet,这也意味着X+S1仍然小于PMTU。
每个skb_buff都有一个struct skb_shared_info的字段(可以通过skb_shinfo(skb)得到)。
//include/linux/skb_buff.h
struct skb_shared_info {
unsigned char nr_frags;///frags number
...
struct sk_buff *frag_list; ///IP packet所有分段(skb)链表,在从L4->L3时,内核会将一个IP packet的所有skb对象都加到该链表
...
/* must be last field, see pskb_expand_head() */
skb_frag_t frags[MAX_SKB_FRAGS]; ///page array
};
/* To allow 64K frame to be packed as single skb without frag_list we
* require 64K/PAGE_SIZE pages plus 1 additional page to allow for
* buffers which do not start on a page boundary.
*
* Since GRO uses frags we allocate at least 16 regardless of page
* size.
*/
#if (65536/PAGE_SIZE + 1) < 16
#define MAX_SKB_FRAGS 16UL
#else
#define MAX_SKB_FRAGS (65536/PAGE_SIZE + 1)
#endif
struct skb_frag_struct {
struct {
struct page *p;
} page;
__u16 page_offset;
__u16 size;
};
skb_shared_info->frags指向这些缓冲区,nr_frags记录有多少个缓冲区(一个缓冲区一个page)最多MAX_SKB_FRAGS个,基于一个IP packet最大64KB。
如果设备不支持SG,frags数组是不会使用的,内核会按PMTU给skb->data分配内存:
一个SKB可能包括多个skb_frag_struct,这些frags可能指向一个或者多个page。如下所示,SKB有2个frags,指向同一个page。
值得注意的是,SG与IP packet分段是互相独立的。SG IO只是让程序和硬件可以使用非相邻的内存区域,就像它们是相邻的那样。但是,每个IP分段必须受限于PMTU。也就是说,即使PAGE_SIZE大于PMTU,但是sk_buff的数据(skb->data所指)加上frags所引用的数据不能超过PMTU。一旦超过,就要创建新的skb_buff。
再记一遍,frags指向的分片与IP packet分段是两回事,只要设备支持SG,skb_buff就可能使用frags保存数据,
而IP packet的每个分段都对应一个skb_buff对象。
另外一个值得注意是skb_shared_info->frag_list,它表示IP packet所有分段(skb)链表,在从L4->L3时,内核会将一个IP packet的所有skb对象都加到该链表:
udp_push_pending_frames -> ip_finish_skb -> __ip_make_skb
/*
* Combined all pending IP fragments on the socket as one IP datagram
* and push them out.
*/
struct sk_buff *__ip_make_skb(struct sock *sk,
struct flowi4 *fl4,
struct sk_buff_head *queue,
struct inet_cork *cork)
{
...
if ((skb = __skb_dequeue(queue)) == NULL)
goto out;
tail_skb = &(skb_shinfo(skb)->frag_list);
/* move skb->data to ip header from ext header */
if (skb->data < skb_network_header(skb))
__skb_pull(skb, skb_network_offset(skb));
///move from sock->sk_write_queue to skb_shinfo(skb)->frag_list
while ((tmp_skb = __skb_dequeue(queue)) != NULL) {
__skb_pull(tmp_skb, skb_network_header_len(skb));
*tail_skb = tmp_skb;
tail_skb = &(tmp_skb->next);
skb->len += tmp_skb->len;
skb->data_len += tmp_skb->len;
skb->truesize += tmp_skb->truesize;
tmp_skb->destructor = NULL;
tmp_skb->sk = NULL;
}
...
UDP fragmentation example
发送1472字节的UDP数据,不会发生分片:
发送1473字节的时候,发生分片:
注意,第2个分片对应的frame的总长度为35字节,包括L2 header(14 bytes)、L3 header(20 bytes)、data(1 byte)。
发送过程如下:
几个注意点:
(1)2个分片会创建2个skb_buff对象
(2)__ip_make_skb将除第1个SKB的其它SKB加到第1个SKB的frag_list
(3)ip_fragment对frag_list中的每个SKB设置IP header
TCP fragmentation
每个TCP数据包(segment)的大小受MSS(TCP_MAXSEG选项)限制。最大报文段长度 ( MSS )表示 TCP 传往另一端的最大块数据的长度。当一个连接建立时(SYN packet), 连接的双方都要通告各自的MSS。
一般说来,如果没有分段发生, MSS还是越大越好。报文段越大允许每个报文段传送的数据就越多,相对IP和TCP首部有更高的网络利用率。当TCP发送一个SYN时,或者是因为一个本地应用进程想发起一个连接,或者是因为另一端的主机收到了一个连接请求,它能将MSS值设置为外出接口上的MTU长度减去固定的IP首部(20 bytes)和TCP首部长度(20 bytes)。对于一个以太网,MSS值可达1460字节。详细参考tcp_sendmsg。
TCP/SCTP会将数据按MTU进行切片,然后3层的工作只需要给传递下来的切片加上 ip头就可以了(也就是说调用这个函数的时候,其实4层已经切好片了)。所以ip_queue_xmit(TCP调用该函数将数据下发至L3)的实现非常简单。
Fragmentation in L3
一般来说,L4已经根据PMTU完成分片,L3只需要给每个分片加下IP header即可。如果skb的数据长度仍然超过PMTU,L3就会进行分片,保证每个分片的大小不超过PMTU:
int ip_output(struct sk_buff *skb)
{
IP_INC_STATS(IPSTATS_MIB_OUTREQUESTS);
///大于MTU(且不支持GSO),必须在IP层进行分片
if (skb->len > dst_pmtu(skb->dst) && !skb_shinfo(skb)->tso_size)
return ip_fragment(skb, ip_finish_output);
else
return ip_finish_output(skb);
}
///skbuff.h
static inline bool skb_is_gso(const struct sk_buff *skb)
{
return skb_shinfo(skb)->gso_size;
}
有几个地方值得注意:
-
(1) 第1个skb_buff->len等于skb->data、skb_shinfo(skb)->frags和skb_shinfo(skb)->frag_list中的所有数据之和。
-
(2) 对于UDP,只有NIC支持UFO,skb_shinfo(skb)->gso_size才会被设置:
static int __ip_append_data(struct sock *sk,
struct flowi4 *fl4,
struct sk_buff_head *queue,
struct inet_cork *cork,
struct page_frag *pfrag,
int getfrag(void *from, char *to, int offset,
int len, int odd, struct sk_buff *skb),
void *from, int length, int transhdrlen,
unsigned int flags)
{
...
if (((length > mtu) || (skb && skb_has_frags(skb))) && ///len(L4 header + user data) > mtu and UFO
(sk->sk_protocol == IPPROTO_UDP) &&
(rt->dst.dev->features & NETIF_F_UFO) && !rt->dst.header_len &&
(sk->sk_type == SOCK_DGRAM)) {///UDP offload
err = ip_ufo_append_data(sk, queue, getfrag, from, length,
hh_len, fragheaderlen, transhdrlen,
maxfraglen, flags);
if (err)
goto error;
return 0;
}
...
static inline int ip_ufo_append_data(struct sock *sk,
struct sk_buff_head *queue,
int getfrag(void *from, char *to, int offset, int len,
int odd, struct sk_buff *skb),
void *from, int length, int hh_len, int fragheaderlen,
int transhdrlen, int maxfraglen, unsigned int flags)
{
struct sk_buff *skb;
int err;
/* There is support for UDP fragmentation offload by network
* device, so create one single skb packet containing complete
* udp datagram
*/
if ((skb = skb_peek_tail(queue)) == NULL) {
skb = sock_alloc_send_skb