tcp sk_forward_alloc
keep sk->sk_forward_alloc as small as possible patch。!!!
预分配缓存额度sk_forward_alloc与发送缓存队列统计sk_wmem_queued一同用于计算当前套接口所占用的内存量。sk_forward_alloc属于为套接口预分配,所以缓存并没有实际分配出去。
sk->sk_rmem_alloc表示当前已经使用的接收缓冲区内存
/* * 预分配缓存长度,这只是一个标识,目前 只用于TCP。 * 当分配的缓存小于该值时,分配必然成功,否则需要 * 重新确认分配的缓存是否有效。参见__sk_mem_schedule(). * 在sk_clone()中,sk_forward_alloc被初始化为0. * * update:sk_forward_alloc表示预分配长度。当我们第一次要为 * 发送缓冲队列分配一个struct sk_buff时,我们并不是直接 * 分配需要的内存大小,而是会以内存页为单位进行 * 预分配(此时并不是真的分配内存)。当把这个新分配 * 成功的struct sk_buff放入缓冲队列sk_write_queue后,从sk_forward_alloc * 中减去该sk_buff的truesize值。第二次分配struct sk_buff时,只要再 * 从sk_forward_alloc中减去新的sk_buff的truesize即可,如果sk_forward_alloc * 已经小于当前的truesize,则将其再加上一个页的整数倍值, * 并累加如tcp_memory_allocated。 * 也就是说,通过sk_forward_alloc使全局变量tcp_memory_allocated保存 * 当前tcp协议总的缓冲区分配内存的大小,并且该大小是 * 页边界对齐的。 */ //这是本sock的缓存大小,如果要看整个tcp sock的缓存大小,要参考tcp_prot中的memory_allocated成员 ////阅读函数__sk_mem_schedule可以了解proto的内存情况判断方法 。 注意和上面的sk_wmem_alloc的区别
sk_forward_alloc初始化
对于面向连接的套接口类型如TCP,在创建子套接口时,将其sk_forward_alloc初始化为0。
struct sock *sk_clone_lock(const struct sock *sk, const gfp_t priority) { newsk = sk_prot_alloc(sk->sk_prot, priority, sk->sk_family); if (newsk != NULL) { ----------------- newsk->sk_forward_alloc = 0; } }
sk_forward_alloc预分配
套接口内存的页面是按照SK_MEM_QUANTUM的大小为单位,定义为4096(4K),与大多数系统的PAGE_SIZE定义相同。曾经在老一点版本的内核中,SK_MEM_QUANTUM直接使用PAGE_SIZE宏定义,导致一些将页面大小PAGE_SIZE定义为64K字节的系统,预分配不必要的大量内存额度。
————————————————
#define SK_MEM_QUANTUM 4096 #define SK_MEM_QUANTUM_SHIFT ilog2(SK_MEM_QUANTUM) static inline int sk_mem_pages(int amt) { return (amt + SK_MEM_QUANTUM - 1) >> SK_MEM_QUANTUM_SHIFT; }
但是,sk_forward_alloc的单位还是以字节表示,只不过其大小为SK_MEM_QUANTUM的整数倍。 预分配额度的基础函数为__sk_mem_schedule如下,函数__sk_mem_raise_allocated判断此次分配是否符合协议的内存限定,如果不符合,需要回退预分配的额度。
/** * __sk_mem_schedule - increase sk_forward_alloc and memory_allocated * @sk: socket * @size: memory size to allocate * @kind: allocation type * * If kind is SK_MEM_SEND, it means wmem allocation. Otherwise it means * rmem allocation. This function assumes that protocols which have * memory_pressure use sk_wmem_queued as write buffer accounting. */ int __sk_mem_schedule(struct sock *sk, int size, int kind) { int ret, amt = sk_mem_pages(size); sk->sk_forward_alloc += amt << SK_MEM_QUANTUM_SHIFT; ret = __sk_mem_raise_allocated(sk, size, amt, kind); if (!ret) sk->sk_forward_alloc -= amt << SK_MEM_QUANTUM_SHIFT; return ret; }
__sk_mem_raise_allocated函数还将增加协议总内存的占用统计(memory_allocated),其单位为SK_MEM_QUANTUM大小的页面数量(第三个参数amt)。由于内核的网络协议内存限额是以PAGE_SIZE大小页面为单位,如TCP协议,可通过PROC文件/proc/sys/net/ipv4/tcp_mem查看。所以在进行比较时,内核使用函数sk_prot_mem_limits将限定的页面数值转换为以SK_MEM_QUANTUM为单位的页面值。
函数__sk_mem_schedule的封装函数有两个sk_wmem_schedule和sk_rmem_schedule,对应于发送SK_MEM_SEND和接收SK_MEM_RECV两个类别的缓存使用。对于sk_wmem_schedule函数,如果请求的大小在预分配额度内,进行正常分配,否则,由__sk_mem_schedule函数分配新的额度。
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); } 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); }
另外一个预分配缓存额度的函数为sk_forced_mem_schedule,与以上的__sk_mem_schedule函数不同,如果内存额度不够,其强制进行缓存额度的预分配,而不管是否超出网络协议的内存限定。用在比如FIN报文发送等情况下,其不必等待可尽快结束一个连接,否则可能导致FIN报文的延迟或者放弃发送FIN而关闭连接,其结束后又可释放连接的缓存占用。
/* We allow to exceed memory limits for FIN packets to expedite * connection tear down and (memory) recovery. * Otherwise tcp_send_fin() could be tempted to either delay FIN * or even be forced to close flow without any FIN. * In general, we want to allow one skb per socket to avoid hangs * with edge trigger epoll() */ void sk_forced_mem_schedule(struct sock *sk, int size) { int amt; 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); if (mem_cgroup_sockets_enabled && sk->sk_memcg) mem_cgroup_charge_skmem(sk->sk_memcg, amt); }
sk_forward_alloc预分配额度使用
sk_forward_alloc预分配额度使用有一对sk_mem_charge和sk_mem_uncharge函数组成。在得到套接口预分配额度后,函数sk_mem_charge可由额度中获取一定量的数值使用。
static inline void sk_mem_charge(struct sock *sk, int size) { if (!sk_has_account(sk)) return; sk->sk_forward_alloc -= size; } static inline void sk_mem_uncharge(struct sock *sk, int size) { if (!sk_has_account(sk)) return; sk->sk_forward_alloc += size; /* Avoid a possible overflow. * TCP send queues can make this happen, if sk_mem_reclaim() * is not called and more than 2 GBytes are released at once. * * If we reach 2 MBytes, reclaim 1 MBytes right now, there is * no need to hold that much forward allocation anyway. */ if (unlikely(sk->sk_forward_alloc >= 1 << 21)) __sk_mem_reclaim(sk, 1 << 20); }
函数sk_mem_uncharge可将一定量回填到预分配额度中。如果sk_forward_alloc预分配额度大于2M字节,立即回收1M字节,预分配额度没有必要太大,立即回收以防止sk_mem_reclain回收不及时导致溢出。
在清空发送队列函数tcp_write_queue_purge和清空重传队列函数tcp_rtx_queue_purge,以及重传队列元素移除函数tcp_rtx_queue_unlink_and_free中调用sk_wmem_free_skb释放skb,并且uncharge预分配的额度。
static inline void sk_wmem_free_skb(struct sock *sk, struct sk_buff *skb) { sk_wmem_queued_add(sk, -skb->truesize); sk_mem_uncharge(sk, skb->truesize); if (static_branch_unlikely(&tcp_tx_skb_cache_key) && !sk->sk_tx_skb_cache && !skb_cloned(skb)) { skb_ext_reset(skb); skb_zcopy_clear(skb, true); sk->sk_tx_skb_cache = skb; return; } __kfree_skb(skb); }
sk_forward_alloc预分配额度回收
基础的回收函数为__sk_mem_reclaim,以上介绍的sk_mem_uncharge函数也会使用到。除此之外,内核使用两个函数回收预分配内存额度:分别为sk_mem_reclaim和sk_mem_reclaim_partial函数。前者回收之后有可能将额度全部回收或者仅留下小于SK_MEM_QUANTUM大小的额度;而后者不会全部回收额度,其在额度大于SK_MEM_QUANTUM时,执行回收操作,并且保证留下小于SK_MEM_QUANTUM大小的额度。
static inline void sk_mem_reclaim(struct sock *sk) { if (!sk_has_account(sk))
/* TCP层是有统计内存使用的,所以条件为假 */
return;
if (sk->sk_forward_alloc >= SK_MEM_QUANTUM) __sk_mem_reclaim(sk, sk->sk_forward_alloc);
}
static inline void sk_mem_reclaim_partial(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 - 1);
}
/** * __sk_mem_reclaim - reclaim sk_forward_alloc and memory_allocated
* @sk: socket
* @amount: number of bytes (rounded down to a SK_MEM_QUANTUM multiple) */
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); }
sk_forward_alloc预分配时机
在TCP重要的skb缓存分配函数sk_stream_alloc_skb中,如果TCP协议总的内存处于承压状态,首先回收部分预分配缓存,因为马上要为skb分配内存,不应进行全部回收。在分配skb之后有两种情况,如果指定了强制分配force_schedule参数,即强制增加分配额度而不进行内存超限判断;否则,使用sk_wmem_schedule进行额度分配。只有在分配额度成功之后返回分配的skb,反之释放skb返回失败
struct sk_buff *sk_stream_alloc_skb(struct sock *sk, int size, gfp_t gfp, bool force_schedule) { struct sk_buff *skb; if (likely(!size)) { skb = sk->sk_tx_skb_cache; if (skb) { skb->truesize = SKB_TRUESIZE(skb_end_offset(skb)); sk->sk_tx_skb_cache = NULL; pskb_trim(skb, 0); INIT_LIST_HEAD(&skb->tcp_tsorted_anchor); skb_shinfo(skb)->tx_flags = 0; memset(TCP_SKB_CB(skb), 0, sizeof(struct tcp_skb_cb)); return skb; } } /* The TCP header must be at least 32-bit aligned. */ size = ALIGN(size, 4); if (unlikely(tcp_under_memory_pressure(sk))) sk_mem_reclaim_partial(sk); skb = alloc_skb_fclone(size + sk->sk_prot->max_header, gfp); if (likely(skb)) { bool mem_scheduled; if (force_schedule) { mem_scheduled = true; sk_forced_mem_schedule(sk, skb->truesize); } else { mem_scheduled = sk_wmem_schedule(sk, skb->truesize); } if (likely(mem_scheduled)) { skb_reserve(skb, sk->sk_prot->max_header); /* * Make sure that we have exactly size bytes * available to the caller, no more, no less. */ skb->reserved_tailroom = skb->end - skb->tail - size; INIT_LIST_HEAD(&skb->tcp_tsorted_anchor); return skb; } __kfree_skb(skb); } else { sk->sk_prot->enter_memory_pressure(sk); sk_stream_moderate_sndbuf(sk); } return NULL; }
对于sk_stream_alloc_skb函数的使用,发生在TCP发送路径上比如tcp_sendmsg_locked和do_tcp_sendpages发送函数,分片函数tcp_fragment和tso_fragment,以及tcp_mtu_probe、tcp_send_syn_data和tcp_connect函数。sk_stream_alloc_skb函数获取到了相应的缓存额度,紧接其后就需要使用此额度。如函数tcp_mtu_probe,其调用sk_mem_charge使用了skb的truesize长度的分配额度。
void tcp_close(struct sock *sk, long timeout) { sk_mem_reclaim(sk); } void inet_sock_destruct(struct sock *sk) { struct inet_sock *inet = inet_sk(sk); sk_mem_reclaim(sk); }
TCP的延时ACK处理函数tcp_delack_timer_handler,首先会调用函数sk_mem_reclaim_partial回收部分预分配额度,在执行最后,如果网络协议内存处于承压状态,还会调用sk_mem_reclaim回收函数。另外,在TCP超时重传函数tcp_write_timer_handler和keepalive超时函数中也有调用sk_mem_reclaim回收函数。
sk_forward_alloc超限判断
在函数__sk_mem_schedule预分配额度时,使用函数__sk_mem_raise_allocated判断TCP协议内存是否超过限定值。如下在协议内存承压状态下,如果当前套接口的发送队列缓存、接收缓存已经预分配缓存之和所占用的页面数,乘以当前套接口协议的所有套接口数量,小于系统设定的最大协议内存限值的话(TCP协议:/proc/sys/net/ipv4/tcp_mem),说明还有内存空间可供分配使用。
/**
* __sk_mem_raise_allocated - increase memory_allocated
* @sk: socket
* @size: memory size to allocate
* @amt: pages to allocate
* @kind: allocation type
*1. 如果TCP的内存使用量低于最小值sysctl_tcp_mem[0],就清零TCP的内存压力标志tcp_memory_pressure。
2. 如果TCP的内存使用量高于压力值sysclt_tcp_mem[1],把TCP的内存压力标志tcp_memory_pressure置为1。
3. 如果TCP的内存使用量高于最大值sysctl_tcp_mem[2],就减小sock发送缓存的上限sk->sk_sndbuf。
* Similar to __sk_mem_schedule(), but does not update sk_forward_alloc
*/
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);
bool charged = true;
if (mem_cgroup_sockets_enabled && sk->sk_memcg &&
!(charged = mem_cgroup_charge_skmem(sk->sk_memcg, amt)))
goto suppress_allocation;
/* Under limit. TCP层的内存使用量低于最小值sysctl_tcp_mem[0]。 */
if (allocated <= sk_prot_mem_limits(sk, 0)) {
sk_leave_memory_pressure(sk);
return 1;
}
/* Under pressure.
* 如果TCP的内存使用量高于压力值sysclt_tcp_mem[1],把TCP层的内存压力标志
* tcp_memory_pressure置为1。
*/
if (allocated > sk_prot_mem_limits(sk, 1))
sk_enter_memory_pressure(sk);
/* Over hard limit.
* 如果TCP层的内存使用量高于最大值sysctl_tcp_mem[2],就减小sock发送缓存的上限
* sk->sk_sndbuf。
*/
if (allocated > sk_prot_mem_limits(sk, 2))
goto suppress_allocation;
/* guarantee minimum buffer size under pressure
不管是在发送还是接收时,都要保证sock至少有sysctl_tcp_{r,w}mem[0]的内存可用
*/
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;
}
}
if (sk_has_memory_pressure(sk)) {
u64 alloc;
/* 如果TCP不处于内存压力状态,直接返回 */
if (!sk_under_memory_pressure(sk))
return 1;
alloc = sk_sockets_allocated_read_positive(sk);
/* 如果当前socket使用的内存还不是太高时,返回真 */
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;
}
suppress_allocation:
if (kind == SK_MEM_SEND && sk->sk_type == SOCK_STREAM) {
/* 减小sock发送缓冲区的上限,使得sndbuf不超过发送队列总大小的一半,
* 不低于两个数据包的MIN_TRUESIZE。
*/
sk_stream_moderate_sndbuf(sk);
/* Fail only if socket is _under_ its sndbuf.
* In this case we cannot block, so that we have to fail.
*/
if (sk->sk_wmem_queued + size >= sk->sk_sndbuf)
return 1;
}
if (kind == SK_MEM_SEND || (kind == SK_MEM_RECV && charged))
trace_sock_exceed_buf_limit(sk, prot, allocated, kind);
sk_memory_allocated_sub(sk, amt);
if (mem_cgroup_sockets_enabled && sk->sk_memcg)
mem_cgroup_uncharge_skmem(sk->sk_memcg, amt);
return 0;
}
TCP的核心预分配缓存额度函数为tcp_try_rmem_schedule,如果无法分配缓存额度,将首先调用tcp_prune_queue函数尝试合并sk_receive_queue中的数据包skb以减少空间占用,如果空间仍然不足,最后调用tcp_prune_ofo_queue函数清理乱序数据包队列(out_of_order_queue)。
static int tcp_try_rmem_schedule(struct sock *sk, struct sk_buff *skb,
unsigned int size)
{
if (atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf ||
!sk_rmem_schedule(sk, skb, size)) {
if (tcp_prune_queue(sk) < 0)
return -1;
while (!sk_rmem_schedule(sk, skb, size)) {
if (!tcp_prune_ofo_queue(sk))
return -1;
}
}
return 0;
}
函数tcp_prune_queue和tcp_prune_ofo_queue在清理空间之后,都会使用函数sk_mem_reclaim回收空间。
函数tcp_try_rmem_schedule在TCP的接收路径中调用,比如tcp_data_queue函数和tcp_data_queue_ofo函数,以及用于TCP套接口REPAIR模式的tcp_send_rcvq函数。如下为tcp_data_queue函数,如果接收队列sk_receive_queue为空,使用sk_forced_mem_schedule函数强制分配缓存额度,否则,使用tcp_try_rmem_schedule函数进行正常分配并检查缓存是否超限。最后使用tcp_queue_rcv函数完成接收工作,同时通过调用skb_set_owner_r函数使用分配的缓存额度。函数tcp_send_rcvq的缓存相关操作与此类似。
//函数tcp_data_queue_ofo与以上两者的不同在于,其直接使用skb_set_owner_r函数使用预分配的缓存额度。
static void tcp_data_queue_ofo(struct sock *sk, struct sk_buff *skb)
{
if (unlikely(tcp_try_rmem_schedule(sk, skb, skb->truesize))) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPOFODROP);
sk->sk_data_ready(sk);
tcp_drop(sk, skb);
return;
}
--------------------------------
end:
if (skb) {
/* For non sack flows, do not grow window to force DUPACK
* and trigger fast retransmit.
*/
if (tcp_is_sack(tp))
tcp_grow_window(sk, skb);
skb_condense(skb);
skb_set_owner_r(skb, sk);
}
}
函数tcp_data_queue_ofo与以上两者的不同在于,其直接使用skb_set_owner_r函数使用预分配的缓存额度。
sk_forward_alloc额度的使用与回填
预分配额度使用函数skb_set_owner_r将skb的销毁回调函数destructor设置为sock_rfree函数,其调用sk_mem_uncharge函数将skb占用的缓存回填到预分配额度sk_forward_alloc中。
sk_forward_alloc预分配额度使用由一对sk_mem_charge和sk_mem_uncharge函数组成。在得到套接口预分配额度后,函数sk_mem_charge可由额度中获取一定量的数值使用,函数sk_mem_uncharge可释放一定的额度。
sk_mem_charge函数假定预分配额度足够使用,两个函数都不会做缓存超限判断。
另外,与发送路径上的函数skb_set_owner_w类似,内核使用skb_set_owner_r封装了sk_mem_charge函数,用于接收路径。
/*
* Read buffer destructor automatically called from kfree_skb.
*/
void sock_rfree(struct sk_buff *skb)
{
struct sock *sk = skb->sk;
unsigned int len = skb->truesize;
atomic_sub(len, &sk->sk_rmem_alloc);
sk_mem_uncharge(sk, len);
}
在TCP接收过程中,内核尝试将新接收到的数据包合并到之前的数据包中(sk_receive_queue接收队列的末尾数据包),参见函数tcp_try_coalesce,如果合并成功,
将释放被合并的skb所占用的缓存,所以其skb结构体所占用缓存空间不需要使用缓存额度,仅需要计算其数据部分的缓存额度(delta)。
/**
* tcp_try_coalesce - try to merge skb to prior one
* @sk: socket
* @to: prior buffer
* @from: buffer to add in queue
* @fragstolen: pointer to boolean
*
* Before queueing skb @from after @to, try to merge them
* to reduce overall memory use and queue lengths, if cost is small.
* Packets in ofo or receive queues can stay a long time.
* Better try to coalesce them right now to avoid future collapses.
* Returns true if caller should free @from instead of queueing it
*/
static bool tcp_try_coalesce(struct sock *sk,
struct sk_buff *to,
struct sk_buff *from,
bool *fragstolen)
{
int delta;
*fragstolen = false;
-----------------
if (!skb_try_coalesce(to, from, fragstolen, &delta))
return false;
atomic_add(delta, &sk->sk_rmem_alloc);
sk_mem_charge(sk, delta);
----------------------
return true;
}
如果合并失败,内核将skb添加到接收队列sk_receive_queue中,使用skb_set_owner_r函数将整个skb占用的缓存空间计入缓存额度。
static int __must_check tcp_queue_rcv(struct sock *sk, struct sk_buff *skb,
bool *fragstolen)
{
int eaten;
struct sk_buff *tail = skb_peek_tail(&sk->sk_receive_queue);
eaten = (tail &&
tcp_try_coalesce(sk, tail,
skb, fragstolen)) ? 1 : 0;
tcp_rcv_nxt_update(tcp_sk(sk), TCP_SKB_CB(skb)->end_seq);
if (!eaten) {
__skb_queue_tail(&sk->sk_receive_queue, skb);
skb_set_owner_r(skb, sk);
}
return eaten;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
2022-06-07 安全编译记录