TCP协议内存空间管理

PROC文件tcp_mem包括3个TCP协议内存空间的控制值,单位为页面数,如下,分别为最小空间值34524页面,承压值46032和最大值69048个页面,此三个值与系统的内存大小相关,示例为一个3G内存的Ubuntu系统。内核中对应的变量为sysctl_tcp_mem。

$ cat /proc/sys/net/ipv4/tcp_mem
34524   46032   69048
$ 
static struct ctl_table ipv4_table[] = {
    {
        .procname   = "tcp_mem",
        .maxlen     = sizeof(sysctl_tcp_mem),
        .data       = &sysctl_tcp_mem,
    },
}

TCP内存初始化

如下函数tcp_init_mem所示,TCP协议的三个内存空间控制值以系统中空闲页面总数的16分之一为基准(limit),并且此基准值又不低于128个页面。最小值为基准值的四分之三,承压值等于基准值,最大内存值等于最小值的两倍,即百分之9.375。

static void __init tcp_init_mem(void)
{
    unsigned long limit = nr_free_buffer_pages() / 16;

    limit = max(limit, 128UL);
    sysctl_tcp_mem[0] = limit / 4 * 3;      /* 4.68 % */
    sysctl_tcp_mem[1] = limit;          /* 6.25 % */
    sysctl_tcp_mem[2] = sysctl_tcp_mem[0] * 2;  /* 9.37 % */
}

另外,保存控制值的数组sysctl_tcp_mem赋值给了TCP协议结构体tcp_prot的成员sysctl_mem,以便之后需要访问控制值时,可通过sysctl_mem实现。既然需要控制TCP协议占用的内存,就需要一个变量统计当前的TCP内存占用量,见变量tcp_memory_allocated。如果其值大于承压值(sysctl_mem[1]),TCP进入内存承压状态,直到其值小于最小值(sysctl_mem[0])时,TCP退出内存承压状态。memory_pressure变量表示是否处于内存承压状态。

atomic_long_t tcp_memory_allocated;        /* Current allocated memory. */

struct proto tcp_prot = {
    .name           = "TCP",
    .enter_memory_pressure  = tcp_enter_memory_pressure,
    .leave_memory_pressure  = tcp_leave_memory_pressure,
	.memory_allocated   = &tcp_memory_allocated,
    .memory_pressure    = &tcp_memory_pressure,
    .sysctl_mem     = sysctl_tcp_mem,
    .sysctl_wmem_offset = offsetof(struct net, ipv4.sysctl_tcp_wmem),
    .sysctl_rmem_offset = offsetof(struct net, ipv4.sysctl_tcp_rmem),
}

TCP内存占用统计

基础函数sk_memory_allocated_add,通过其调用增加TCP协议的内存占用统计值(memory_allocated成员变量同tcp_memory_allocated)。

static inline long sk_memory_allocated_add(struct sock *sk, int amt)
{
    return atomic_long_add_return(amt, sk->sk_prot->memory_allocated);
}

TCP协议最重要的内存占用统计函数为__sk_mem_raise_allocated,除了为memory_allocated变量增加amt字节的统计外,还需要进行一些列的判断,保证增加后的数值在合法范围内。sk_prot_mem_limits函数的第二个参数0、1、2分别用于获取sysctl_mem数组中的索引为0、1、2的三个TCP内存控制值。以下代码逻辑可见,增加之后的内存统计值如果小于最小控制值,退出内存承压状态并正确返回。如果大于内存承压值,则进入内存承压状态,再甚者,如果大于最大值,内核将尝试抑制本次的内存分配行为(suppress_allocation分支)。

int __sk_mem_raise_allocated(struct sock *sk, int size, int amt, int kind)
{
    struct proto *prot = sk->sk_prot;
    long allocated = sk_memory_allocated_add(sk, amt);

    /* Under limit. */
    if (allocated <= sk_prot_mem_limits(sk, 0)) {
        sk_leave_memory_pressure(sk);
        return 1;
    }
    /* Under pressure. */
    if (allocated > sk_prot_mem_limits(sk, 1))
        sk_enter_memory_pressure(sk);
    /* Over hard limit. */
    if (allocated > sk_prot_mem_limits(sk, 2))
        goto suppress_allocation;
}

