4. 网络栈内存分配路径

网络栈的内存泄漏往往源于对核心对象生命周期管理不当。本章深入剖析网络栈中四个关键内存对象——sk_buff、net_device、socket内核对象和协议控制块——的分配源头、slab缓存归属以及释放路径。理解这些对象的“出生证明”是定位泄漏的第一步。

4.1 核心对象内存来源总览

Linux内核网络栈为高频分配的对象维护了专用的slab缓存。下表总结了四个核心对象的分配函数、slab缓存名称及主要所有者:

对象 分配函数 Slab缓存名称 主要分配者/上下文
sk_buff __alloc_skb() skbuff_head_cache 驱动层、协议栈各层
net_device alloc_netdev_mqs() net_device (或私有结构) 驱动初始化、命名空间创建
socket (struct socket) sock_alloc() sock_inode_cachep 系统调用 socket()
协议控制块 (struct sock) sk_alloc() tcp_sock / udp_sock 等 系统调用 socket() / accept()
调试提示: 使用 slabtop -s c 可以实时观察 skbuff_head_cache 和 tcp_sock 等缓存的活跃对象数。如果某个缓存的对象数持续增长且不回落,极有可能存在内存泄漏。

4.2 sk_buff:网络数据的“快递盒”

sk_buff 是网络栈中最核心、分配最频繁的数据结构。每个网络数据包在协议栈中传递时,都封装在一个或多个 sk_buff 中。

4.2.1 分配路径

所有 sk_buff 的分配最终都收敛于 __alloc_skb() 函数。该函数从 skbuff_head_cache slab 缓存中分配 sk_buff 结构体本身,而数据缓冲区则根据大小从 kmalloc 或页面分配器获取。

// net/core/skbuff.c
struct sk_buff *__alloc_skb(unsigned int size, gfp_t gfp_mask,
                            int flags, int node)
{
    struct sk_buff *skb;
    // 1. 从 slab 缓存 skbuff_head_cache 分配 sk_buff 结构
    skb = kmem_cache_alloc_node(skbuff_head_cache, gfp_mask & ~__GFP_DMA, node);
    if (!skb)
        goto out;

    // 2. 分配数据缓冲区
    size = SKB_DATA_ALIGN(size);
    data = kmalloc_reserve(size, gfp_mask, node, &pfmemalloc);
    // ...
    return skb;
}

4.2.2 释放路径

kfree_skb() 和 consume_skb() 是主要的释放接口。前者用于异常路径(丢包),后者用于正常路径。两者最终都会调用 __kfree_skb(),将 sk_buff 归还给 skbuff_head_cache。

泄漏风险点: 驱动层的 NAPI 轮询函数中,如果 netif_receive_skb() 返回错误后没有正确调用 kfree_skb(),或者协议栈某层“吞掉”了 sk_buff 却没有释放,都会导致 skbuff_head_cache 泄漏。

4.3 net_device:网络设备的“身份证”

struct net_device 代表一个网络设备(物理或虚拟)。它的生命周期从驱动加载开始,到设备注销结束。

4.3.1 分配与注册

驱动通过 alloc_netdev_mqs() 分配 net_device。该函数会分配一个包含 net_device 和私有数据的连续内存块。

// net/core/dev.c
struct net_device *alloc_netdev_mqs(int sizeof_priv, const char *name,
                                    void (*setup)(struct net_device *),
                                    unsigned int txqs, unsigned int rxqs)
{
    struct net_device *dev;
    size_t alloc_size;

    // 计算总大小:net_device + 私有数据 + 队列数
    alloc_size = sizeof(struct net_device) + sizeof_priv;
    // 使用 kzalloc 分配连续内存
    dev = kzalloc(alloc_size, GFP_KERNEL);
    if (!dev)
        return NULL;
    // ...
    return dev;
}

4.3.2 释放路径

设备注销时,free_netdev() 被调用。它负责释放 net_device 及其关联的所有资源(如 net_device_stats、dev_addr 等)。

