2013年12月16日星期一

Linux TCP协议栈中的预分配缓存

Linux TCP协议栈中的预分配缓存     TCP协议栈中的预分配缓存的大小由sock结构中的sk_forward_alloc成员描述,在创建套接字时该成员被初始化为0(参见sk_clone())。虽然这个成员在内核文档和《Linux内核源码剖析---TCP/IP实现中》都描述为预分配缓存的长度,但是在代码中并没有看到使用这个成员来预先分配一段内存,更多地是通过这个成员来控制TCP协议栈使用的内存。其实sk_forward_alloc更应该被描述为一个限额,就是限制当前套接字可以使用的内存数量。下面我们详细来看一下sk_forward_alloc在协议栈中具体的处理。
1、预分配缓存长度的计算
    在接收到数据包后,不管是放到接收队列还是乱序队列,都会调用tcp_try_rmem_schedule()来确认缓存是否有效,即是否允许缓存当前接收到的数据包。如果当前数据包的truesize(数据加sk_buff相关结构)小于sk_forward_alloc,则直接返回成功,表示可以缓存。truesize小于sk_forward_alloc表示已分配的限额还没有使用完成,所以就无需进一步确认。如果超过限额,则会调用__sk_mem_schedule()来重新计算k_forward_alloc,并进行全面的确认。
    在发送数据时,分配用于发送的SKB或者向sk_buff中拷贝数据都会调用sk_wmem_schedule()来确认发送缓存是否可用,即是否允许将当前的数据添加到发送队列中。和sk_rmem_schedule()类似,如果数据包的truesize小于sk_forward_alloc,则直接返回成功,否则会调用__sk_mem_schedule()来重新计算sk_forward_alloc,并进行全面的确认。
    不管是发送和接收数据包,都要通过sk_forward_alloc来查看是否可以将数据添加到缓冲区。sk_forward_alloc是在__sk_mem_schedule()中计算的,相关代码如下所示:
int __sk_mem_schedule(struct sock *sk, int size, int kind)
{
    struct proto *prot = sk->sk_prot;
    int amt = sk_mem_pages(size);
    int allocated;

    sk->sk_forward_alloc += amt * SK_MEM_QUANTUM;
    allocated = atomic_add_return(amt, prot->memory_allocated);
......
}
    上面的代码中,sk_mem_pages根据待确认的缓存长度size,向上取整获得所占用内存的页面数,所以amt必定大于等于1。SK_MEM_QUANTUM的值即为PAGE_SIZE,所以sk_forward_alloc的值在没有分配配额时为PAGE_SIZE的整数倍。如果是TCP协议,prot->memory_allocated指向全局变量tcp_memory_allocated,保存的是TCP协议栈所有预分配缓存对应的页面数(分配和释放配额时,sk_forward_alloc会变化),注意,并不是说TCP协议栈所有套接字的缓冲区占用的内存数就是tcp_memory_allocated对应的内存总和,它只是一个标识,协议栈并没有真的预先分配出了这么多内存供套接字使用。这里计算完成后,如果缓存确认失败,会取消这里的操作,缓存的确认后面再讲。