在还没有达到TCP内存使用的最大值的情况下,如果TCP套接口当前已用的接收内存大小小于TCP接收缓存tcp_rmem的最小控制值(sk_get_rmem0),或者TCP套接口已用发送内存的大小小于TCP发送缓存tcp_wmem的最小控制值时(sk_get_wmem0),本次分配成功完成。

$ cat /proc/sys/net/ipv4/tcp_rmem
4096    87380   6291456
$ cat /proc/sys/net/ipv4/tcp_wmem
4096    16384   4194304

int __sk_mem_raise_allocated(struct sock *sk, int size, int amt, int kind)
{
    if (kind == SK_MEM_RECV) {
        if (atomic_read(&sk->sk_rmem_alloc) < sk_get_rmem0(sk, prot))
            return 1;
    } else { /* SK_MEM_SEND */
        int wmem0 = sk_get_wmem0(sk, prot);

        if (sk->sk_type == SOCK_STREAM) {
            if (sk->sk_wmem_queued < wmem0)
                return 1;
        } else if (refcount_read(&sk->sk_wmem_alloc) < wmem0) {
                return 1;
        }
    }
}

如果TCP内存处于承压状态(sk_under_memory_pressure),并且TCP协议当前的所有套接口数量,与发送缓存sk_wmem_queued、接收缓存sk_rmem_alloc和sk_forward_alloc三者之和的乘积,小于TCP协议的内存最大控制值,认为本次分配有效。

int __sk_mem_raise_allocated(struct sock *sk, int size, int amt, int kind)
{
    if (sk_has_memory_pressure(sk)) {

        if (!sk_under_memory_pressure(sk))
            return 1;
        alloc = sk_sockets_allocated_read_positive(sk);
        if (sk_prot_mem_limits(sk, 2) > 
			alloc * sk_mem_pages(sk->sk_wmem_queued + atomic_read(&sk->sk_rmem_alloc) + sk->sk_forward_alloc))
            return 1;
    }
}

如果以上条件都未成立,则尝试取消本次分配,由函数sk_memory_allocated_sub实现。但是如果是TCP(SOCK_STREAM)的发送端(SK_MEM_SEND),并且套接口的发送缓存sk_sndbuf大于sk_wmem_queued的一半,并且大于sk_wmem_queued与当前分配缓存size值的和,分配操作正常完成。

int __sk_mem_raise_allocated(struct sock *sk, int size, int amt, int kind)
{
suppress_allocation:
    if (kind == SK_MEM_SEND && sk->sk_type == SOCK_STREAM) {
        sk_stream_moderate_sndbuf(sk);
        if (sk->sk_wmem_queued + size >= sk->sk_sndbuf)
            return 1;
    }
    sk_memory_allocated_sub(sk, amt);
}
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_t(u32, sk->sk_sndbuf, SOCK_MIN_SNDBUF);
    }   
}

内核中对TCP内存的占用一是TCP发送端,一是接收端。发送端通过sk_wmem_schedule函数增加内存分配统计和合法性判断;接收端通过函数sk_rmem_schedule实现内存分配统计与合法性判断。对于发送端内核中的判断点,包括TCP的发送相关函数do_tcp_sendpages和tcp_sendmsg_locked,以及TCP分段函数tcp_fragment,tso_fragment和tcp_mtu_probe,tcp_send_syn_data和tcp_connect等函数。

static inline bool sk_wmem_schedule(struct sock *sk, int size)
{
    if (!sk_has_account(sk))
        return true;
    return size <= sk->sk_forward_alloc || __sk_mem_schedule(sk, size, SK_MEM_SEND);
}

对于接收端,内核中的判断点包括,接收相关函数tcp_rcv_established和sock_queue_rcv_skb函数等。 

static inline bool sk_rmem_schedule(struct sock *sk, struct sk_buff *skb, int size)
{
    if (!sk_has_account(sk))
        return true;
    return size<= sk->sk_forward_alloc || __sk_mem_schedule(sk, size, SK_MEM_RECV) || skb_pfmemalloc(skb);
}

