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_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),
}
基础函数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内存释放时,减少内存的统计值。基础函数为__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_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;
}
}
}
除了以上介绍的__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内存的分配不受内存限额的约束。参见以下函数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);
}
}
}
如函数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文件/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