tcpclose 中tcp_eat_recv_skb替换__kfree_skb

https://git.kernel.org/pub/scm/linux/kernel/git/netdev/net-next.git/commit/net/ipv4?id=b13592d20b210976a0946adf027b7bd9d7734326

旧代码的问题:__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

  1. 进程上下文: 用户调用 close()
  2. 上锁: 为了安全,内核执行 local_bh_disable()此时,这颗 CPU 上的软中断被禁止运行。
  3. 干苦力 (__kfree_skb): 用户进程开始循环调用 __kfree_skb 释放几万个包。这个过程很慢(涉及内存操作、锁竞争)。假设耗时 100ms。
  4. 解锁: 清理完,执行 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_allocsk_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);
}

 

 

 

posted @ 2025-12-12 14:48  codestacklinuxer  阅读(0)  评论(0)    收藏  举报