Linux 内核中实现了大量简单实用的函数来操作sk_buff的数据域和管理sk_buff的存放队列。内核提供这些操作函数的目的是使对sk_buff的操作与TCP/IP协议栈的实现相独立。以后无论是对sk_buff 进行扩展还是在TCP/IP协议栈中实现了新的网络协议实例,原来的代码无须修改,只需扩展sk_buff的操作API就可实现前后操作的一致性。
在下面几种情况下需要应用sk_buff的操作函数:开发网络设备驱动程序时;在TCP/IP协议栈基础上开发新的网络协议实例时。
这些函数从其功能上可以划分为以下几类。
● 创建、释放和复制 Socket Buffer。
● 操作sk_buff结构中的参数和指针。
● 管理Socket Buffer的队列。
文件路径
sk_buff操作函数的实现集中在两个源文件中:
net/core/skbuff.c 和include/linux/skbuff.h。
说明: 在阅读Linux的源代码时,你可以看到很多同名函数有两个版本,一个是不带下画线的如do_something,一个是带双下画线的如__do_something。带双下画线的函数是实际的功能实现裸函数,不带下画线的函数通常是带双下画线函数的包装函数。包装函数在裸函数基础上增加了安全检查、实现防止并发访问的锁定机制等功能,或向实现函数传送正确的参数,建议一般不要直接调用带双下画线函数。大部分包装函数定义在include/linux/skbuff.h头文件中,而裸函数定义在net/core/skbuff.c源文件中。
创建 Socket Buffer比常规内存分配复杂。在高速网络的环境下,每秒有几千个网络数据包接收/发送,需要频繁地创建/释放 Socket Buffer。如果设计Socket Buffer的创建/释放过程不合理,将大大降低整个系统的性能。尤其内存分配是系统最耗时的一个操作。
为此内核在系统初始化时已创建了两个sk_buff的内存对象池。
● skbuff_head_cache
● skbuff_fclone_cache
这两个对象池是由skb_init创建的(net/core/skbuff.c)。每当需要为sk_buff数据结构分配内存时,根据所需的sk_buff 是克隆的还是非克隆的,分别从以上两个cache中获取内存对象,释放sk_buff时也就将对象放回以上两个cache。
Linux内核中实现的各种创建Socket Buffer的函数如表2-5所示。
表2-5 创建Socket Buffer的函数列表
__alloc_skb是为Socket Buffer分配内存的主要函数。创建Socket Buffer需考虑的主要因素有:应从哪个cache中获取内存对象; __alloc_skb会在哪里调用; Socket Buffer的数据结构两个实体单独分配。对以上因素的处理都体现在__alloc_skb的传入参数上,如下。
● size:存放数据包需要的内存空间的大小。
● gfp_mask:做内存分配时的标志。该标志描述了__alloc_skb函数的运行特点,即可否被别的进程中断。
◆ GFP _ATOMIC:在中断处理程序中申请内存,gfp-mask标志必须为该值,因为中断不能休眠。
◆ GFP_KERNEL:表示常规内核函数申请内存分配。
◆ 其他值。
● fclone:是否对该sk_buff克隆。其值决定了从哪一个sk_buff内存对象池中获取sk_buff数据结构所需的内存空间。
◆ 0:skbuff_head_cache
◆ 1:skbuff_fclone_cache
__alloc_skb完成内存分配的流程如图2-7所示。
图2-7 __alloc_skb创建Socket Buffer的实现流程
Socket Buffer创建结果如图2-8所示。
alloc_skb_fclone
我们在为Socket Buffer分配内存时,应调用它的两个包装函数,它们将为__alloc_skb传递正确的参数。
__dev_alloc_skb
图2-8 分配Socket Buffer
__dev_alloc_skb是给网络设备驱动程序使用的函数,当网络设备从网络上收到一个数据包时,它调用该函数向系统申请缓冲区来存放数据包,它是alloc_skb的包装函数,在其中调用了alloc_skb来分配Socket Buffer。
为了提高效率,__dev_alloc_skb为数据链路层在数据包缓冲区前预留了16个字节的headroom,(NET_SKB_PAD,其值为16),以避免头信息增长时原空间不够而重新分配内存空间造成额外系统开销。
现代网络设备数据链路层大多数使用的是以太网协议,协议头的长度为14个字节,预留16个字节是为了保证数据在2 n 的边界对齐。
dev_alloc_skb是__dev_alloc_skb的包装函数。由于网络数据包是在中断处理程序中接收的,中断不能休眠,dev_alloc_skb用GFP_ATOMIC标志传给__dev_alloc_skb来分配内存。
netdev_alloc_skb
__netdev_alloc_skb与__dev_alloc_skb函数类似,它为指定了某个接收数据包的网络设备分配Socket Buffer的内存空间。因此,在返回分配的sk_buff之前,初始化了sk_buff的设备指针*dev域。同样netdev_alloc_skb包装函数也是在接收数据包的中断处理程序中调用的,要用GFP_ATOMIC选项来申请内存分配。
2.释放Socket Buffer
● kfreeskb_函数用于释放Socket Buffer的内存。先前分配的skbuff_会返回到内存对象池(cache)中。对Socket Buffer 内存的释放操作比常规内存对象释放操作更复杂,需要考虑以下情形:
● kfreeskb___只有在skbuff的引用计数(skb->users)为1时才会完全释放skbuff 数据结构本身(即没有进程使用该数据结构了),否则该函数只做对skb->users数据域的减1操作。
例如,如果当前有3个进程在使用该sk_buff,只有第3次调用kfree_skb的时候才会最后释放该sk_buff。图2-9给出了释放Socket Buffer的所有步骤。
● sk_buff数据结构中还包含指向其他数据结构的指针,如路由表、连接跟踪等,所以它可能持有对其他数据结构的引用计数,比如对struct dst_entry *dst, nfct的引用计数。既然该sk_buff要释放,也要调用相应的函数对与其连接的数据结构的引用计数减1。
● 如果sk_buff的destructor函数指针初始化了,在释放sk_buff时,需要调用该函数做清理工作。
● 紧接在sk_buff底部有另一个数据结构实体skb_shared_info,该数据结构描述的是数据包被分片后的相关信息。skb_shared_info内部的指针还可能指向别的数据片所在内存,这时kfree_skb也要释放数据片占用的内存以及管理数据片内存的队列。
● 最后sk_buff数据结构将放回到sk_buff的cache中。如果该sk_buff没有克隆(skb->fclone为SKB_FCLONE_UNAVAILABLE),它将返回到skbuff_head_cache缓冲区中。如果该sk_buff有克隆,该sk_buff数据结构将返回到skbuff_fclone_cache缓冲区中。完成Socket Buffer释放的主要函数如表2-6所示。
图2-9 Socket Buffer的释放操作
表2-6 释放Socket Buffer 的操作函数
表2-7列出了在内核中实现的完成在Socket Buffer数据包存放缓冲区数据空间的预留和对齐操作的主要函数。这些函数提供的是操作sk_buff数据结构的data、tail 指针的方法。
表2-7 在Socket Buffer 中预留空间和对齐操作函数
1.skb_reserve
在存放数据包的缓冲区的头部预留指定的空间,通常用于在数据包存储区插入网络协议头或强制数据边界对齐。该函数同时将skb->data、skb->tail指针向后移动。即:
skb->data += len;
skb->tail += len;
看一看以太网设备驱动程序的接收函数,你会看到,在将数据存入刚分配的SocketBuffer之前,都要使用以下命令:
因为cs89x0是以太网类的网络适配器,它的驱动程序知道其接收的是以太网协议的网络数据包,以太网的协议头长度为14个字节,所以驱动程序在数据包缓冲区头部预留2个字节,这样网络层的协议头在插入时可以按照16字节的边界对齐。
2.skb_put
将skb->tail指针向后移动len的位置,在Socket Buffer存放数据包的缓冲区尾部为数据预留长度为len的空间。
3.skb_push
将skb->data指针向前移动len的位置,在Socket Buffer存放数据包的缓冲区的头部为将要写入的数据预留长度为len的空间。
4.skb_pull
从Socket Buffer存放数据包缓冲区的头部移走长度为len的数据,即skb->data指针向后移动skb->data + len。
5.skb_trim
从缓冲区尾部移走长度为len的数据,即skb->tail指针向前移动skb->tail – len。
以上各项操作的示意图如图2-10与图2-11所示。
图2-10 skb_put与skb_push的操作示意图
图2-11 skb_pull与skb_trim的操作示意图
skb_put和skb_push并不做实际的数据复制,只是为将要复制到数据缓冲区的数据预留空间,实际的数据复制在相应的协议层由该层自己的函数来完成。
从网络上接收的数据包有时有多个进程要对其进行处理,这些处理过程有时需要修改数据包的内容,有时只需修改sk_buff数据结构的内容。这时我们需要对Socket Buffer进行克隆和复制。
在什么时候需要对Socket Buffer 进行克隆?某些时候,同一个Socket Buffer会由不同的进程独立处理,但这些进程所需要操作的只是sk_buff数据结构描述符,不需要对数据包本身做改动。这时为了提高处理性能,内核不需对整个SocketBuffer(sk_buff数据结构和数据包缓冲区)做完全的复制,只对sk_buff数据结构做完全的复制,并将数据包的引用计数(dataref)加1,以此来防止在还有进程使用该Socket Buffer的数据包情况下,缓冲区被释放。这就是sk_buff克隆函数的功能。这时,原sk_buff和克隆的sk_buff共享同一个数据包。克隆操作过程如图2-12所示。
实现克隆操作的函数为:
skb_clone产生一个对skb的克隆数据结构,返回指向克隆出来的sk_buff数据结构的指针。克隆的sk_buff都具有以下特点:
● 不放入任何sk_buff的管理队列。
● 不属于任何套接字,即没有任何套接字拥有克隆的sk_buff。
● 两个sk_buff结构的skb->cloned域都需置为1。克隆出来的sk_buff的sk->users域应设为1,这样当要释放克隆出来的sk_buff数据结构时,第一次对它的释放就能成功了。
● 当一个sk_buff被克隆后,它的数据包中的值就不能再修改,这时我们访问数据包时可以不加锁,因为只能对数据包作读操作,也就不存在并发访问的问题。
与克隆相关的帮助函数skb_cloned(skb)可用于查看某个skb是否为克隆的缓冲区。
图2-12 克隆Socket Buffer
与克隆不同,当多个进程对同一个Socket Buffer的操作为既要修改sk_buff数据结构中的内容,也要修改数据包内容时,必须对Socket Buffer进行复制。这时可以有两个选择:
● 如果既要修改主数据包中的内容,又要操作分片数据段中的值,函数structsk_buff *skb_copy( struct sk_buff *skb ) 可以完成该功能。
● 如果只修改主数据包中的内容,而不需读/写分片数据段中的值,可以使用函数:struct sk_buff *pskb_copy( struct sk_buff *skb)来完成操作。
图2-13给出了这两个函数操作的差别。
图2-13 Socket Buffer的复制和部分复制
Socket Buffer 在内核中组织在不同的队列里,相应的内核也提供了一系列的函数来管理队列操作,比如将sk_buff数据结构插入队列,从队列中取出sk_buff等。这些函数如表2-8所示。
表2-8 内核中操作队列的函数
这类函数的执行必须是原子操作,在操作队列前它们必须首先获取sk_buff_head结构中的spinlock,否则,操作可能被异步事件(如中断操作)打断。
对Socket Buffer引用计数的操作由skb_get函数完成。
对数据包的引用计数不在sk_buff数据结构中,在2.5节将介绍数据包的引用计数在描述数据分片的数据结构变量中。
从Linux-2.6.26以后,sk_buff中描述各层协议头信息在数据包中地址的数据域发生了重大的变化。原来的版本中,传输层、网络层、数据链路层的协议头在数据包中的起始地址是由联合(union)变量表示的。各协议层都可以有多个协议,例如在传输层最常用的就有TCP和UDP。不同的协议头的长度是不一样的。在Linux-2.6.26以后将其改为指针(32位体系结构)或头数据相对于数据包起始地址的偏移量(64位体系结构)。在include/linux/skbuff.h中也增加了一系列的函数来操作这些指针,其函数原型及功能如表2-9所示。
表2-9 各层协议头指针操作函数