一. SKB_BUFF的基本概念
1. 一个完整的skb buff组成
(1) struct sk_buff--用于维护socket buffer状态和描述信息
(2) header data--独立于sk_buff结构体的数据缓冲区,用来存放报文分组,使各层协议的header存储在连续的空间中,以方便协议栈对其操作
(3) struct skb_shared_info --作为header data的补充,用于存储ip分片,其中sk_buff *frag_list是一系列子skbuff链表,而frag[]是由一组单独的page组成的数据缓冲区
skb buff结构图如下:
struct skb_buff
表示接收或发送数据包的包头信息,其成员变量在从一层向另一层传递时会发生修改。例如L3向L2传递前,会添加一个L3的头部,所以在添加头部前调用skb_reserve在缓冲区的头部给协议头预留一定的空间;L2向L3传递时候,L2的头部只有在 网络驱动处理L2的协议时有用,L3是不会关心它的信息的。但是,内核不会把L2的头部从缓冲区中删除,
sk_buff->h
sk_buff->nh
sk_buff->mac
指向TCP/IP各层协议头的指针:h指向L4(传输层),nh指向L3(网络层),mac指向L2(数据链路层)。每个指针的类型都是一个联合, 包含多个数据结构,
sk_buff->head
sk_buff->data
sk_buff->tail
sk_buff->end
表示缓冲区和数据部分的边界。在每一层申请缓冲区时,它会分配比协议头或协议数据大的空间。head和end指向缓冲区的头部和尾部,而data和 tail指向实际数据的头部和尾部。每一层会在head和data之间填充协议头,或者在tail和end之间添加新的协议数据。数据部分会在尾部包含一 个附加的头部。
下图是TCP(L4)向下发送数据给链路层L2的过程。注意skb_buff->data在从L4向L2穿越过程中的变化
几个len的区别?
(1)sk_buff->len:表示当前协议数据包的长度。它包括主缓冲区中的数据长度(data指针指向它)和分片中的数据长度。比如,处在网络层,len指的是ip包的长度,如果包已经到了应用层,则len是应用层头部和数据载荷的长度。
(2)sk_buff->data_len: data_len只计算分片中数据的长度,即skb_shared_info中有效数据总长度(包括frag_list,frags[]中的扩展数据),一般为0
(3)sk_buff->truesize:这是缓冲区的总长度,包括sk_buff结构和数据部分。如果申请一个len字节的缓冲区,alloc_skb函数会把它初始化成len+sizeof(sk_buff)。当skb->len变化时,这个变量也会变化。
通常,Data Buffer 只是一个简单的线性 buffer,这时候 len 就是线性 buffer 中的数据长度;
但在有 ‘paged data’ 情况下, Data Buffer 不仅包括第一个线性 buffer ,还包括多个 page buffer;这种情况下, ‘data_len’ 指的是 page buffer 中数据的长度,’len’ 指的是线性 buffer 加上 page buffer 的长度;len – data_len 就是线性 buffer 的长度。
二. sk_buff结构操作函数
内核通过alloc_skb()和dev_alloc_skb()为套接字缓存申请内存空间。这两个函数的定义位于net/core/skbuff.c文件内。通过这alloc_skb()申请的内存空间有两个,一个是存放实际报文数据的内存空间,通过kmalloc()函数申请;一个是sk_buff数据结构的内存空间,通过 kmem_cache_alloc()函数申请。dev_alloc_skb()的功能与alloc_skb()类似,它只被驱动程序的中断所调用,与alloc_skb()比较只是申请的内存空间长度多了16个字节。
内核通过kfree_skb()和dev_kfree_skb()释放为套接字缓存申请的内存空间。dev_kfree_skb()被驱动程序使用,功能与kfree_skb()一样。当skb->users为1时kfree_skb()才会执行释放内存空间的动作,否则只会减少skb->users的值。skb->users为1表示已没有其他用户使用该缓存了。
skb_reserve()函数为skb_buff缓存结构预留足够的空间来存放各层网络协议的头信息。该函数在在skb缓存申请成功后,加载报文数据前执行。在执行skb_reserve()函数前,skb->head,skb->data和skb->tail指针的位置的一样的,都位于skb内存空间的开始位置。这部份空间叫做headroom。有效数据后的空间叫tailroom。skb_reserve的操作只是把skb->data和skb->tail指针向后移,但缓存总长不变。
sk_buff的定义,依次注释如下:
struct sk_buff {
struct sk_buff * next;
struct sk_buff * prev;
struct sk_buff_head * list;
以上三个变量将sk_buff链接到一个双向循环链表中,链表的结构会在后面讲
到。
struct sock *sk;
此报文所属的sock结构,此值在本机发出的报文中有效,从网络设备收到的报
文此值为空。
struct timeval stamp; //此报文收到时的时间
struct device *dev; //收到此报文的网络设备
union
{
struct tcphdr *th;
struct udphdr *uh;
struct icmphdr *icmph;
struct igmphdr *igmph;
struct iphdr *ipiph;
struct spxhdr *spxh;
unsigned char *raw;
} h;
union
{
struct iphdr *iph;
struct ipv6hdr *ipv6h;
struct arphdr *arph;
struct ipxhdr *ipxh;
unsigned char *raw;
} nh;
union
{
struct ethhdr *ethernet;
unsigned char *raw;
} mac;
以上三个union结构依次是传输层,网络层,链路层的头部结构指针。这些指
针在网络报文进入这一层时被赋值,其中raw是一个无结构的字符指针,用于
扩展的协议。
struct dst_entry *dst; //此报文的路由,路由确定后赋此值
char cb[48]; //用于在协议栈之间传递参数,参数内容的涵义由
使用它的函数确定。
unsigned int len;
此报文的长度,这是指网络报文在不同协议层中的长度,包括头部和数据。在
协议栈的不同层,这个长度是不同的。
unsigned char is_clone,
cloned,
以上两个变量描述此控制结构是否是clone的控制结构。一个网络报文可以对
应多个控制结构,其中只有一个是原始的结构,其他的都是clone出来的。由
于可能存在多个控制结构,所以在释放网络报文时要确定它所有的控制结构都
已被释放。
pkt_type,
网络报文的类型,常见的有PACKET_HOST,代表发给本机的报文;还有
PACKET_OUTGOING,代表本机发出的报文。
unsigned short protocol; //链路层协议
unsigned int truesize; //此报文存储区的长度,这个长度是16字节
对齐的,一般要比报文的长度大。
unsigned char *head;
unsigned char *data;
unsigned char *tail;
unsigned char *end;
以上四个变量指向此报文存储区,具体的涵义后面会解释。
__u32 fwmark; //防火墙在报文中做的标记
};
网络报文的存储空间是在网络设备收到网络报文或者应用程序发送数据时分配
的,分配的空间以16字节对齐。分配成功之后,将网络报文填充到这个存储空
间中去。填充时先在存储空间的头部预留了一定数量的空隙,然后将网络报文
放到剩余的空间中去。但是网络报文不一定填满整个存储空间,有可能在存储
空间的后部还有一定数量的空隙,所以sk_buff里面的head指针指向存储空间
的起始地址,end指针指向存储空间的结束地址,data指针指向网络报文的起始
地址,tail指针指向网络报文的结束地址。网络报文在存储空间里的存放的顺序
依次是:链路层的头部,网络层的头部,传输层的头部,传输层的数据。在协
议栈的不同层,sk_buff的指针data指向这一层的网络报文的头部。同时,在
sk_buff里,也有相关的数据结构来表示不同层头部信息。sk_buff和网络报文之
间的关系如图所示:
(注:控制结构sk_buff和网络报文的存储空间是从两个不同的缓存中分配的,
所以它们在内存中不是连续存放的。在参考资料里也有一个关于sk_buff和网络
报文之间的关系的一个图,但是不要误解它们在内存中是连续存放的)
2.2 与 sk_buff相关的函数
与sk_buff相关的函数涉及到网络报文存储结构和控制结构的分配、复制、释
放,以及控制结构里的各指针的操作,还有各种标志的检查。重要的函数说明
如下:
struct sk_buff *alloc_skb(unsigned int size,int gfp_mask)
分配大小为size的存储空间存放网络报文,同时分配它的控制结构。size的值
是16字节对齐的,gfp_mask是内存分配的优先级。常见的内存分配优先级有
GFP_ATOMIC,代表分配过程不能被中断,一般用于中断上下文中分配内存;
GFP_KERNEL,代表分配过程可以被中断,相应的分配请求被放到等待队列
中。分配成功之后,因为还没有存放具体的网络报文,所以sk_buff的data,
tail指针都指向存储空间的起始地址,len的大小为0,而且is_clone和cloned两
个标记的值都是0。
struct sk_buff *skb_clone(struct sk_buff *skb, int gfp_mask)
从控制结构skb中clone出一个新的控制结构,它们都指向同一个网络报文。
clone成功之后,将新的控制结构和原来的控制结构的is_clone,cloned两个标
记都置位。同时还增加网络报文的引用计数(这个引用计数存放在存储空间的
结束地址的内存中,由函数atomic_t *skb_datarefp(struct sk_buff *skb)访问,引
用计数记录了这个存储空间有多少个控制结构)。由于存在多个控制结构指向
同一个存储空间的情况,所以在修改存储空间里面的内容时,先要确定这个存
储空间的引用计数为1,或者用下面的拷贝函数复制一个新的存储空间,然后
才可以修改它里面的内容。
struct sk_buff *skb_copy(struct sk_buff *skb, int gfp_mask)
复制控制结构skb和它所指的存储空间的内容。复制成功之后,新的控制结构
和存储空间与原来的控制结构和存储空间相对独立。所以新的控制结构里的
is_clone,cloned两个标记都是0,而且新的存储空间的引用计数是1。
void kfree_skb(struct sk_buff *skb)
释放控制结构skb和它所指的存储空间。由于一个存储空间可以有多个控制结
构,所以只有在存储空间的引用计数为1的情况下才释放存储空间,一般情况
下,只释放控制结构skb。
unsigned char *skb_put(struct sk_buff *skb, unsigned int len)
将tail指针下移,并增加skb的len值。data和tail之间的空间就是可以存放网
络报文的空间。这个操作增加了可以存储网络报文的空间,但是增加不能使tail
的值大于end的值,skb的len值大于truesize的值。
unsigned char *skb_push(struct sk_buff *skb, unsigned int len)
将data指针上移,并增加skb的len值。这个操作在存储空间的头部增加了一
段可以存储网络报文的空间,上一个操作在存储空间的尾部增加了一段可以存
储网络报文的空间。但是增加不能使data的值小于head的值,skb的len值大
于truesize的值。
unsigned char * skb_pull(struct sk_buff *skb, unsigned int len)
将data指针下移,并减小skb的len值。这个操作使data指针指向下一层网络
报文的头部。
void skb_reserve(struct sk_buff *skb, unsigned int len)
将data指针和tail指针同时下移。这个操作在存储空间的头部预留len长度的空
隙。
void skb_trim(struct sk_buff *skb, unsigned int len)
将网络报文的长度缩减到len。这个操作丢弃了网络报文尾部的填充值。
int skb_cloned(struct sk_buff *skb)
判断skb是否是一个clone的控制结构。如果是clone的,它的cloned标记是
1,而且它指向的存储空间的引用计数大于1。
2.3 sk_buff_head的定义
在网络协议栈的实现中,有时需要把许多网络报文放到一个队列中做异步处
理。LINUX 为此定义了相关的数据结构sk_buff_head。这是一个双向链表的
头,它把sk_buff链接成一个双向链表,如图:
[图2.2 sk_buff_head与sk_buff的关系]
2.4 与 sk_buff_head相关的函数
与链表相关的函数,其功能无非是添加,删除链表上的节点,重要的函数说明
如下:
void skb_queue_head(struct sk_buff_head *list, struct sk_buff *newsk)
将newsk加到链表list的头部。
void skb_queue_tail(struct sk_buff_head *list, struct sk_buff *newsk)
将newsk加到链表list的尾部。
struct sk_buff *skb_dequeue(struct sk_buff_head *list)
从链表list的头部取下一个sk_buff。
struct sk_buff *skb_dequeue_tail(struct sk_buff_head *list)
从链表list的尾部取下一个sk_buff。
skb_insert(struct sk_buff *old, struct sk_buff *newsk)
将newsk加到old所在的链表上,并且newsk在old的前面。
void skb_append(struct sk_buff *old, struct sk_buff *newsk)
将newsk加到old所在的链表上,并且newsk在old的后面。
void skb_unlink(struct sk_buff *skb)
将skb从它所在的链表上取下。
以上的链表操作都是先关中断的。这在中断上下文中是不需要的,所以另外有
一套与上面函数同名但是有前缀“__”的函数供运行在中断上下文中的函数调
用。
3 LINUX2.4.x中的SKBUFF
LINUX2.4.x中的网络报文在内存中不一定是连续存储的,同一个网络报文有可
能被分成几片存放在内存的不同位置,这一点与LINUX2.2.x不同(注意不要和
IP的分片混淆,IP分片是将一个网络报文分成多个网络报文,这里是将一个网
络报文分成几片存放在不同的内存空间中)。
frags是一个数组,frag_list是一个单向链表。它们所指向的存储空间是
一个页的大小(即4k)。这些额外的存储空间并不是一开始就使用的,只有在
data所指的存储空间不够用的情况下才使用这些存储空间。以页为单位划分的
存储空间有利于和用户空间的程序共享这一块内存的数据。
为了记录网络报文的长度,在sk_buff里增加了一个变量data_len。这个变量记
录的是在frags和frag_list里面存储的报文的长度。原有的变量len记录网络报
文的总长度。truesize是head所指的存储区的大小。
LINUX2.2.x里分配,复制,释放sk_buff以及存储区的函数在LINUX2.4.x中的
涵义没有变化,只是在操作时增加了对frags和frag_list的分配,复制和释放,
并且在需要的时候将分散存储的网络报文整合成一个连续存储的网络报文。具
体的函数可以参考源代码。
LINUX2.4.x中对sk_buff_head的操作与LINUX2.2.x基本相同,只是多加了一
个spinlock使队列可以在SMP的机器上更好地共享。具体地例子可以参考源代
码,在此不做赘述。
4 小结
网络报文的存储结构是实现网络协议栈的基础。网络报文在协议栈各层之间传
递,因此,如何快速地定位本层关心的数据,并尽量避免在处理时复制网络报
文成为提高协议栈性能的关键。本文分析了LINUX2.2.x和LINUX2.4.x中网络
报文的存储结构,以及对存储结构的操作。可以看到,在LINUX的协议栈实现
中,一般情况下只分配一个网络报文的存储空间,只要不修改网络报文的内
容,不同层或不同的处理函数都是通过控制结构sk_buff来共享这个网络报文
的。只有在需要修改此报文的情况下,才复制一份。这样即节约的存储空间也
方便了数据的定位,使得LINUX的网络协议栈的性能在应用中表现良好。