上一篇文章中介绍了sk_buff的基本结构,对sk_buff的基本组织结构以及对数据的处理有了一定的了解,接下来我们继续来介绍sk_buff中另外两个比较重要的概念。

4.sk_buff的clone
首先,为什么要对skb进行clone呢?
我们知道对于面向连接的协议,数据传到下层并不是真正的完成了发送过程,只有在接到对方的确认后才能丢弃数据。clone的意义就在于这,保存发送的数据,在发送超时时可以对这段数据进行重传。在我们熟知的协议中,最好的例子当然是TCP了。
下面我们再来看内核中是如何实现的clone,还是先图解:
 

(图1)
关于这副图的几点说明:
* 两个skb在一片连续的内存中,且在这片内存的末尾隐藏了一个计数器,用来给这片内存做引用计数
* 原始skb的fclone位为1,克隆skb的fclone位为2
* 克隆前两个skb的cloned位均为0,克隆后两个skb的cloned位均为1
这种skb是使用alloc_skb_fclone()申请的,它从sk_buff的一个专门的cache中分配。alloc_skb_fclone()返回第一个skb的地址。
但如果像这样两个skb共同引用一个header缓冲区,会带来严重的竞争问题,所以其中的一个必须要放弃对header缓冲的控制。因为原始skb不需要操作header(因为它的作用只是保存缓冲区中的数据),所以,就应该由原始skb来放弃对header的控制权。该操作分两个部分:首先要给自己的nohdr位置0,表示该skb不包括header;然后就要操作以前提到的dataref了。1094604
之前我们提到了dataref分成了高16位和低16位,低16位是对整个数据缓冲区的引用计数,高16位是对header没有引用的计数。那么在clone的情况下,原始skb只引用了payload,没有引用header,所以,它要给高16位和低16位都加1。而克隆skb引用了整个数据区,所以,它要给低16位加1。这样一来,就完成了对整个数据区,header和payload三个部分的计数。至于为什么要把高16位定义的那样怪,应该是想尽量减少atomic操作。
 
5.sk_buff的限额及缓冲区控制
首先,限额的功能只是针对于STREAM类型的协议,因为只有这种协议才需要保留已的发送缓冲区,因而也就需要有内存使用限制。
另外,这只是socket子系统提供的一套机制,具体的响应还是要由协议来决定,只能是起到一个“建议”的作用。
还有,限额不是由skb本身提供的一个机制,它实际是由sk和proto以及一系列接口组成的。但它是控制sk_buff缓冲区生长的一个重要机制,所以权且写在这里。下面先从字段来看。
proto相关字段释义:
限额包含了两个层面的内存申请限制:协议层面和单个socket层面。使用该机制的协议必须提供三个数组,即proto中的三个sysctl_开头的字段。
sysctl_mem: 协议的总内存限额(在此处称之为协议内存限额)。单位是SK_MEM_QUANTUM(即PAGE_SIZE)。
和memory_allocated协同使用,表示目前TCP对内存的使用情况。
sysctl_wmem: 单个写队列的内存限额(在此处称之为sock写队列内存限额)。单位是字节。
sysctl_rmem: 单个读队列的内存限额(在此处称之为sock读队列内存限额)。单位是字节
这三个数组的长度都为3,每个数都表示了使用空间的一个阈值。
memory_pressure:空间使用进入压力状态的标志,表示协议现在是否使用了过量的空间。需要赋值才能启用。
memory_allocated:协议已分配内存的数量,单位是SK_MEM_QUANTUM。
和协议内存限额配合使用,当超过sysctl_mem[1]时就进入pressure状态,对memory_pressure赋1。
sk相关字段释义:
sk_rmem_alloc 接收队列已使用空间。
sk_wmem_alloc 发送队列已使用空间。在STREAM类型协议中由sk_wmem_queued成员所替代。
sk_omem_alloc 目前尚不明。
sk_sndbuf: 发送缓冲区大小。最大不超过sock写队列内存限额的最大值。
sk_rcvbuf: 接收缓冲区大小。最大不超过sock读队列内存限额的最大值。
sk_forward_alloc:发送队列中可以直接使用的空间大小,是指已经分配了但尚未使用的空间。
sk_wmem_queued: 发送队列所占空间,仅用于STREAM类型协议,替代sk_wmem_alloc。最大不能超过发送缓冲区大小。
 
接口:
sk_wmem_schedule() 发送队列内存申请。从协议内存限额中分配,分配的空间在sk_forward_alloc中。
sk_rmem_schedule() 接收队列内存申请。从协议内存限额中分配,分配的空间在sk_forward_alloc中。
sk_stream_memory_free() 检查当前是否已经将发送缓冲区填满。
sk_stream_moderate_sndbuf()修改发送缓冲区(在申请内存失败的情况下),修改后的sndbuf小于目前发送队列所占空间,大于SOCK_MIN_SNDBUF。
sk_stream_wait_memory() 在申请内存失败的情况下等待发送缓冲区释放空间。
 
当发送缓冲区填满(或申请不到skb,page,或达到协议内存限额上限)时,若在阻塞条件下,系统会等待内存空闲,发送调用阻塞。在这种情况下,只有等接收对方相应,重新调整发送缓冲区大小,才能唤醒发送队列。并且发送队列的唤醒是有条件的,发送缓冲区的空闲空间要占总空间的1/3以上,才能将写队列唤醒,继续接收应用程序输入请求。另外对于发送缓冲区的调整,是根协议相关的,这个以后看到再说吧。
当接收缓冲区填满(或达到协议内存限额上限)时,当有新的skb到来时,对于TCP来说就直接将该包丢弃,此时不会对接收缓冲区进行调整。但接收缓冲区的大小也并非是不会改变,当收到应用程序输出请求时,会根据协议来调整接收缓冲区大小。
发送缓冲区和接收缓冲区都是在单个socket层面上的控制,内核网络子系统还提供了以协议为单位的内存限额功能。它主要使用了上面提到的memory_allocated和sysctl_mem两个字段。在TCP协议中,协议内存限额是根据系统内存数量计算出来的,所以在内存很少的情况下可能会影响TCP的性能。
 
socket内存管理框架大概就是这样,以后看到相关的再补充。但是在继续研究TCP建立连接的过程之前,还需要先了解内核对TCP滑动窗口协议的实现,下面先来研究一下这个问题吧。