LINUX网络协议栈实现分析(-) SKBUFF的实现

时间:2021-01-07 11:04:16

LINUX网络协议栈实现分析(-)
SKBUFF的实现
本文是我尝试分析LINUX网络协议栈实现的一系列文章中的第一篇,主要分析
LINUX网络协议栈中SKBUFF的实现。分析以LINUX2.2.x为基础,同时也包
括了相同的描述对象在LINUX2.4.x中的新变化。本文引用的代码的版本分别
是:LINUX2.2.25,LINUX2.4.20。
1 简介
了解网络协议栈的人都知道,网络协议栈是一个有层次的软件结构,层与层之
间通过预定的接口传递网络报文。网络报文中包含了在协议栈各层使用到的各
种信息。网络报文的长度是不固定的,因此采用什么样的数据结构来存储这些
网络报文就显得非常重要。在BSD的实现中,采用的数据结构是mbuf,它所
能存储的数据的长度是固定的,如果一个网络报文需要多个mbuf,这些mbuf
链接成一个链表。所以同一个网络报文里的数据在内存中的存储可能是不连续
的。在LINUX的实现中,同一个网络报文的数据在内存中是连续存放的,每个
网络报文都有一个控制结构,叫做sk_buff。当然,这只是在LINUX2.2.x里面
的情况,sk_buff在LINUX2.4.x有一点变化,将会在下面讲到。
2 LINUX2.2.x中的SKBUFF
2.1 sk_buff的定义
前面提到,sk_buff是一个控制结构,通过它,才可以访问网络报文里的各种数
据。所以在分配网络报文存储空间时,同时也分配它的控制结构sk_buff。在这
个控制结构里,有指向网络报文的指针,也有描述网络报文的变量。下面是
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
{
PDF created with pdfFactory trial version www.pdffactory.com
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指针指向网络报文的结束地址。网络报文在存储空间里的存放的顺序
依次是:链路层的头部,网络层的头部,传输层的头部,传输层的数据。在协
PDF created with pdfFactory trial version www.pdffactory.com
议栈的不同层,sk_buff的指针data指向这一层的网络报文的头部。同时,在
sk_buff里,也有相关的数据结构来表示不同层头部信息。sk_buff和网络报文之
间的关系如图所示:
[图2.1 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)
PDF created with pdfFactory trial version www.pdffactory.com
释放控制结构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)
PDF created with pdfFactory trial version www.pdffactory.com
将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分片是将一个网络报文分成多个网络报文,这里是将一个网
络报文分成几片存放在不同的内存空间中)。一个大概的示意图如下:
[3.1 LINUX2.4.x的sk_buff与网络报文之间的关系]
图中的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的分配,复制和释放,
并且在需要的时候将分散存储的网络报文整合成一个连续存储的网络报文。具
体的函数可以参考源代码。
PDF created with pdfFactory trial version www.pdffactory.com
LINUX2.4.x中对sk_buff_head的操作与LINUX2.2.x基本相同,只是多加了一
个spinlock使队列可以在SMP的机器上更好地共享。具体地例子可以参考源代
码,在此不做赘述。
4 小结
网络报文的存储结构是实现网络协议栈的基础。网络报文在协议栈各层之间传
递,因此,如何快速地定位本层关心的数据,并尽量避免在处理时复制网络报
文成为提高协议栈性能的关键。本文分析了LINUX2.2.x和LINUX2.4.x中网络
报文的存储结构,以及对存储结构的操作。可以看到,在LINUX的协议栈实现
中,一般情况下只分配一个网络报文的存储空间,只要不修改网络报文的内
容,不同层或不同的处理函数都是通过控制结构sk_buff来共享这个网络报文
的。只有在需要修改此报文的情况下,才复制一份。这样即节约的存储空间也
方便了数据的定位,使得LINUX的网络协议栈的性能在应用中表现良好。
5 参考资料
1:《 TCP/IP详解卷2:实现》, Gray R. Wright,W.Richard Stevens,机械工
业出版社
2:Kernel Korner: Network Buffers and Memory Management,
www.linuxjournal.com
3:Linux IP Networking by Glenn Herrin
4:Building Into The Linux Network Layer,phrack55
PDF created with pdfFactory trial version www.pdffactory.com