TCP内存释放统计


在TCP内存释放时,减少内存的统计值。基础函数为__sk_mem_reduce_allocated。其不仅执行了统计值的递减,而且之后判断了当前是否处于内存承压状态,若干内存使用以及降低至最小控制值,退出承压状态。

static inline void sk_memory_allocated_sub(struct sock *sk, int amt)
{
    atomic_long_sub(amt, sk->sk_prot->memory_allocated);
}
void __sk_mem_reduce_allocated(struct sock *sk, int amount)
{
    sk_memory_allocated_sub(sk, amount);

    if (sk_under_memory_pressure(sk) && (sk_memory_allocated(sk) < sk_prot_mem_limits(sk, 0)))
        sk_leave_memory_pressure(sk);
}

TCP内存统计值的递减的封装函数,有sk_mem_reclaim函数,sk_mem_reclaim_partial函数和sk_mem_uncharge函数。第一个函数sk_mem_reclaim用于TCP套接口销毁之时,回收其预分配的内存配额(sk_forward_alloc),例如套接口销毁函数inet_csk_destroy_sock以及inet_sock_destruct函数对其进行调用;或者在套接口清空队列时减低内存统计值,如函数tcp_write_queue_purge对其的调用。

void __sk_mem_reclaim(struct sock *sk, int amount)
{
    amount >>= SK_MEM_QUANTUM_SHIFT;
    sk->sk_forward_alloc -= amount << SK_MEM_QUANTUM_SHIFT;
    __sk_mem_reduce_allocated(sk, amount);
}
static inline void sk_mem_reclaim(struct sock *sk)
{
    if (!sk_has_account(sk))
        return;
    if (sk->sk_forward_alloc >= SK_MEM_QUANTUM)
        __sk_mem_reclaim(sk, sk->sk_forward_alloc);
}

函数sk_mem_reclaim_partial与第一个函数基本相同,区别在于其并不回收全部的套接口预分配内存配额,仅回收了(sk->sk_forward_alloc - 1)。所有其调用之处与sk_mem_reclaim由很大的差别,例如在sk_stream_alloc_skb函数,在进行skb分配之前,如果内存处于承压状态,回收部分预分配内存,以保证接下来的内存分配能够成功完成。另外在延迟ACK定时器处理函数中(tcp_delack_timer_handler),也调用了内存部分回收函数,确保ACK的顺利发送。

static inline void sk_mem_reclaim_partial(struct sock *sk)
{
    if (sk->sk_forward_alloc > SK_MEM_QUANTUM)
        __sk_mem_reclaim(sk, sk->sk_forward_alloc - 1);
}
struct sk_buff *sk_stream_alloc_skb(struct sock *sk, int size, gfp_t gfp, bool force_schedule)
{
    if (unlikely(tcp_under_memory_pressure(sk)))
        sk_mem_reclaim_partial(sk);
}

第三个函数sk_mem_uncharge,如果当前套接口的预分配内存额度大于2M字节,回收1M字节,保留太多的预分配内存额度没有必要。而且,TCP发送队列的执行流程如果没有调用sk_mem_reclaim回收内存,有可能导致超过2G的内存一次性的释放,此处减缓释放的操作。

static inline void sk_mem_uncharge(struct sock *sk, int size)
{
    sk->sk_forward_alloc += size;

    if (unlikely(sk->sk_forward_alloc >= 1 << 21))
        __sk_mem_reclaim(sk, 1 << 20);
}

对于接收缓存中的数据包,在kfree_skb函数销毁skb之时,通过sock_rfree函数调用sk_mem_uncharge减低内存统计值。对于发送缓存中(接收和重传队列)的数据包,使用函数sk_wmem_free_skb减低内存统计,并进行内存的释放操作。例如函数tcp_write_queue_purge和tcp_rtx_queue_purge函数。

void sock_rfree(struct sk_buff *skb)
{
    sk_mem_uncharge(sk, len);
}
static inline void sk_wmem_free_skb(struct sock *sk, struct sk_buff *skb)
{
    sk_mem_uncharge(sk, skb->truesize);
    __kfree_skb(skb);
}


