继上一篇文章,我们继续探讨linux内核网络子系统的源码剖析,上一篇中我们讲到了skb的聚合分散IO的知识点,现在继续讲解。
1. SKB之聚合分散IO
1.1 frag_list 成员变量
由上一篇文章的介绍,我们可以了解到,frag_list是skb_shared_info的成员变量,它的主要用途如下:
1) 用于IP报文的分片重组,将重组后的IP报文链接成一个链表,这个链表的组织就是靠frag_list这个成员。
2) 当输出的UDP报文较大时,这个时候需要进行分片,所以将分片的报文链接到第一个的SKB。
3) 用来支持FRAGLIST类型的数据包,如果网络设备支持该类型,就将skb挂到frag_list上。
1.2 数据存储在SKB中的几种方式
说起skb中存储数据的方式,不得不说一个linux内核的函数,即skb_is_nonlinear,这个函数用来判断skb中是否存在聚合分散IO的区域,实际上就是判断skb中的成员变量data_len是否为0,如果不为0,则说明skb存在聚合分散的IO区域。
如果你不想处理skb的聚合分散IO区域,可以使用skb_linearize把聚合分散IO区域的数据线性化到线性区域,也就是data所指的数据区域。
图1是一个数据全部存在了skb中的data区域,即线性区,从图中我们可以看到数据的总长度是Y,即skb中data指针和tail指针的差值,我们还可观察到,skb中的data_len为0,说明skb中不存在聚合分散IO的区域,可以观察到,nr_frags和frag_list都为0,又再一次的说明了skb不存在聚合分散的IO区域的数据。
图1
图2是skb中的包含聚合分散IO的区域,从图2中我们可以看到数据的总长度是len为Y+S1+S2,其中Y是skb中data指针和tail指针的差值,S1和S2是两个分片报文的长度,可知,两个分片报文的长度data_len为S1+S2,我们可以看到,skb_shared_info结构中的nr_frags为2,frag_list为NULL,这再次说明这个报文包含了聚合分散的IO数据,且分片数是2,这两个分片存在一个页面内,偏移分别为0和S1。
图2
图3是skb中的包含聚合分散IO的区域,从图3中我们可以看到数据的总长度是len为Y+S1,其中Y是skb中data指针和tail指针的差值,S1是FRAGLIST类型的数据的长度,可知,这个分片报文的长度data_len为S1,我们可以看到,skb_shared_info结构中的nr_frags为0,frag_list不为NULL,这再次说明这个报文包含了聚合分散的IO数据,且分片数是1,这这个分片挂在了frag_list的链表上。
图3
1.3 skb相关的函数介绍
我们知道,程序在运行时,现申请内存会影响性能,所以,正常在写程序时,如果碰到经常使用的结构,我们通常都会先申请一段内存池,当程序申请内存时,直接从内存池里拿数据,这样省去了现申请内存的消耗,那么linux内核在处理skb时,也是提供了一个内存池。
1.3.1 skb内存池相关的函数
\t void __init skb_init(void)
{
skbuff_head_cache = kmem_cache_create("skbuff_head_cache",
sizeof(struct sk_buff),
0,
SLAB_HWCACHE_ALIGN|SLAB_PANIC,
NULL);
skbuff_fclone_cache = kmem_cache_create("skbuff_fclone_cache",
(2*sizeof(struct sk_buff)) +
sizeof(atomic_t),
0,
SLAB_HWCACHE_ALIGN|SLAB_PANIC,
NULL);
}
以上的函数,创建了两个高速的缓存,skbuff_head_cache这个缓存主要用来分配一个skb时用的,一般情况下,都会用这个缓存,skbuff_fclone_cache这个缓存的用途是,当你要分配两倍的skb时,就用这个缓存,以增加分配的效率,例如,想要克隆一个skb时,建议skb从skbuff_fclone_cache进行分配,这样在克隆时,就不需要再次进行克隆了,而是直接使用后备的skb。
1.3.2 skb的分配函数
\t struct sk_buff *__alloc_skb(unsigned int size, gfp_t gfp_mask,
int fclone, int node)
{
struct kmem_cache *cache;
struct skb_shared_info *shinfo;
struct sk_buff *skb;
u8 *data;
cache = fclone ? skbuff_fclone_cache : skbuff_head_cache;
/* Get the HEAD */
skb = kmem_cache_alloc_node(cache, gfp_mask & ~__GFP_DMA, node);
if (!skb)
goto out;
size = SKB_DATA_ALIGN(size);
data = kmalloc_node_track_caller(size + sizeof(struct skb_shared_info),
gfp_mask, node);
if (!data)
goto nodata;
/*
* Only clear those fields we need to clear, not those that we will
* actually initialise below. Hence, don't put any more fields after
* the tail pointer in struct sk_buff!
*/
memset(skb, 0, offsetof(struct sk_buff, tail));
skb->truesize = size + sizeof(struct sk_buff);
atomic_set(&skb->users, 1);
skb->head = data;
skb->data = data;
skb_reset_tail_pointer(skb);
skb->end = skb->tail + size;
kmemcheck_annotate_bitfield(skb, flags1);
kmemcheck_annotate_bitfield(skb, flags2);
#ifdef NET_SKBUFF_DATA_USES_OFFSET
skb->mac_header = ~0U;
#endif
/* make sure we initialize shinfo sequentially */
shinfo = skb_shinfo(skb);
atomic_set(&shinfo->dataref, 1);
shinfo->nr_frags = 0;
shinfo->gso_size = 0;
shinfo->gso_segs = 0;
shinfo->gso_type = 0;
shinfo->ip6_frag_id = 0;
shinfo->tx_flags.flags = 0;
skb_frag_list_init(skb);
memset(&shinfo->hwtstamps, 0, sizeof(shinfo->hwtstamps));
if (fclone) {
struct sk_buff *child = skb + 1;
atomic_t *fclone_ref = (atomic_t *) (child + 1);
kmemcheck_annotate_bitfield(child, flags1);
kmemcheck_annotate_bitfield(child, flags2);
skb->fclone = SKB_FCLONE_ORIG;
atomic_set(fclone_ref, 1);
child->fclone = SKB_FCLONE_UNAVAILABLE;
}
out:
return skb;
nodata:
kmem_cache_free(cache, skb);
skb = NULL;
goto out;
}
1: 根据参数fclone的值,来确定从哪个高速缓存分配skb。
2: 调用kmem_cache_alloc_node从高速缓存中分配内存,大家可以看到,去掉了__GFP_DMA标志,目的是不从DMA区域分配内存,因为DMA区域比较小,主要用于特殊的用途,因此skb描述符的分配不从DMA区域分配。
3: 对size进行对齐。
4: 调用kmalloc_node_track_caller函数,对skb分配其数据的缓存区域,我们可以看到长度是size和sizeof(struct skb_shared_info)之和,因为在其末尾要留有skb_shared_info这么大的数据大小的区间,为聚合分散做准备。
5: 对skb的成员进行初始化。
6: 如果fclone置为1,则表示要申请两倍的skb,因此要将父skb的fclone置为SKB_FCLONE_ORIG,表示可以被克隆,child的skb被置为SKB_FCLONE_UNAVAILABLE,表示child还没有被利用。
最后创建的skb结构,如下图所示:
skb结构示意图
1.3.3 skb的空间预留
skb_reserve专门在数据区的前面预留一定的空间,主要的作用有两个,第一是,进行内存上的对齐,第二,需要在数据区的前面预留一定的空间,来插入协议的首部的部分。
例如:当skb从上层向下层传递时,会更新skb->data的指针,使得data指针向上移动,来添加协议的首部,如下图所示:
skb_reserve 示意图
1.3.4 skb_push 函数的作用
其实这个函数的作用就是在data区域加入各层报文的首部,例如,当要发送TCP的数据时,通常都会对data区域预留一个空间,预留空间的目的就是添加各层协议的首部,通常TCP预留的首部的大小是MAX_TCP_HEADER。
执行的流程如下图所示:
skb push 示意图1
未完待续