关键检查点: 使用 kmemleak 扫描时,如果发现 net_device 对象泄漏,通常是因为驱动模块卸载时没有调用 unregister_netdev() 和 free_netdev(),或者存在循环引用(如 net_device 的 rx_handler 回调持有自身引用)。

4.4 socket内核对象与协议控制块

用户态创建的每个 socket,在内核中对应两个主要对象:struct socket(VFS 层抽象)和 struct sock(协议控制块)。

4.4.1 分配流程

当用户调用 socket() 系统调用时,内核执行以下分配链:

  1. sock_alloc():分配 struct socket,并关联一个 inode。该 inode 从 sock_inode_cachep slab 缓存分配。
  2. sk_alloc():分配 struct sock。对于 TCP,实际分配的是 struct tcp_sock,它从 tcp_sock slab 缓存分配。
  3. 初始化:协议栈将 socket 的 sk 指针指向 sock,完成绑定。
// net/socket.c
struct socket *sock_alloc(void)
{
    struct inode *inode;
    struct socket *sock;

    // 分配 inode(包含 socket 结构)
    inode = new_inode_pseudo(sock_mnt->mnt_sb);
    sock = SOCKET_I(inode);
    // ...
    return sock;
}

// net/core/sock.c
struct sock *sk_alloc(struct net *net, int family, gfp_t priority,
                      struct proto *prot, int kern)
{
    struct sock *sk;

    // 从协议特定的 slab 缓存分配(如 tcp_sock)
    sk = prot->slab ? kmem_cache_alloc(prot->slab, priority) : NULL;
    // ...
    return sk;
}

4.4.2 释放路径

用户调用 close() 时,内核执行 __sock_release() → sock_close() → sk_common_release()。最终,sock 对象通过 sk_free() 归还给对应的 slab 缓存,socket 对象随 inode 一起释放。

调试技巧: 使用 cat /proc/net/sockstat 可以查看当前系统中各种协议(TCP、UDP)的 socket 数量。如果 TCP: inuse 数值持续增长,说明有 socket 未被正确关闭,对应的 tcp_sock 对象可能泄漏。

4.5 内存分配路径全景图

下图展示了从用户态系统调用到内核 slab 缓存的完整内存分配路径:

网络栈核心对象内存分配路径全景 用户态:socket() / accept() / close() VFS 层:sock_alloc() 协议层:sk_alloc() Slab 缓存层 sock_inode_cachep tcp_sock / udp_sock skbuff_head_cache (struct socket + inode) (struct sock / tcp_sock) (struct sk_buff) 驱动层:alloc_netdev_mqs() net_device (kzalloc 分配) 说明:每个箭头代表一次内存分配调用,slab 缓存层是内存泄漏的高发区域 kmemleak 可以追踪这些对象是否被正确释放

4.6 泄漏场景与排查对照

下表总结了每种核心对象常见的泄漏场景及对应的排查方法:

泄漏对象 典型泄漏场景 排查方法
sk_buff 驱动NAPI轮询未释放;协议层错误路径未调用 kfree_skb() slabtop 观察 skbuff_head_cache;kmemleak 扫描
net_device 驱动模块卸载时未注销设备;rx_handler 循环引用 kmemleak 扫描;检查驱动卸载路径
socket 用户态未调用 close();内核态 socket 创建后未释放 /proc/net/sockstat;lsof 检查文件描述符
协议控制块 TCP 连接未正常关闭;sk_free() 未被调用 slabtop 观察 tcp_sock;ss -s 查看连接状态
实战建议: 在定位网络栈内存泄漏时,建议先通过 slabtop 确定哪个 slab 缓存的对象数异常增长,然后针对性地使用 kmemleak 扫描该缓存的对象。例如,如果 tcp_sock 缓存持续增长,可以编写内核模块遍历 tcp_hashinfo 中的连接表,检查是否有连接处于 TCP_CLOSE 状态但未被释放。