TCP内存承压状态

是否处于内存承压状态由变量tcp_memory_pressure表示,内核使用封装函数tcp_under_memory_pressure来判断内存是否承压。另外,TCP函数tcp_enter_memory_pressure和tcp_leave_memory_pressure函数分别在进入和退出承压状态时调用,进入承压状态时,tcp_memory_pressure变量记录下当前时间,在退出承压状态时,计算TCP处于承压状态的时长,保存在当前网络命名空间的统计信息中。

unsigned long tcp_memory_pressure __read_mostly;
static inline bool tcp_under_memory_pressure(const struct sock *sk)
{
    return tcp_memory_pressure;
}

如前对函数__sk_mem_raise_allocated的描述,在当前的TCP内存使用值超过设定的承压值时,进入承压状态,当小于设定的最小值时,退出承压状态。函数__sk_mem_raise_allocated的两个封装函数sk_wmem_schedule和sk_rmem_schedule,分别用于在TCP发送路径和接收路径上统计和控制内存使用情况。

发送路径上的函数由诸如:tcp_sendmsg_locked,do_tcp_sendpages和通用的发送skb分配函数sk_stream_alloc_skb。如下,在skb分配成功之后,增加内存使用统计值,反之,分配失败的话进入内存承压状态。有一点需要注意,即使skb分配成功,如果在随后的sk_wmem_schedule函数中的判断中,新分配的内存空间致使内存使用量超过限制,还是会导致分配失败,释放掉分配好的skb缓存。

struct sk_buff *sk_stream_alloc_skb(struct sock *sk, int size, gfp_t gfp, bool force_schedule)
{
    skb = alloc_skb_fclone(size + sk->sk_prot->max_header, gfp);
    if (likely(skb)) {
        if (force_schedule) {
        } else {
            mem_scheduled = sk_wmem_schedule(sk, skb->truesize);
        }
        if (likely(mem_scheduled)) {
            return skb;
        }
        __kfree_skb(skb);
    } else {
        sk->sk_prot->enter_memory_pressure(sk);
    }
}

接收路径上的函数诸如:__sock_queue_rcv_skb函数,tcp_data_queue_ofo和tcp_data_queue函数。如下为tcp_data_queue函数,在接收到TCP数据包之后,检查增加此数据包长度后TCP的内存占用是否超限,超限的话丢弃数据包。

static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
{
    struct tcp_sock *tp = tcp_sk(sk);
	
    if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
        /* Ok. In sequence. In window. */
queue_and_out:
        if (skb_queue_len(&sk->sk_receive_queue) == 0)
            sk_forced_mem_schedule(sk, skb->truesize);
        else if (tcp_try_rmem_schedule(sk, skb, skb->truesize))
            goto drop;
    }
}

最后,在发送路径上,当套接口中的页面不足以存放数据包分片时,进入内存承压状态。如下函数__ip_append_data所示,只有当数据包的出口设备支持分散/聚集(NETIF_F_SG)特性时,才会执行此流程。

bool sk_page_frag_refill(struct sock *sk, struct page_frag *pfrag)
{               
    if (likely(skb_page_frag_refill(32U, pfrag, sk->sk_allocation)))
        return true;

    sk_enter_memory_pressure(sk);
}
static int __ip_append_data(struct sock *sk, struct flowi4 *fl4, struct sk_buff_head *queue, ...)
{
    while (length > 0) {
        if (!(rt->dst.dev->features&NETIF_F_SG)) {
        } else {
            int i = skb_shinfo(skb)->nr_frags;
            if (!sk_page_frag_refill(sk, pfrag))
                goto error;
		}
	}
}

TCP内存超限

除了以上介绍的__sk_mem_raise_allocated函数之外,tcp_out_of_memory函数也用于TCP内存超过设定的最大值的判断。sk_prot_mem_limits函数第二个参数为2表示获取出设定的TCP内存最大限值。其被函数tcp_check_oom所封装,如果TCP当前内存超限,内核会有打印信息输出到终端上。

