4. 内存管理基础:页表、虚拟地址空间、伙伴系统与slab分配器
内存管理,说白了就是操作系统怎么管好这块「地皮」。我做了这么多年嵌入式,见过太多因为内存管理不当导致的系统崩溃。嗯,今天咱们就把这块硬骨头啃下来。
4.1 虚拟地址空间——给每个进程一个「假象」
你想想看,如果所有程序都直接操作物理内存,那得多乱?A程序不小心写到了B程序的地盘,整个系统就挂了。所以,我们给每个进程一个「虚拟地址空间」。
在32位系统里,这个空间是4GB。内核占1GB,用户空间占3GB。64位系统就更大了,几乎用不完。
核心概念:虚拟地址是假的,物理地址是真的。中间靠页表来映射。
我在项目中遇到过一个问题:一个嵌入式设备只有64MB内存,但跑的应用却需要128MB。怎么办?靠虚拟内存!把不常用的数据换到Flash上,用的时候再换回来。这就是虚拟地址空间的威力。
4.2 页表——地址翻译的「字典」
页表就是一本字典。CPU拿到一个虚拟地址,查页表,找到对应的物理地址。这个过程叫「地址翻译」。
页表的基本单位是「页」。通常是4KB大小。为什么是4KB?嗯,这是个历史遗留问题,但实践证明这个大小挺合适。
// 页表项的结构(简化版)
struct page_table_entry {
unsigned long present : 1; // 页面是否在内存中
unsigned long rw : 1; // 读写权限
unsigned long user : 1; // 用户态是否可访问
unsigned long pfn : 20; // 物理页帧号
};
多级页表是个好东西。32位系统用两级页表,64位系统用四级甚至五级。为什么?因为如果只用一级页表,那得存100多万个条目,太浪费了。
我的习惯:调试页表问题时,先检查「present」位。如果这个位没置上,CPU就会触发缺页异常。我曾经花了两天时间,才发现是页表项没初始化。
4.3 伙伴系统——大块内存的「分分合合」
伙伴系统是内核用来管理物理内存的算法。它的核心思想很简单:把内存分成2的幂次方大小的块。
比如,你有64KB内存。先分成两个32KB的块。再分,变成四个16KB的块。以此类推。分配时,找大小最合适的块。释放时,看看旁边的「伙伴」是不是也空闲,是的话就合并。
| 阶数 | 块大小 | 最大块数 |
|---|---|---|
| 0 | 4KB | 16 |
| 1 | 8KB | 8 |
| 2 | 16KB | 4 |
| 3 | 32KB | 2 |
| 4 | 64KB | 1 |
我曾经在一个路由器项目里,发现内存碎片化严重。系统跑几天后,明明还有几十MB空闲,但就是分配不出一个8KB的块。原因就是伙伴系统没做好合并。后来加了定时内存整理,问题才解决。
注意:伙伴系统只适合分配大块内存(至少一个页)。小块内存用slab分配器更合适。
4.4 Slab分配器——小块内存的「精打细算」
内核里经常要分配几十字节的小对象,比如进程描述符、文件节点。如果用伙伴系统,一个4KB的页只放一个对象,太浪费了。Slab分配器就是解决这个问题的。
Slab的原理:先向伙伴系统申请一个或多个页,然后切成多个大小相同的对象。每个对象正好放一个数据结构。
// Slab缓存创建示例
struct kmem_cache *my_cache;
my_cache = kmem_cache_create("my_object",
sizeof(struct my_object),
0, 0, NULL);
// 从缓存中分配对象
struct my_object *obj;
obj = kmem_cache_alloc(my_cache, GFP_KERNEL);
// 使用完后释放
kmem_cache_free(my_cache, obj);
你想想看,如果每次创建进程都去伙伴系统申请一个页,那得慢成什么样?Slab分配器提前把对象准备好,用的时候直接拿,速度飞快。
关键点:Slab分配器有三种状态:满、半满、空。分配时优先从半满的slab里取,避免浪费。
我记得有一次优化一个网络驱动,发现sk_buff(网络缓冲区)的分配特别慢。后来改用slab缓存,性能提升了30%。嗯,这就是slab的威力。
4.5 它们怎么配合工作?
说白了,这三者的关系是这样的:
- 虚拟地址空间:给进程一个「假象」,让它以为独占内存。
- 页表:负责把虚拟地址翻译成物理地址。
- 伙伴系统:管理物理内存的大块分配。
- Slab分配器:在伙伴系统的基础上,管理小块内存。
举个例子:你调用malloc(100),glibc会从堆里分配。如果堆不够,就通过brk()系统调用向内核申请。内核用slab分配器(或者直接伙伴系统)分配物理页,然后更新页表,把虚拟地址映射到物理地址。整个过程一气呵成。
避坑指南:我曾经在调试一个内存泄漏问题时,发现slab缓存里的对象越来越多,但伙伴系统的空闲页却没减少。后来才明白,slab释放对象时,页面不一定还给伙伴系统,而是留在缓存里备用。所以看内存占用,不能只看伙伴系统。
4.6 小结
内存管理是操作系统的核心。页表、虚拟地址空间、伙伴系统、slab分配器,这四个东西缺一不可。你想想看,没有虚拟地址空间,进程之间怎么隔离?没有伙伴系统,大块内存怎么分配?没有slab,小块内存怎么高效管理?
嗯,这一章的内容就到这儿。下一章咱们聊聊文件系统的核心——VFS(虚拟文件系统)。到时候你会发现,内存管理的这些知识,在文件系统里也会用到。