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 分配器)。
内核虚拟地址空间与物理内存映射关系 内核虚拟地址空间与物理内存映射 虚拟地址空间 (内核) 直接映射区 (lowmem) 虚拟地址 = 物理地址 + 固定偏移 (PAGE_OFFSET) vmalloc 区 非连续映射,适用于大块内存 模块/固定映射区 内核模块、kmap、fixmap 栈/陷阱/设备映射 内核栈、IDT、MMIO 物理内存 (RAM) ZONE_NORMAL 直接映射,大部分内核分配 ZONE_HIGHMEM (32位) 需 kmap 临时映射 ZONE_DMA / ZONE_DMA32 低 16MB / 4GB,用于 DMA 保留/硬件保留 ACPI、MMIO、固件 关键点:网络驱动 DMA 缓冲区通常需要 ZONE_DMA 或 ZONE_NORMAL 中的连续物理页

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 分配与合并流程

  1. 分配:请求 order N 的连续页块。从 free_area[N] 链表获取;若为空,则从 order N+1 分裂一个块,一半返回,另一半挂入 order N 链表。
  2. 释放:释放 order N 页块时,检查其伙伴(buddy)是否空闲。若伙伴空闲且 order 相同,则合并为 order N+1 块,递归向上合并。
  3. 每 CPU 页缓存:为单页分配(order=0)提供热/冷页缓存,减少锁竞争。
网络栈关联: 网卡驱动通过 alloc_pages() 或 __get_free_pages() 分配 DMA 接收环缓冲区。如果驱动在卸载或重置时未正确释放这些页,将导致伙伴系统泄漏,表现为 /proc/buddyinfo 中特定 order 的可用块持续减少。
伙伴系统分配与合并流程 伙伴系统分配与合并流程 初始:一个 order-2 块 (4页, 16KB) 请求 order-1 (2页) order-1 块 A (已分配) order-1 块 B (空闲,伙伴) 释放块 A 合并为 order-2 块 (4页, 16KB) 伙伴条件:两个块物理地址连续、order 相同、来自同一个父块 合并条件:伙伴块空闲且 order 相同 → 递归向上合并,减少碎片

2.3 slab 分配器概述

伙伴系统以页为单位分配,但内核中大量对象(如 struct sk_buff、struct sock、struct inode)只有几十到几百字节。slab 分配器在伙伴系统之上提供对象缓存,避免频繁的页分配与释放,同时提高缓存亲和性。

2.3.1 slab 分配器的三种实现

实现引入版本特点调试支持
SLOB早期简单链表,适合小内存系统(嵌入式)弱
SLAB2.0经典实现,每 CPU 队列 + 着色(coloring)优化缓存强(slabinfo、slabtop)
SLUB2.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_cachestruct sk_buff256~320 字节skb 未释放(协议栈或驱动)
sock_inode_cachestruct socket + inode~1024 字节套接字未关闭
tcp_sockstruct tcp_sock~2048 字节TCP 连接未清理
net_namespacestruct net~4096 字节网络命名空间泄漏
fib_trie路由表节点~64 字节路由规则未释放
SLUB 分配器对象缓存结构 SLUB 分配器对象缓存结构 struct kmem_cache name: "skbuff_head_cache" size: 256, align: 64 kmem_cache_cpu (CPU 0) freelist → 对象 A → 对象 B kmem_cache_cpu (CPU 1) freelist → 对象 C struct page (slab) freelist, inuse=5 struct page (slab) freelist, inuse=8 alloc_pages 对象分配路径:kmem_cache_alloc() → per-CPU freelist → slab page → 伙伴系统 释放路径:kmem_cache_free() → per-CPU freelist → slab 全空 → 伙伴系统 网络栈调试关键点 1. 使用 slabtop -s c 查看缓存对象数变化 2. cat /sys/kernel/slab/skbuff_head_cache/objects 获取实时计数 3. kmemleak 扫描未释放对象时,会遍历所有 slab 缓存 4. 开启 CONFIG_SLUB_DEBUG 可启用 red zone 和 poisoning 检测越界

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 调试命令的基础。