一、内核内存管理基础:物理内存与虚拟内存、页框与页表、伙伴系统原理、SLUB分配器原理

大家好,我是你们的老朋友。今天咱们来聊聊内核内存管理这块硬骨头。说实话,我早年刚接触内核时,被这些概念绕得晕头转向。后来踩了不少坑,才慢慢摸清了门道。这一章,我会把最核心的几个概念掰开揉碎了讲给你听。

1.1 物理内存 vs 虚拟内存:一个经典的“障眼法”

先问个问题:你的程序能访问多大的内存?

你可能会说:“我机器有8GB物理内存,那就能访问8GB呗。” 错!实际上,每个进程都以为自己独占整个地址空间——在32位系统上是4GB,64位系统上更是大到离谱。这就是虚拟内存的魔力。

物理内存就是实实在在的DRAM颗粒,插在主板上。而虚拟内存是内核给每个进程画的“大饼”。进程以为自己拥有连续、完整的地址空间,实际上内核在背后偷偷做映射。

核心要点:虚拟内存让每个进程拥有独立的地址空间,互不干扰。物理内存则由内核统一管理,按需分配。

我在项目中遇到过一个问题:某个进程突然OOM(内存溢出)被杀掉了。查了半天,发现是另一个进程疯狂申请内存,把物理内存耗尽了。这就是虚拟内存的“副作用”——你以为自己独占,其实大家都在抢同一块物理内存。

1.2 页框与页表:映射的“积木”

虚拟地址怎么变成物理地址?靠的是页表

内核把虚拟内存切成固定大小的块,叫(Page)。物理内存也切成同样大小的块,叫页框(Page Frame)。页表就是一张映射表,记录每个虚拟页对应哪个物理页框。

举个例子:

  • 虚拟页0 → 页框100
  • 虚拟页1 → 页框200
  • 虚拟页2 → 页框50

你看,虚拟地址是连续的,但物理地址可以东一块西一块。这就是内存管理的核心思想——离散分配,连续视图

我的经验:页表本身也占内存。一个4KB的页表项,在4GB地址空间下需要约1MB的页表。64位系统下页表更大,所以内核引入了多级页表来节省空间。嗯,这里要注意,多级页表虽然省内存,但访问速度会慢一点——因为要多查几次。

页表里还存了权限位:可读、可写、可执行。这就是为什么你写代码时访问空指针会触发段错误——页表里根本没映射那个地址。

1.3 伙伴系统(Buddy System):物理内存的“大管家”

物理内存怎么分配?内核用了一个经典算法——伙伴系统

说白了,就是把物理内存按2的幂次分成块。比如:

  • 2^0 = 1页(4KB)
  • 2^1 = 2页(8KB)
  • 2^2 = 4页(16KB)
  • ……
  • 2^10 = 1024页(4MB)

分配时,找刚好够大的块。如果找不到,就拆大块。释放时,检查相邻的块是否空闲,如果是,就合并成更大的块。这个相邻的块就叫“伙伴”。

核心逻辑:分配时拆分,释放时合并。避免外部碎片。

我曾经在调试一个网络驱动时,发现内存分配总是失败。查了半天,原来是伙伴系统里大块内存被拆得太碎,合并不回去了。最后我手动触发了一次内存压缩,才解决问题。你想想看,如果伙伴系统设计得不好,内存碎片会多到让你崩溃。

伙伴系统的数据结构很简单:一个数组,每个元素是一个链表,链着相同大小的空闲块。分配时从对应链表取,释放时放回去并尝试合并。

// 伙伴系统的核心结构(简化版)
struct free_area {
    struct list_head free_list;  // 空闲块链表
    unsigned long nr_free;       // 空闲块数量
};

struct zone {
    struct free_area free_area[MAX_ORDER]; // MAX_ORDER通常是11
    // ... 其他字段
};

MAX_ORDER是11,意味着最大块是2^10=1024页(4MB)。为什么是11?嗯,这是经验值,太大了合并开销高,太小了碎片多。

1.4 SLUB分配器:小对象的“快递员”

伙伴系统分配的最小单位是1页(4KB)。但内核里经常要分配几十字节的小对象,比如task_struct、inode等。用伙伴系统直接分配4KB,太浪费了。

所以内核引入了SLUB分配器。它是专门管理小对象的。

SLUB的原理很简单:

  1. 从伙伴系统拿一大块内存(比如2页)。
  2. 切成多个相同大小的小对象(比如每个64字节)。
  3. 维护一个空闲链表,分配时从链表头取一个,释放时放回去。

我的习惯:调试内存泄漏时,我经常用/proc/slabinfo查看SLUB的使用情况。哪个slab缓存增长异常,基本就能定位到问题模块。

SLUB比老版的SLAB分配器更简洁。它去掉了复杂的着色和缓存机制,但性能反而更好。为什么?因为现代CPU的L1/L2缓存已经够快了,不需要内核再搞一套“软件缓存”。

每个SLUB缓存有一个名字,比如task_structmm_struct。你可以通过slabtop命令查看:

# slabtop -o | head -20
 Active / Total Objects (% used)    : 123456 / 234567 (52.6%)
 Active / Total Slabs (% used)      : 1234 / 2345 (52.6%)
 Active / Total Caches (% used)     : 80 / 120 (66.7%)
 
  OBJS ACTIVE  USE OBJ SIZE  SLABS OBJ/SLAB CACHE SIZE NAME
  1234  567    45%    0.02K    10     123       40K    kmalloc-16
  5678  1234   21%    0.25K   100      56      400K    task_struct
  2345  678    28%    1.00K    50      46      800K    mm_struct

你看,task_struct每个对象0.25KB,用了400KB内存。如果这个数值异常增长,很可能有内存泄漏。

避坑指南:我曾经遇到一个bug,某个驱动在卸载时没有释放SLUB缓存,导致kmalloc-16缓存一直增长。查了三天才发现,是kfree()调用少了一次。所以,写驱动时一定要成对使用kmalloc/kfree,或者用devm_系列自动管理函数。

1.5 知识体系总览

下面这张图,是我自己画的,帮你理清本章的核心逻辑:

内核内存管理知识体系 虚拟内存 每个进程独立4GB/64位空间 页表 虚拟页 → 物理页框映射 物理内存 DRAM颗粒,页框管理 伙伴系统 (Buddy System) 管理物理页框,2的幂次分配/合并 解决外部碎片问题 SLUB分配器 管理小对象(task_struct等) 从伙伴系统获取大块,切分使用 内存泄漏检测入口 /proc/slabinfo, kmemleak, 内核调试选项 图1:内核内存管理核心组件关系图 虚拟内存通过页表映射到物理内存 伙伴系统管理物理页框,SLUB管理小对象

这张图把本章的核心逻辑串起来了。你从虚拟内存出发,经过页表映射到物理内存。物理内存由伙伴系统管理大块,SLUB管理小块。最后,内存泄漏检测工具从这两个分配器入手,查找异常。

好了,这一章就到这里。记住:理解物理内存和虚拟内存的区别,掌握页表映射的原理,熟悉伙伴系统和SLUB的工作方式,是后续排查内存泄漏的基础。下一章,我们会深入kmemleak工具,看看怎么用它抓内存泄漏的“元凶”。


公众号:蓝海资料掘金营,微信deep3321