ip 协议栈源码剖析第三节

继上一篇文章,我们继续探讨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

未完待续