tcpclose 中tcp_eat_recv_skb替换__kfree_skb
旧代码的问题:__kfree_skb() 直接释放,代价巨大
以前在 __tcp_close() 清理接收队列时写的是:
while ((skb = __skb_dequeue(&sk->sk_receive_queue)) != NULL) {
__kfree_skb(skb);
}
如果一个应用开启了很多 TCP 连接(攻击场景就是大量半开或大量未读数据的连接),当它关闭 socket 时:
🔥 每个连接都需要对 所有未读的 SKB 调用 _kfree_skb()
问题来了:
kfree_skb() 会触发复杂的 free 逻辑:
-
可能触发 page frag 回收
-
atomic 操作(refcount)
-
skb->destructor 操作
-
cache line 冲突
-
cross-CPU free(NUMA overhead)
-
触发 tracepoint
- 进程上下文: 用户调用
close()。 - 上锁: 为了安全,内核执行
local_bh_disable()。此时,这颗 CPU 上的软中断被禁止运行。 - 干苦力 (
__kfree_skb): 用户进程开始循环调用__kfree_skb释放几万个包。这个过程很慢(涉及内存操作、锁竞争)。假设耗时 100ms。 - 解锁: 清理完,执行
local_bh_enable()。
所以,__kfree_skb 的罪过不在于它制造了软中断,而在于它霸占了 CPU 时间,导致已经被硬件触发的软中断无法得到调度。
特殊情况:什么时候释放 SKB 会触发软中断?
为了严谨,有一种特殊情况下的释放函数是会利用软中断的,但不是 __tcp_close 里用的这个。
-
函数:
dev_kfree_skb_irq() -
场景: 当你在硬中断(Hard IRQ) 上下文中(比如网卡驱动里)想要释放一个 skb 时。
-
行为: 因为硬中断里不能做太复杂、耗时的事情,也不能睡眠,所以这个函数不会立刻释放内存。它会把 skb 挂到一个列表里,然后触发(Raise)
NET_TX_SOFTIRQ软中断。等硬中断结束,软中断运行的时候,再由软中断去慢慢释放内存。
为什么 tcp_eat_recv_skb 更优(核心所在:会计 + 延迟/批量释放,降低原子争抢)
你的指正关于「核 心差异是通用昂贵的 accounting vs TCP 专用廉价的 accounting」完全正确 —— 这是要点。
-
无论是
__kfree_skb()还是tcp_eat_recv_skb(),都必须保证 skb 被释放时执行必要的析构 / 会计(也就是确保sk->sk_rmem_alloc等被正确减少)。这是不可省略的。 -
区别在于如何做这些会计与释放:
-
通用路径往往为每个 skb 做原子计数/全局更新(例如针对
sk_rmem_alloc的原子减),并可能触发昂贵的跨 CPU 原子操作或 wakeups —— 在大量 skb 同时释放时,这会造成原子争用/缓存线抖动。 -
tcp_eat_recv_skb()的实现先行做 socket 侧的会计调整(调用sock_rfree()或对 socket 的局部预留做回收),然后把实际的内存释放交给skb_attempt_defer_free()—— 该函数会尝试把 free 操作延迟到合适的上下文/批量处理(per-CPU defer list / NAPI 时批量回收等)。这能把大量单次昂贵的原子/释放操作,转换成更少次且更友好的批量操作,从而大幅降低在 close 大量 socket(或遭到“未读数据关闭”攻击)时的开销。
-
-
也就是说:真正的优化点是减少每个 skb 的全局原子争用与即时重释放,改为 socket-local或批量延迟释放,从而降低 CPU/cache 抖动与原子成本(这正是你指出的“通用昂贵的 accounting vs TCP 专用廉价的 accounting”)。相关内核字段(像
sk_forward_alloc、sk_rmem_alloc等)就是内核里用于跟踪 socket 层内存配额的指标。
static void tcp_eat_recv_skb(struct sock *sk, struct sk_buff *skb) { __skb_unlink(skb, &sk->sk_receive_queue); if (likely(skb->destructor == sock_rfree)) { sock_rfree(skb); skb->destructor = NULL; skb->sk = NULL; return skb_attempt_defer_free(skb); } __kfree_skb(skb); }
skb_attempt_defer_free() 的核心目标是:
把 skb 的释放工作“挪回”到它最初分配的 CPU 上,以减少 cache line 抖动、NUMA 远程访问、内存 zone 锁竞争,并通过批量方式完成释放。
❌ 传统做法的问题
当 skb 在 CPU A 分配,却在 CPU B 释放 时:
-
slab cache 跨 CPU 修改
-
cache line bouncing
-
NUMA 远程内存访问
-
page allocator / zone lock 争用
-
atomic refcount 热点
在高 PPS(包/秒)或攻击流量下非常致命
/** * skb_attempt_defer_free - queue skb for remote freeing * @skb: buffer * * Put @skb in a per-cpu list, using the cpu which * allocated the skb/pages to reduce false sharing * and memory zone spinlock contention. */ void skb_attempt_defer_free(struct sk_buff *skb) { struct skb_defer_node *sdn;//每 CPU + 每 NUMA node 的 defer 队列 unsigned long defer_count;//当前 defer 队列长度 int cpu = skb->alloc_cpu;//skb 最初分配它的 CPU unsigned int defer_max;//defer 队列最大允许值 bool kick;//是否需要 kick 远端 CPU 处理 if (cpu == raw_smp_processor_id() || WARN_ON_ONCE(cpu >= nr_cpu_ids) || !cpu_online(cpu)) {//已经是“正确 CPU 没必要 defer,直接 free /**alloc_cpu 已 offline defer 会永远不被处理*/ nodefer: kfree_skb_napi_cache(skb);//仍然是 NAPI cache / bulk free 优化路径 return; } //即使 fallback,也尽量便宜 /*确保: 没有 dst 引用 destructor 已清空 没有 nf_conntrack 绑定 */ DEBUG_NET_WARN_ON_ONCE(skb_dst(skb)); DEBUG_NET_WARN_ON_ONCE(skb->destructor); DEBUG_NET_WARN_ON_ONCE(skb_nfct(skb)); /*CPU + NUMA 双维度 per_cpu_ptr(..., cpu) → alloc_cpu 的 defer 队列 + numa_node_id() → 对应 NUMA 节点 skb 回到“它的内存 zone slab/page allocator 锁争用最小*/ sdn = per_cpu_ptr(net_hotdata.skb_defer_nodes, cpu) + numa_node_id(); defer_max = READ_ONCE(net_hotdata.sysctl_skb_defer_max); defer_count = atomic_long_inc_return(&sdn->defer_count); /*超过阈值,立刻回退到直接 free defer 是优化,不是必须*/ if (defer_count >= defer_max) goto nodefer; llist_add(&skb->ll_node, &sdn->defer_list); /*真正 defer 行为 使用 lockless llist(RCU-safe) 将 skb 挂到 alloc_cpu 的 defer_list 上 当前 CPU 不释放 skb*/ /* Send an IPI every time queue reaches half capacity. IPI 限速机制 defer 队列达到一半时,触发一次 IPI 避免每次 defer 都发 IPI(灾难) */ kick = (defer_count - 1) == (defer_max >> 1); /* Make sure to trigger NET_RX_SOFTIRQ on the remote CPU * if we are unlucky enough (this seems very unlikely). 触发远端 CPU;给 alloc_cpu 发 IPI 确保它尽快运行: NET_RX_SOFTIRQ 或 defer_list_purge() 这是 兜底机制---.“万一远端 CPU 一直不处理 defer 队列怎么办?” */ if (unlikely(kick)) kick_defer_list_purge(cpu); }

浙公网安备 33010602011771号