2. 内核内存管理基础
在深入网络栈内存泄漏定位之前,必须牢固掌握内核内存管理的核心机制。网络协议栈的每一个数据包(sk_buff)、套接字结构(struct sock)、连接跟踪条目,最终都落脚于物理内存页或 slab 缓存上。本章将建立从硬件到内核分配器的完整知识图谱,为后续使用 kmemleak 和 slab 调试工具打下坚实基础。
2.1 物理内存与虚拟内存:内核的地址空间模型
内核运行在 CPU 的最高特权级(Ring 0),拥有独立的地址空间。理解物理内存与虚拟内存的映射关系,是分析内存泄漏的前提。
| 概念 | 定义 | 内核视角的关键点 |
|---|---|---|
| 物理内存 | 硬件 RAM 芯片提供的实际存储单元,以字节编址。 | 通过 struct page 管理每个物理页框;存在 ZONE_DMA、ZONE_NORMAL、ZONE_HIGHMEM 等分区。 |
| 虚拟内存 | CPU 通过 MMU 提供的线性地址空间,每个进程拥有独立的 4GB(32位)或巨大空间(64位)。 | 内核空间(高地址)与用户空间(低地址)隔离;内核直接映射区(lowmem)将物理内存线性映射到虚拟地址。 |
| 页表 | MMU 使用的多级映射表,将虚拟页号转换为物理页框号。 | 缺页异常(page fault)是动态分配物理页的入口;大页(HugeTLB)可减少 TLB 压力。 |
调试启示: 网络驱动中的 DMA 缓冲区通常需要物理连续内存,而协议栈的 sk_buff 数据区则可能使用虚拟地址。泄漏发生时,需要区分是物理页泄漏(伙伴系统)还是 slab 对象泄漏(slab 分配器)。
2.2 页分配器与伙伴系统
伙伴系统(Buddy System)是内核最底层的物理页框分配算法,负责管理连续物理内存的分配与回收。所有 slab 缓存、vmalloc、直接页映射最终都依赖伙伴系统提供物理页。
2.2.1 核心数据结构
- struct page:每个物理页框对应一个
struct page,存储在全局数组mem_map中。包含_refcount(引用计数)、_mapcount(映射计数)、flags(PG_locked、PG_slab 等标志)。 - struct zone:每个内存区(DMA/NORMAL/HIGHMEM)由一个
struct zone管理,包含free_area数组(伙伴系统的核心)、watermark(水位线)、pageset(每 CPU 页缓存)。 - free_area:每个 order(0~MAX_ORDER-1)对应一个链表,存储该 order 的空闲页块。
2.2.2 分配与合并流程
- 分配:请求 order N 的连续页块。从
free_area[N]链表获取;若为空,则从 order N+1 分裂一个块,一半返回,另一半挂入 order N 链表。 - 释放:释放 order N 页块时,检查其伙伴(buddy)是否空闲。若伙伴空闲且 order 相同,则合并为 order N+1 块,递归向上合并。
- 每 CPU 页缓存:为单页分配(order=0)提供热/冷页缓存,减少锁竞争。
网络栈关联: 网卡驱动通过
alloc_pages() 或 __get_free_pages() 分配 DMA 接收环缓冲区。如果驱动在卸载或重置时未正确释放这些页,将导致伙伴系统泄漏,表现为 /proc/buddyinfo 中特定 order 的可用块持续减少。
2.3 slab 分配器概述
伙伴系统以页为单位分配,但内核中大量对象(如 struct sk_buff、struct sock、struct inode)只有几十到几百字节。slab 分配器在伙伴系统之上提供对象缓存,避免频繁的页分配与释放,同时提高缓存亲和性。
2.3.1 slab 分配器的三种实现
| 实现 | 引入版本 | 特点 | 调试支持 |
|---|---|---|---|
| SLOB | 早期 | 简单链表,适合小内存系统(嵌入式) | 弱 |
| SLAB | 2.0 | 经典实现,每 CPU 队列 + 着色(coloring)优化缓存 | 强(slabinfo、slabtop) |
| SLUB | 2.6.22 | 默认实现,简化 per-CPU 结构,减少元数据开销 | 强(/sys/kernel/slab/、kmemleak 集成) |
调试注意: 现代内核(2.6.22+)默认使用 SLUB。SLUB 将 slab 元数据直接存放在页框内,并通过
/sys/kernel/slab/<cache_name>/ 暴露每个缓存的详细统计。kmemleak 与 SLUB 深度集成,可以追踪未释放对象的分配栈。
2.3.2 核心数据结构与操作
- struct kmem_cache:描述一个对象缓存,包含对象大小、对齐、构造函数、
struct kmem_cache_node(NUMA 节点)、struct kmem_cache_cpu(每 CPU 空闲对象指针)。 - struct slab / struct page:SLUB 中每个 slab 由一个
struct page表示,page->freelist指向空闲对象链表,page->inuse记录已分配对象数。 - 分配路径:
kmem_cache_alloc()→ 从 per-CPU 空闲列表获取 → 若空则从 slab 节点获取 → 若空则从伙伴系统分配新 slab 页。 - 释放路径:
kmem_cache_free()→ 将对象放回 per-CPU 空闲列表 → 若 slab 完全空闲则释放回伙伴系统。
2.3.3 网络栈中的典型 slab 缓存
| 缓存名称 | 对象类型 | 典型大小 | 泄漏风险点 |
|---|---|---|---|
skbuff_head_cache | struct sk_buff | 256~320 字节 | skb 未释放(协议栈或驱动) |
sock_inode_cache | struct socket + inode | ~1024 字节 | 套接字未关闭 |
tcp_sock | struct tcp_sock | ~2048 字节 | TCP 连接未清理 |
net_namespace | struct net | ~4096 字节 | 网络命名空间泄漏 |
fib_trie | 路由表节点 | ~64 字节 | 路由规则未释放 |
2.4 内存分配路径总结:从 kmalloc 到物理页
网络栈中最常用的 kmalloc() 和 kzalloc() 实际上是 slab 分配器的封装。理解完整的调用链有助于定位泄漏发生在哪一层。
kmalloc(size, flags)
└─ __kmalloc(size, flags)
└─ slab_alloc() // 或 slub_alloc()
├─ 从 per-CPU freelist 获取对象
├─ 若空:从 slab 节点获取新 slab
│ └─ new_slab()
│ └─ allocate_slab()
│ └─ __alloc_pages_nodemask() // 伙伴系统
│ └─ rmqueue() → 从 free_area 摘取页块
└─ 返回对象地址
实践建议: 当怀疑网络栈内存泄漏时,首先通过
/proc/slabinfo 或 slabtop 观察 skbuff_head_cache、tcp_sock 等缓存的对象数是否持续增长。若增长,再使用 kmemleak 定位具体分配栈。若 slabinfo 显示正常但系统可用内存持续下降,则可能是伙伴系统级别的页泄漏,需检查 /proc/buddyinfo 和 /proc/pagetypeinfo。
2.5 本章小结
- 内核内存管理分为三层:伙伴系统(物理页框)→ slab 分配器(对象缓存)→ kmalloc/vmalloc(通用接口)。
- 网络栈内存泄漏通常表现为 slab 缓存对象数持续增长(如
skbuff_head_cache),少数情况为伙伴系统页泄漏(如 DMA 缓冲区未释放)。 - SLUB 是当前默认 slab 实现,通过
/sys/kernel/slab/提供细粒度调试接口,与 kmemleak 深度集成。 - 掌握
struct page、struct zone、struct kmem_cache等核心数据结构,是理解后续 kmemleak 输出和 slab 调试命令的基础。