一、内核内存管理基础:物理内存与虚拟内存、页框与页表、伙伴系统原理、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的原理很简单:
- 从伙伴系统拿一大块内存(比如2页)。
- 切成多个相同大小的小对象(比如每个64字节)。
- 维护一个空闲链表,分配时从链表头取一个,释放时放回去。
我的习惯:调试内存泄漏时,我经常用/proc/slabinfo查看SLUB的使用情况。哪个slab缓存增长异常,基本就能定位到问题模块。
SLUB比老版的SLAB分配器更简洁。它去掉了复杂的着色和缓存机制,但性能反而更好。为什么?因为现代CPU的L1/L2缓存已经够快了,不需要内核再搞一套“软件缓存”。
每个SLUB缓存有一个名字,比如task_struct、mm_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 知识体系总览
下面这张图,是我自己画的,帮你理清本章的核心逻辑:
这张图把本章的核心逻辑串起来了。你从虚拟内存出发,经过页表映射到物理内存。物理内存由伙伴系统管理大块,SLUB管理小块。最后,内存泄漏检测工具从这两个分配器入手,查找异常。
好了,这一章就到这里。记住:理解物理内存和虚拟内存的区别,掌握页表映射的原理,熟悉伙伴系统和SLUB的工作方式,是后续排查内存泄漏的基础。下一章,我们会深入kmemleak工具,看看怎么用它抓内存泄漏的“元凶”。
公众号:蓝海资料掘金营,微信deep3321