4. 网络栈内存分配路径:sk_buff结构、net_device、socket内核对象、协议控制块的内存来源
网络栈的内存泄漏往往源于对核心对象生命周期管理不当。本章深入剖析网络栈中四个关键内存对象——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() 系统调用时,内核执行以下分配链:
- sock_alloc():分配
struct socket,并关联一个 inode。该 inode 从sock_inode_cachepslab 缓存分配。 - sk_alloc():分配
struct sock。对于 TCP,实际分配的是struct tcp_sock,它从tcp_sockslab 缓存分配。 - 初始化:协议栈将
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 缓存的完整内存分配路径:
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 状态但未被释放。