2、预分配缓存的分配和释放
    注意,这里的分配和释放并不是操作内存,而是操作预分配缓存的长度。预分配缓存的分配和释放分配是由sk_mem_charge()和sk_mem_uncharge()实现,这两个函数做的工作主要是在sk_forward_alloc上加上或减去当前skb的truesize或者拷贝数据的长度。
    接收数据时,如果数据包可以缓存,则会调用skb_set_owner_r()设置当前SKB所属的传输控制块,该函数会更新接收队列中缓存数据的长度,并且调用sk_mem_charge()来分配配额。前面已经调用过tcp_try_rmem_schedule()来确认缓存是否有效,所以如果执行到skb_set_owner_r(),分配配额肯定是可以成功的。在skb_set_owner_r()中还会设置SKB包的destructor接口为sock_rfree()函数,在销毁SKB包时会调用该接口。sock_rfree()中会调用sk_mem_uncharge()释放当前SKB占用的配额。
    发送数据时,配额的分配是在将创建的SKB添加到发送队列或者向SKB中拷贝数据的时候,这是直接调用sk_mem_charge()来完成的,并不是通过skb_set_owner_w()。skb_set_owner_r()操作的SKB通常是从接收队列中的SKB克隆出来的,在发送到下层协议栈后就会释放,这些SKB占用的内存并没有算到TCP协议栈的预分配缓存中。发送队列中的SKB占用的配额是在接收到对端的确认、发送超时调用sk_mem_uncharge()释放占用的配额。
    除了单个SKB数据包占用的预分配缓存的释放,在断开连接、释放传输控制块或者关闭套接字等情况下会调用sk_mem_reclaim()来释放为套接字的预分配缓存。sk_mem_reclaim()中只有在sk_forward_alloc大于SK_MEM_QUANTUM(即PAGE_SIZE)时才调用__sk_mem_reclain()进行真正的缓存回收,因为__sk_mem_reclain()中更新sk_forward_alloc和prot->memory_allocated是按照SK_MEM_QUANTUM进行的,如果sk_forward_alloc小于SK_MEM_QUANTUM,这两个成员并不会更新,代码如下所示:
void __sk_mem_reclaim(struct sock *sk)
{
    struct proto *prot = sk->sk_prot;

    atomic_sub(sk->sk_forward_alloc >> SK_MEM_QUANTUM_SHIFT,
           prot->memory_allocated);
    sk->sk_forward_alloc &= SK_MEM_QUANTUM - 1;
.......
}
    我们前面讲到过,sk_forward_alloc在计算时总是SK_MEM_QUANTUM的整数倍,如果套接字占用的预分配缓存都释放的话,sk_forward_alloc的值应该为0。如果在释放传输控制块的时候,sk_forward_alloc的值不为0,说明内核中某处可能有内存泄漏或者没有正确地更新预分配缓存的长度。
3、预分配缓存的确认
    如果预分配缓存已经用完,则调用__sk_mem_schedule()增加预分配缓存,该函数会首先计算根据新增加的数据长度计算出要新分配的页面数,然后更新到sk_forward_alloc和prot->memory_allocated。如果缓存确认失败,则撤销更新操作。
    缓存确认是否成功和sysctl_tcp_mem、sysctl_tcp_rmem、sysctl_tcp_wmem三个系统参数有关,满足下列情况之一,缓存确认会成功:
    1)所有预分配缓存(更新后的值,下同)低于低水平线,即sysctl_tcp_mem[0]的值
    2)所有预分配缓存高于低水平线,低于硬性限制线,确认的是接收缓存,并且接收队列中的数据总长度小于接收缓冲区的长度上限(即sysctl_tcp_rmem[0])
    3)所有预分配缓存高于低水平线,低于硬性限制线,确认的是发送缓存,如果套接字类型是SOCK_STREAM(tcp或者sctp等),发送队列中数据总长度小于发送缓冲区的长度上限(即sysctl_tcp_wmem[0])
    4)所有预分配缓存高于低水平线,低于硬性限制线,确认的是发送缓存,如果套接字类型不是SOCK_STREAM,为发送而分配的所有SKB数据区的总长度小于发送缓冲区的长度上限(即sysctl_tcp_wmem[0])
    5)不满足以上条件,没有进入警告状态,或者当前套接字发送队列中所有段数据总长度、接收队列中所有段数据总长度、预分配缓存长度(即sk_forward_alloc)三者之和乘以当前系统中套接字数量得到的值小于硬性限制线
    6)所有预分配缓存高于硬性限制线,确认的是发送缓存,并且套接字类型是SOCK_STREAM,会调用sk_stream_moderate_sndbuf()调整套接字的发送缓冲区。只有用户没有通过套接字选项设置发送缓冲区大小的情况下才会调整,调整为发送队列占用内存的一半,但是要大于等于2048。如果发送队列占用的内存大小和待确认长度之和大于调整后的发送缓冲区,则确认成功。在这种情况下,把size再添加到发送队列中,这样发送队列占用的内存容量就会超过发送缓冲区,在分配sk_buff前调用tcp_memory_free()就会返回失败,这样就阻止内存的占用。

没有评论:

发表评论