static inline bool tcp_out_of_memory(struct sock *sk)
{
    if (sk->sk_wmem_queued > SOCK_MIN_SNDBUF && sk_memory_allocated(sk) > sk_prot_mem_limits(sk, 2))
        return true;
    return false;
}
bool tcp_check_oom(struct sock *sk, int shift)
{
    out_of_socket_memory = tcp_out_of_memory(sk);
    if (out_of_socket_memory)
        net_info_ratelimited("out of memory -- consider tuning tcp_mem\n");
    return  out_of_socket_memory;
}

在函数tcp_close函数和一系列的TCP定时器函数如tcp_write_timeout和tcp_probe_timer中检测TCP的OOM情况。以上定时器函数通过调用tcp_out_of_resources,间接调用tcp_check_oom函数。


TCP内存分配的特例


在特定的情况下,内核允许TCP内存的分配不受内存限额的约束。参见以下函数sk_forced_mem_schedule,与之上函数__sk_mem_raise_allocated不同,其直接增加内存的分配统计,不考虑任何的超限情况。

void sk_forced_mem_schedule(struct sock *sk, int size)
{
    if (size <= sk->sk_forward_alloc)
        return;
    amt = sk_mem_pages(size);
    sk->sk_forward_alloc += amt * SK_MEM_QUANTUM;
    sk_memory_allocated_add(sk, amt);
}

特定的情况包括,TCP的FIN报文发送等,内核尽可能的保证FIN报文的成功发送,以便尽快的结束一个TCP连接。

void tcp_send_fin(struct sock *sk)
{
    struct sk_buff *skb, *tskb = tcp_write_queue_tail(sk);

    if (tskb) {
    } else {
        skb = alloc_skb_fclone(MAX_TCP_HEADER, sk->sk_allocation);
        sk_forced_mem_schedule(sk, skb->truesize);
    }
}

另外,在TCP套接口接收到一个正常序号的数据包,并且其位于接收窗口之内,而此时的接收队列又是为空,内核强制增加内存的统计值,以保证其正确接收。这样可避免对端无谓的重传,但是又能保护TCP的内存不至于超限被过度使用(接收队列为空判断时执行)。在函数tcp_data_queue中,与以上tcp_send_fin不同,内核仅增加内存统计值,并未实际分配内存,数据包skb内存由网卡驱动程序分配而来。

static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
{
    struct tcp_sock *tp = tcp_sk(sk);

    if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
        /* Ok. In sequence. In window. */
queue_and_out:
        if (skb_queue_len(&sk->sk_receive_queue) == 0)
            sk_forced_mem_schedule(sk, skb->truesize);
	}
}

再者,在TCP分配内存时,可通过参数force_schedule指定强制增加内存统计,不受限定值约束。目前在内核中TCP分片相关函数tcp_fragment和tso_fragment,TCP连接发起函数tcp_connect在使用此强制选项,这些函数的alloc_skb_fcone操作不受限定值约束,只要系统内存做够,即可分配成功。

struct sk_buff *sk_stream_alloc_skb(struct sock *sk, int size, gfp_t gfp, bool force_schedule)
{
    skb = alloc_skb_fclone(size + sk->sk_prot->max_header, gfp);
    if (likely(skb)) {
        if (force_schedule) {
            mem_scheduled = true;
            sk_forced_mem_schedule(sk, skb->truesize);
        }
    }
}

最后,在TCP发送函数,无论是do_tcp_sendpages还是tcp_sendmsg_locked函数中,在TCP的重传队列和发送队列都为空的情况下,即tcp_rtx_and_write_queues_empty为真,说明发送缓冲区都已清空,可强制进行内存分配,并增加内存统计。

