在网络编程中,像 TCP 或 UDP 这样的协议通常会对套接字的缓冲区内存使用进行统计,并将消耗记录在由 sk->sk_proto->memory_allocated 指向的协议全局计数器里。然而,某些特定的系统进程可能不希望受到这类全局限制的约束。
一个类似的场景已经在内存控制组(memcg)中得到了解决:内核提供了 SO_RESERVE_MEM 套接字选项,可以让隶属于某个 memcg 的套接字绕过其内存预留限制。那么,对于同样由 memcg 管理的套接字,如果能同时绕过协议级别的内存统计,岂不是可以避免双重统计带来的额外开销?在后续相关的 BPF 补丁中,将会通过一个微基准测试来展示这种优化带来的性能提升。
这个补丁引入了一个简单的机制:当 sk->sk_bypass_prot_mem 标志位被置为真(true)时,该套接字的内存分配与释放操作将不再计入其所属协议的全局统计中。
从实现细节来看,sk->sk_bypass_prot_mem 与 sk->sk_prot 位于同一个 CPU 缓存行中。由于 sk_has_account() 这类函数在访问 sk->sk_bypass_prot_mem 之前总是会先获取 sk->sk_prot,因此新增这个标志位几乎不会引入额外的缓存未命中惩罚,设计上考虑了性能影响。
后续的补丁会负责在适当的上下文中将 sk->sk_bypass_prot_mem 设置为真,从而实现跳过协议内存统计的目标。需要明确的是,此机制不会禁用内存控制组(memcg)的功能,它仅仅影响各协议自身的内存统计。
在网络/系统层面的底层开发中,内存管理的优化常常是提升性能的关键。另一种不利用 struct sock_common 结构中空闲位的实现方案,是创建类似 tcp_prot_bypass 这样的 sk_prot 变体。但这种方法会使 SOCKMAP 逻辑、tcp_bpf_prots 等组件变得复杂,因此当前采用的标志位方案更为简洁高效。
此补丁由 Kuniyuki Iwashima 提交,并经过了 Martin KaFai Lau、Shakeel Butt、Eric Dumazet 和 Roman Gushchin 的审阅与认可。
代码变更概览 (Diffstat)
-rw-r--r-- include/net/proto_memory.h 3
-rw-r--r-- include/net/sock.h 3
-rw-r--r-- include/net/tcp.h 3
-rw-r--r-- net/core/sock.c 32
-rw-r--r-- net/ipv4/tcp.c 3
-rw-r--r-- net/ipv4/tcp_output.c 7
-rw-r--r-- net/mptcp/protocol.c 7
-rw-r--r-- net/tls/tls_device.c 3
主要代码改动
diff --git a/include/net/proto_memory.h b/include/net/proto_memory.h
index 8e91a8fa31b52a..ad6d703ce6fe1d 100644
--- a/include/net/proto_memory.h
+++ b/include/net/proto_memory.h
@@ -35,6 +35,9 @@ static inline bool sk_under_memory_pressure(const struct sock *sk)
mem_cgroup_sk_under_memory_pressure(sk))
return true;
+ if (sk->sk_bypass_prot_mem)
+ return false;
+
return !!READ_ONCE(*sk->sk_prot->memory_pressure);
}
diff --git a/include/net/sock.h b/include/net/sock.h
index 30ac2eb4ef9bf7..415e7381aa5051 100644
--- a/include/net/sock.h
+++ b/include/net/sock.h
@@ -118,6 +118,7 @@ typedef __u64 __bitwise __addrpair;
* @skc_reuseport: %SO_REUSEPORT setting
* @skc_ipv6only: socket is IPV6 only
* @skc_net_refcnt: socket is using net ref counting
+ * @skc_bypass_prot_mem: bypass the per-protocol memory accounting for skb
* @skc_bound_dev_if: bound device index if != 0
* @skc_bind_node: bind hash linkage for various protocol lookup tables
@@ -174,6 +175,7 @@ struct sock_common {
unsigned char skc_reuseport:1;
unsigned char skc_ipv6only:1;
unsigned char skc_net_refcnt:1;
+ unsigned char skc_bypass_prot_mem:1;
int skc_bound_dev_if;
union {
struct hlist_node skc_bind_node;
@@ -381,6 +383,7 @@ struct sock {
#define sk_reuseport __sk_common.skc_reuseport
#define sk_ipv6only __sk_common.skc_ipv6only
#define sk_net_refcnt __sk_common.skc_net_refcnt
+#define sk_bypass_prot_mem __sk_common.skc_bypass_prot_mem
#define sk_bound_dev_if __sk_common.skc_bound_dev_if
#define sk_bind_node __sk_common.skc_bind_node
#define sk_prot __sk_common.skc_prot
diff --git a/net/core/sock.c b/net/core/sock.c
index 08ae20069b6d28..5bf208579c02bc 100644
--- a/net/core/sock.c
+++ b/net/core/sock.c
@@ -1046,9 +1046,13 @@ static int sock_reserve_memory(struct sock *sk, int bytes)
if (!charged)
return -ENOMEM;
+ if (sk->sk_bypass_prot_mem)
+ goto success;
+
/* pre-charge to forward_alloc */
sk_memory_allocated_add(sk, pages);
allocated = sk_memory_allocated(sk);
+
/* If the system goes into memory pressure with this
* precharge, give up and return error.
*/
@@ -1057,6 +1061,8 @@ static int sock_reserve_memory(struct sock *sk, int bytes)
mem_cgroup_sk_uncharge(sk, pages);
return -ENOMEM;
}
+
+success:
sk_forward_alloc_add(sk, pages << PAGE_SHIFT);
WRITE_ONCE(sk->sk_reserved_mem,
@@ -3263,10 +3269,12 @@ int __sk_mem_raise_allocated(struct sock *sk, int size, int amt, int kind)
{
bool memcg_enabled = false, charged = false;
struct proto *prot = sk->sk_prot;
- long allocated;
-
- sk_memory_allocated_add(sk, amt);
- allocated = sk_memory_allocated(sk);
+ long allocated = 0;
+
+ if (!sk->sk_bypass_prot_mem) {
+ sk_memory_allocated_add(sk, amt);
+ allocated = sk_memory_allocated(sk);
+ }
if (mem_cgroup_sk_enabled(sk)) {
memcg_enabled = true;
@@ -3275,6 +3283,9 @@ int __sk_mem_raise_allocated(struct sock *sk, int size, int amt, int kind)
goto suppress_allocation;
}
+ if (!allocated)
+ return 1;
+
/* Under limit. */
if (allocated <= sk_prot_mem_limits(sk, 0)) {
sk_leave_memory_pressure(sk);
@@ -3353,7 +3364,8 @@ suppress_allocation:
trace_sock_exceed_buf_limit(sk, prot, allocated, kind);
- sk_memory_allocated_sub(sk, amt);
+ if (allocated)
+ sk_memory_allocated_sub(sk, amt);
if (charged)
mem_cgroup_sk_uncharge(sk, amt);
@@ -3392,11 +3404,14 @@ EXPORT_SYMBOL(__sk_mem_schedule);
*/
void __sk_mem_reduce_allocated(struct sock *sk, int amount)
{
- sk_memory_allocated_sub(sk, amount);
-
if (mem_cgroup_sk_enabled(sk))
mem_cgroup_sk_uncharge(sk, amount);
+ if (sk->sk_bypass_prot_mem)
+ return;
+
+ sk_memory_allocated_sub(sk, amount);
+
if (sk_under_global_memory_pressure(sk) &&
(sk_memory_allocated(sk) < sk_prot_mem_limits(sk, 0)))
sk_leave_memory_pressure(sk);
以上是核心改动摘要,其他文件(如 tcp.c, tcp_output.c, mptcp/protocol.c, tls/tls_device.c)的修改逻辑类似,都是在相应的内存分配、压力状态判断路径中增加了对 sk_bypass_prot_mem 标志位的检查。
这项改动是基础 & 综合性性能优化的一部分,为特定的高性能或精细化内存管理场景提供了更大的灵活性。想了解更多Linux内核与网络编程的深度解析,欢迎到云栈社区参与讨论。
来源
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=7c268eaeec63