常见的内存分配算法
在操作系统中,内存管理是核心环节之一。程序运行时需要申请内存空间,而系统如何分配这些空间,直接影响性能和安全性。不同的内存分配算法决定了内存块的划分、回收与再利用方式,也间接为某些安全漏洞提供了滋生土壤。
首次适应算法(First Fit)
首次适应算法从空闲内存区域的起始位置开始查找,一旦找到第一个足够大的空闲块就进行分配。这种策略实现简单,速度较快。比如你去停车场找车位,看到第一个能停下车子的位置就直接停了,不再往后看。
但由于倾向于使用低地址部分的内存,可能导致高地址区域留下大量碎片。长期运行后,即便总空闲内存充足,也可能无法满足较大内存请求。
最佳适应算法(Best Fit)
这个算法会遍历所有空闲块,选择一个大小最接近需求的块进行分配。听起来很“聪明”,但问题在于它容易产生大量难以利用的小碎片。就像切蛋糕,每次都切刚好够吃的那一块,结果剩下很多边角料,谁都不想要。
这些碎片在后续分配中很难被重新利用,反而增加了内存管理的复杂度,也为缓冲区溢出类攻击埋下隐患。
最坏适应算法(Worst Fit)
与最佳相反,最坏适应算法总是选择最大的空闲块来分配。目的是保留中等大小的块以应对未来可能的需求。可现实往往是,大块被不断拆解,最终导致没有足够的大块应对真正的大请求。
这种策略在实际系统中用得不多,因为它对碎片化的控制并不理想,反而更容易造成资源浪费。
循环首次适应算法(Next Fit)
它是首次适应的改进版,不是每次都从头开始搜索,而是从上一次分配结束的位置继续向后找。相当于上次停车停在第50个车位,这次就从那里接着往后看。
虽然减少了重复扫描的开销,但可能导致内存分布不均,某些区域长期得不到使用,形成“内存死角”。
伙伴系统(Buddy System)
这是一种更结构化的分配方式,常用于内核级内存管理。内存按2的幂次方分割成固定大小的块。当需要分配时,找最小的满足条件的2^n大小块;释放时,尝试与“伙伴”合并成更大的块。
例如要分配13KB,系统就会分配16KB的块。这种方式有效减少外部碎片,合并机制也提高了回收效率。Linux 内核中的 slab 分配器就基于此机制构建。
void* buddy_alloc(size_t size) {
int order = get_order(size); // 计算对应的2^n阶
for (; order < MAX_ORDER; order++) {
if (!list_empty(&buddy_lists[order]))
return split_and_remove(order, size);
}
return NULL;
}内存分配与安全的关系
不合理的内存分配策略可能加剧堆溢出、Use-After-Free 等漏洞的利用风险。例如频繁的小块分配和释放会产生堆碎片,攻击者可借此操控堆布局,实现精确的内存布置,进而执行任意代码。
像 glibc 的 ptmalloc2 使用了多种策略混合的机制,包含 arena、bins 等结构,虽提升了并发性能,但也因复杂性成为漏洞频发点。近年来一些新型分配器如 jemalloc、tcmalloc 通过隔离分配、缓存线程本地内存等方式增强安全性与效率。
了解这些底层机制,有助于安全研究人员分析漏洞成因,也能帮助开发人员写出更健壮的代码。比如避免频繁申请释放小对象,或主动使用安全函数替代传统 malloc/free 组合。