ssize_t do_tcp_sendpages(struct sock *sk, struct page *page, int offset, size_t size, int flags)
{
    while (size > 0) {
        struct sk_buff *skb = tcp_write_queue_tail(sk);

        if (!skb || (copy = size_goal - skb->len) <= 0 || !tcp_skb_can_collapse_to(skb))
            skb = sk_stream_alloc_skb(sk, 0, sk->sk_allocation, tcp_rtx_and_write_queues_empty(sk));
} 
int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size)
{
    while (msg_data_left(msg)) {
        if (copy <= 0 || !tcp_skb_can_collapse_to(skb)) {
            first_skb = tcp_rtx_and_write_queues_empty(sk);
            skb = sk_stream_alloc_skb(sk, select_size(sk, sg, first_skb), sk->sk_allocation, first_skb);
		}
	}
}

TCP内存承压与超限的影响

如函数sk_stream_alloc_skb所示,在内存承压之后,内核开始回收已分配的内存。并且停止TCP内存分配相关的操作。

struct sk_buff *sk_stream_alloc_skb(struct sock *sk, int size, gfp_t gfp, bool force_schedule)
{
    if (unlikely(tcp_under_memory_pressure(sk)))
        sk_mem_reclaim_partial(sk);
}

如FIN报文发送函数tcp_send_fin,在进入内存承压状态后,试图避免单独的FIN报文分配,将FIN标志设置到重传队列的最后一个数据包上。

void tcp_send_fin(struct sock *sk)
{
    struct sk_buff *skb, *tskb = tcp_write_queue_tail(sk);

    if (!tskb && tcp_under_memory_pressure(sk))
        tskb = skb_rb_last(&sk->tcp_rtx_queue);
}

另外,停止TCP接收窗口的增长,以及停止发送缓存的增长。

static void tcp_grow_window(struct sock *sk, const struct sk_buff *skb)
{
    struct tcp_sock *tp = tcp_sk(sk);
    if (tp->rcv_ssthresh < tp->window_clamp && (int)tp->rcv_ssthresh < tcp_space(sk) &&
        !tcp_under_memory_pressure(sk)) {
    } 
}
static bool tcp_should_expand_sndbuf(const struct sock *sk)
{
    if (tcp_under_memory_pressure(sk))
        return false;
    if (sk_memory_allocated(sk) >= sk_prot_mem_limits(sk, 0))
        return false;
}

再者,对于TCP的接收路径,如果内存处于承压状态,丢掉进入的数据包不在接收。对于发送端函数,诸如tcp_sendmsg_locked或者do_tcp_sendpages函数,在内存承压时,可进入等待队列,即函数sk_stream_wait_memory等待内存可用。内存可用时内核使用函数sk_stream_write_space唤醒等待的队列。

static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
{
    if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
queue_and_out:
        if (skb_queue_len(&sk->sk_receive_queue) == 0)
            sk_forced_mem_schedule(sk, skb->truesize);
        else if (tcp_try_rmem_schedule(sk, skb, skb->truesize))
            goto drop;
}
int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size)
{
    while (msg_data_left(msg)) {
        if (skb_availroom(skb) > 0) {
        } else if (!uarg || !uarg->zerocopy) {
            if (!sk_page_frag_refill(sk, pfrag))
                goto wait_for_memory;
            if (!sk_wmem_schedule(sk, copy))
                goto wait_for_memory;
		}
wait_for_memory:
        err = sk_stream_wait_memory(sk, &timeo);
    }
}

PROC文件protocols

PROC文件/proc/net/protocols可查看当前各个协议包括TCP的内存使用情况,已经承压状态,如下所示:

$ cat /proc/net/protocols 
protocol  size sockets  memory press maxhdr  slab module     cl co di ac io in de sh ss gs se re sp bi br ha uh gp em
TCPv6     2152      2       1   no     304   yes  kernel      y  y  y  y  y  y  y  y  y  y  y  y  y  n  y  y  y  y  y
RAW        928      0      -1   NI       0   yes  kernel      y  y  y  n  y  y  y  n  y  y  y  y  n  y  y  y  y  n  n
UDP        984      5       5   NI       0   yes  kernel      y  y  y  n  y  y  y  n  y  y  y  y  y  n  n  y  y  y  n
TCP       1992      9       1   no     304   yes  kernel      y  y  y  y  y  y  y  y  y  y  y  y  y  n  y  y  y  y  y

 

内核版本 4.15.0

 

你可能感兴趣的:(TCPIP协议)