本次内存分析基于linux-2.6.35.2
tcp协议栈内存控制主要由3个数组变量控制,分别是sysctl_tcp_mem[3]、sysctl_tcp_rmem[3]、
sysctl_tcp_wmem[3],这几个数组的值都可以通过proc文件系统的修改来改变,主要有两种方法,示例如下所示:
sysctl net.ipv4.tcp_synack_retries=10
下面看看sock内存相关的变量:
sk_rcvbuf和sk_sndbuf,这两个值分别代表每个sock的读写buf的最大限制
sk_rmem_alloc和sk_wmem_alloc这两个值分别代表已经提交的数据包的字节数
sk_forward_alloc,这个值表示一个预分配置,也就是整个tcp协议栈的内存cache,第一次为一个缓冲区分配
buf的时候,我们不会直接分配精确的大小,而是按页来分配,而分配的大小就是这个值
sk_wmem_queued也就代表skb的写队列write_queue的大小
__sk_mem_schedule和__sk_mem_reclaim函数是
下面看看这些sock内存相关的变量主要在哪些地方调用:
sk_rcvbuf变量,搜索一下,主要有:
int sock_setsockopt(struct socket *sock, int level, int optname,
char __user *optval, unsigned int optlen)
{
...
case SO_RCVBUF:
if (val > sysctl_rmem_max)
val = sysctl_rmem_max;
set_rcvbuf:
sk->sk_userlocks |= SOCK_RCVBUF_LOCK; /* 置上用户空间进行了修改标志 */
if ((val * 2) < SOCK_MIN_RCVBUF)
sk->sk_rcvbuf = SOCK_MIN_RCVBUF;
else
sk->sk_rcvbuf = val * 2;
break;
...
}
static int tcp_v4_init_sock(struct sock *sk)
{
...
/*
* sk_sndbuf、sk_rcvbuf的初始化在这边进行,其初始值分别为sysctl_tcp_wmem[1]、sysctl_tcp_rmem[1]
*/
sk->sk_sndbuf = sysctl_tcp_wmem[1];
sk->sk_rcvbuf = sysctl_tcp_rmem[1];
...
}
/**
* 这个函数对于tcp协议栈是由sk_add_backlog调用,该函数主要是再加入一个skb,所有队列的报文大小是否会
* 超过sk_rcvbuf的大小,其中sk_rmem_alloc的大小主要包括sk_receive_queue和out_of_order_queue队列里面
* 数据的长度(单位为字节)
*/
static inline bool sk_rcvqueues_full(const struct sock *sk, const struct sk_buff *skb)
{
unsigned int qsize = sk->sk_backlog.len + atomic_read(&sk->sk_rmem_alloc);
return qsize + skb->truesize > sk->sk_rcvbuf;
}
/* 检查该sock是否设置成快速接收,这边只是进行判断,对内存管理没有什么关系,只是关系快速与慢速路径 */
static inline void tcp_fast_path_check(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
if (skb_queue_empty(&tp->out_of_order_queue) &&
tp->rcv_wnd &&
atomic_read(&sk->sk_rmem_alloc) < sk->sk_rcvbuf &&
!tp->urg_data)
tcp_fast_path_on(tp);
}
/* 判断剩余的接收缓存,tcp_win_from_space函数只是判断是否scale */
static inline int tcp_space(const struct sock *sk)
{
return tcp_win_from_space(sk->sk_rcvbuf -
atomic_read(&sk->sk_rmem_alloc));
}
/* 接收缓存大小 */
static inline int tcp_full_space(const struct sock *sk)
{
return tcp_win_from_space(sk->sk_rcvbuf);
}
/**
* 调整接收缓存的大小,如果接收缓存的大小小于4个报文大小就调整为4个报文大小和sysctl_tcp_rmem[2]中较
* 小的值,至于为什么是4个报文大小,注释中有解释,主要是因为快速重传要工作至少要有4个报文,这个函数
* 主要是被tcp_init_buffer_space函数调用,而tcp_init_buffer_space函数tcp三次握手的时候调用,被
* tcp_rcv_synsent_state_process和tcp_rcv_state_process函数调用,而tcp_init_buffer_space函数主要是
* 有调整rcvbuf、sndbuf的大小(有条件的,用户空间没有置上调整大小的标志)、rcvq_space.space、
* window_clamp(根据rcvbuf等相关变量进行调整)、rcv_ssthresh变量大小
*/
static void tcp_fixup_rcvbuf(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
/* 对于快转只需要一个ef_buff的大小 */
int rcvmem = tp->advmss + MAX_TCP_HEADER + 16 + sizeof(struct sk_buff);
/* Try to select rcvbuf so that 4 mss-sized segments
* will fit to window and corresponding skbs will fit to our rcvbuf.
* (was 3; 4 is minimum to allow fast retransmit to work.)
*/
while (tcp_win_from_space(rcvmem) < tp->advmss)
rcvmem += 128;
if (sk->sk_rcvbuf < 4 * rcvmem)
sk->sk_rcvbuf = min(4 * rcvmem, sysctl_tcp_rmem[2]);
}
/**
* 这个函数中也会调整sk_rcvbuf的大小,但是其调整要具备一定的条件,其中tcp_memory_pressure、
* tcp_memory_allocated主要是在__sk_mem_schedule和__sk_mem_reclaim函数中改变,tcp_clamp_window函数被
* tcp_prune_queue函数调用
*/
static void tcp_clamp_window(struct sock *sk)
{
...
if (sk->sk_rcvbuf < sysctl_tcp_rmem[2] &&
!(sk->sk_userlocks & SOCK_RCVBUF_LOCK) &&
!tcp_memory_pressure &&
atomic_read(&tcp_memory_allocated) < sysctl_tcp_mem[0]) {
sk->sk_rcvbuf = min(atomic_read(&sk->sk_rmem_alloc), sysctl_tcp_rmem[2]);
}
...
}
/**
* 该函数主要是被tcp_try_rmem_schedule函数调用
*/
static int tcp_prune_queue(struct sock *sk)
{
...
if (atomic_read(&sk->sk_rmem_alloc) >= sk->sk_rcvbuf)
tcp_clamp_window(sk);
else if (tcp_memory_pressure)
tp->rcv_ssthresh = min(tp->rcv_ssthresh, 4U * tp->advmss);
... /* 聚合报文,减少内存占用,对于ef_buff无需该操作 */
sk_mem_reclaim(sk);
/* 减少memory_allocated的大小,并且根据条件调整memory_pressure的值为0,也就是解除内存警戒 */
if (atomic_read(&sk->sk_rmem_alloc) <= sk->sk_rcvbuf)
return 0;
tcp_prune_ofo_queue(sk);
if (atomic_read(&sk->sk_rmem_alloc) <= sk->sk_rcvbuf)
return 0;
tp->pred_flags = 0;
return -1;
}
/**
* 试图调整收包端缓存的大小,只有在队列里面的长度大于sk_rcvbuf才会进行调整,进行调整首先是调用
* sk_rmem_schedule函数,而sk_rmem_schedule函数只是对__sk_mem_reclaim函数的简单封装
*/
static inline int tcp_try_rmem_schedule(struct sock *sk, unsigned int size)
{
if (atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf ||
!sk_rmem_schedule(sk, size)) {
if (tcp_prune_queue(sk) < 0)
return -1;
if (!sk_rmem_schedule(sk, size)) {
if (!tcp_prune_ofo_queue(sk))
return -1;
if (!sk_rmem_schedule(sk, size))
return -1;
}
}
return 0;
}
int __sk_mem_schedule(struct sock *sk, int size, int kind)
{
struct proto *prot = sk->sk_prot;
/* 首先得到size占用几个内存页 */
int amt = sk_mem_pages(size);
int allocated;
/* 更新sk_forward_alloc,可以看到这个值是页的大小的倍数 */
sk->sk_forward_alloc += amt * SK_MEM_QUANTUM;
/*
* amt+memory_allocated也就是当前的总得内存使用量加上将要分配的内存的话,现在的tcp协议栈的总得内
* 存使用量,以页为单位
*/
allocated = atomic_add_return(amt, prot->memory_allocated);
/* 先判断是否小于等于内存最小使用限额 */
if (allocated <= prot->sysctl_mem[0]) {
if (prot->memory_pressure && *prot->memory_pressure)
*prot->memory_pressure = 0;
return 1;
}
/* 判断Under pressure */
if (allocated > prot->sysctl_mem[1])
/* 大于sysctl_mem[1]说明,已经进入pressure,设置memory_pressure标志 */
if (prot->enter_memory_pressure)
prot->enter_memory_pressure(sk);
/* 如果超过的hard limit。则进入另外的处理 */
if (allocated > prot->sysctl_mem[2])
goto suppress_allocation;
/* 判断类型,这里只有两种类型,读和写。总的内存大小判断完,这里开始判断单独的sock的读写内存 */
if (kind == SK_MEM_RECV) {
if (atomic_read(&sk->sk_rmem_alloc) < prot->sysctl_rmem[0])
return 1;
} else { /* SK_MEM_SEND */
///这里当为tcp的时候,写队列的大小只有当对端数据确认后才会更新,因此我们要用sk_wmem_queued来判断。
if (sk->sk_type == SOCK_STREAM) {
if (sk->sk_wmem_queued < prot->sysctl_wmem[0])
return 1;
} else if (atomic_read(&sk->sk_wmem_alloc) <
prot->sysctl_wmem[0])
return 1;
}
///程序到达这里说明总的内存大小在sysctl_mem[0]和sysctl_mem[2]之间,因此我们再次判断memory_pressure
if (prot->memory_pressure) {
int alloc;
///如果没有在memory_pressure区域,则我们直接返回1。
if (!*prot->memory_pressure)
return 1;
///这个其实也就是计算整个系统分配的socket的多少。
alloc = percpu_counter_read_positive(prot->sockets_allocated);
///这里假设其余的每个sock所占用的buf都和当前的sock一样大的时候,如果他们的总和小于sysctl_mem[2],也就是hard limit。那么我们也认为这次内存请求是成功的。
if (prot->sysctl_mem[2] > alloc *
sk_mem_pages(sk->sk_wmem_queued +
atomic_read(&sk->sk_rmem_alloc) +
sk->sk_forward_alloc))
return 1;
}
suppress_allocation:
///到达这里说明,我们超过了hard limit或者说处于presure 区域。
if (kind == SK_MEM_SEND && sk->sk_type == SOCK_STREAM) {
///调整sk_sndbuf(减小).这个函数前面已经分析过了。
sk_stream_moderate_sndbuf(sk);
///然后比较和sk_sndbuf的大小,如果大于的话,就说明下次我们再次要分配buf的时候会在tcp_memory_free阻塞住,因此这次我们返回1.
if (sk->sk_wmem_queued + size >= sk->sk_sndbuf)
return 1;
}
/* Alas. Undo changes. */
///到达这里说明,请求内存是不被接受的,因此undo所有的操作。然后返回0.
sk->sk_forward_alloc -= amt * SK_MEM_QUANTUM;
atomic_sub(amt, prot->memory_allocated);
return 0;
}
sk_rmem_alloc变量,搜索一下,主要有:
/**
* sk_rmem_alloc加上接收skb的总长度,sk_forward_alloc减去skb的总长度,主要在skb加入sk_receive_queue
* 和out_of_order_queue队列时调用
*/
static inline void skb_set_owner_r(struct sk_buff *skb, struct sock *sk)
{
skb_orphan(skb);
skb->sk = sk;
skb->destructor = sock_rfree;
atomic_add(skb->truesize, &sk->sk_rmem_alloc);
sk_mem_charge(sk, skb->truesize);
}
/**
* sk_rmem_alloc减去接收skb的总长度,sk_forward_alloc加上skb的总长度,在释放skb时调用
*/
void sock_rfree(struct sk_buff *skb)
{
struct sock *sk = skb->sk;
atomic_sub(skb->truesize, &sk->sk_rmem_alloc);
sk_mem_uncharge(skb->sk, skb->truesize);
}
下面就看看在收包流程中是怎么样进行收包的内存控制的:
在创建sock时会调用tcp_v4_init_sock函数对sk_sndbuf和sk_rcvbuf进行初始化,在三次握手的时候也会对sk_sndbuf
和sk_rcvbuf进行一个调整,在tcp_rcv_synsent_state_process和tcp_rcv_state_process函数中进行调整,具体
的也不一一分析了,三次握手完成后,就进入建立状态,这才是内存控制的主要区域,报文首先进入tcp_rcv_established
函数,对于快速路径,首先调用skb_set_owner_r(skb, sk);函数对sk_rmem_alloc和sk_forward_alloc进行操作,
然后会调用tcp_event_data_recv(sk, skb)函数,该函数里面有可能会调用sk_mem_reclaim函数,另外,如果在快
速路径上应用层有进行收包,收完包后会调用tcp_rcv_space_adjust函数调整sk_rcvbuf的大小,至此快速路径上
的已经OK了;接下来看看慢速路径的,慢速路径处理skb的函数是tcp_data_queue,在一定条件下调用tcp_try_rmem_schedule
函数,如果不能接收报文了,则把报文丢弃,还能则会调用skb_set_owner_r函数;另外在应用层收报文的时候会
调用tcp_rcv_space_adjust函数调整sk_rcvbuf的大小,当然各个定时器里面也有内存管理相关的操作
下面看一下sk_wmem_alloc变量,主要有:
struct sock *sk_alloc(struct net *net, int family, gfp_t priority,
struct proto *prot)
{
...
atomic_set(&sk->sk_wmem_alloc, 1); /* 初始化为1 */
...
}
/* 主要由sock_put函数调用 */
void sk_free(struct sock *sk)
{
/*
* We substract one from sk_wmem_alloc and can know if
* some packets are still in some tx queue.
* If not null, sock_wfree() will call __sk_free(sk) later
*/
if (atomic_dec_and_test(&sk->sk_wmem_alloc))
__sk_free(sk);
}
/* 该函数由skb_set_owner_w调用 */
void sock_wfree(struct sk_buff *skb)
{
struct sock *sk = skb->sk;
unsigned int len = skb->truesize;
if (!sock_flag(sk, SOCK_USE_WRITE_QUEUE)) {
/*
* Keep a reference on sk_wmem_alloc, this will be released
* after sk_write_space() call
*/
atomic_sub(len - 1, &sk->sk_wmem_alloc);
sk->sk_write_space(sk);
len = 1;
}
/*
* if sk_wmem_alloc reaches 0, we must finish what sk_free()
* could not do because of in-flight packets
*/
if (atomic_sub_and_test(len, &sk->sk_wmem_alloc))
__sk_free(sk);
}
/**
* 该函数由tcp_transmit_skb和sock_wmalloc函数调用,而sock_wmalloc函数由tcp_make_synack调用,总之是在
* 把报文发往ip层时调用该函数
*/
static inline void skb_set_owner_w(struct sk_buff *skb, struct sock *sk)
{
skb_orphan(skb);
skb->sk = sk;
skb->destructor = sock_wfree;
/*
* We used to take a refcount on sk, but following operation
* is enough to guarantee sk_free() wont free this sock until
* all in-flight packets are completed
*/
atomic_add(skb->truesize, &sk->sk_wmem_alloc);
}
sk_wmem_queued变量的使用,搜索一下,主要有以下地方,只要有进入发送队列,sk_wmem_queued的值都会增加:
struct sock *sk_clone(const struct sock *sk, const gfp_t priority)
{
newsk->sk_wmem_queued
= 0;
}
int __sk_mem_schedule(struct sock *sk, int size, int kind) -- 参见前面
/* 该函数由tcp_sendmsg、tcp_write_queue_purge等函数调用 */
static inline void sk_wmem_free_skb(struct sock *sk, struct sk_buff *skb)
{
sock_set_flag(sk, SOCK_QUEUE_SHRUNK);
sk->sk_wmem_queued -= skb->truesize;
sk_mem_uncharge(sk, skb->truesize);
__kfree_skb(skb);
}
static inline void sk_stream_moderate_sndbuf(struct sock *sk)
{
if (!(sk->sk_userlocks & SOCK_SNDBUF_LOCK)) {
sk->sk_sndbuf = min(sk->sk_sndbuf, sk->sk_wmem_queued >> 1);
sk->sk_sndbuf = max(sk->sk_sndbuf, SOCK_MIN_SNDBUF);
}
}
sk_sndbuf变量的使用主要有如下地方:
/* 更新sk_sndbuf的大小 */
static void tcp_new_space(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
if (tcp_should_expand_sndbuf(sk)) {
int sndmem = max_t(u32, tp->rx_opt.mss_clamp, tp->mss_cache) +
MAX_TCP_HEADER + 16 + sizeof(struct sk_buff);
int demanded = max_t(unsigned int, tp->snd_cwnd,
tp->reordering + 1);
sndmem *= 2 * demanded;
if (sndmem > sk->sk_sndbuf)
sk->sk_sndbuf = min(sndmem, sysctl_tcp_wmem[2]);
tp->snd_cwnd_stamp = tcp_time_stamp;
}
sk->sk_write_space(sk);
}