4. 内存管理:静态内存与动态内存、内存池管理机制、内存泄漏检测与调试、伙伴算法与slab算法实现
内存管理,说白了就是操作系统怎么把有限的物理内存分给各个任务用。你想想看,一个系统里跑着几十个线程,每个都要申请释放内存,如果管理不好,轻则性能下降,重则直接死机。我在OpenHarmony内核里折腾了这么久,踩过的坑大多跟内存有关。
这一章我们重点聊四个东西:静态内存和动态内存的区别、内存池怎么管、内存泄漏怎么查、还有伙伴算法和slab算法到底怎么实现的。嗯,内容不少,但都是硬核干货。
4.1 静态内存 vs 动态内存
先搞清楚概念。静态内存,就是编译时就定好了大小,生命周期跟任务绑定。动态内存呢,运行时按需分配,用完就释放。
| 对比项 | 静态内存 | 动态内存 |
|---|---|---|
| 分配时机 | 编译时 | 运行时 |
| 大小确定 | 固定 | 可变 |
| 分配速度 | 极快(无开销) | 较慢(有算法开销) |
| 碎片问题 | 无碎片 | 可能产生碎片 |
| 典型用途 | 任务栈、全局变量 | 消息队列、大数据块 |
我个人习惯,能静态分配的就尽量静态分配。为什么?因为动态分配有不确定性。我在项目中遇到过一个问题:一个传感器驱动在中断里调了 malloc,结果分配失败导致系统挂掉。后来改成静态内存池,问题就解决了。
4.2 内存池管理机制
内存池,说白了就是提前划出一块区域,自己管自己。OpenHarmony内核里提供了两种内存池:固定大小块的内存池和可变大小块的内存池。
固定块内存池的实现思路很简单:
- 初始化时把一整块内存切成N个等大小的块
- 用链表把空闲块串起来
- 分配时从链表头取一个,释放时放回链表头
代码大概长这样:
typedef struct {
void *pool_start; // 内存池起始地址
uint32_t block_size; // 每个块的大小
uint32_t block_count; // 块总数
void *free_list; // 空闲块链表头
} MemPool;
// 分配一个块
void *mem_pool_alloc(MemPool *pool) {
if (pool->free_list == NULL) {
return NULL; // 没有空闲块了
}
void *block = pool->free_list;
pool->free_list = *(void **)block; // 链表头后移
return block;
}
// 释放一个块
void mem_pool_free(MemPool *pool, void *block) {
*(void **)block = pool->free_list;
pool->free_list = block;
}
你看,这个实现O(1)复杂度,分配和释放都是常数时间。我在做音频驱动时就用这个,因为音频缓冲区大小是固定的,用内存池比用malloc快了一个数量级。
4.3 伙伴算法与slab算法实现
这两个算法是内核动态内存管理的核心。我分开讲。
4.3.1 伙伴算法
伙伴算法解决什么问题?外部碎片。你想想看,频繁申请释放后,内存里会出现很多小空洞,每个都不大,但加起来不少,而且大块分配会失败。
伙伴算法的思路:把内存按2的幂次分成块。比如2KB、4KB、8KB……分配时找刚好能装下的最小块,如果找不到就拆大块。释放时检查相邻的块是否空闲,如果是就合并。
我画个图帮你理解:
伙伴算法的核心数据结构是空闲链表数组,每个元素对应一种大小的块:
// 假设最大阶数为10(即最大块为1024个单位)
#define MAX_ORDER 10
typedef struct {
struct list_head free_list[MAX_ORDER + 1]; // 每阶一个空闲链表
unsigned long free_pages; // 总空闲页数
} BuddySystem;
// 分配:从指定阶数开始找,找不到就升阶拆分
void *buddy_alloc(BuddySystem *bs, int order) {
int current = order;
while (current <= MAX_ORDER) {
if (!list_empty(&bs->free_list[current])) {
// 找到空闲块,取出
struct page *page = list_first_entry(&bs->free_list[current], struct page, list);
list_del(&page->list);
// 如果当前阶数大于请求阶数,拆分
while (current > order) {
current--;
// 把另一半加入低一阶的空闲链表
struct page *buddy = page + (1 << current);
list_add(&buddy->list, &bs->free_list[current]);
}
return page;
}
current++;
}
return NULL; // 内存不足
}
4.3.2 Slab算法
Slab算法的思路:把内存按对象大小缓存起来。比如内核里经常分配task_struct结构体,大小固定,那就专门为它建一个slab缓存。
每个slab缓存包含多个slab,每个slab是一块连续内存,被切成等大小的对象。对象有3种状态:
- 满:所有对象都被分配了
- 空:所有对象都空闲
- 部分:部分分配,部分空闲
分配时优先从部分slab里取,没有就从空slab里取。释放时如果slab变空了,可以归还给伙伴系统。
我在项目中遇到过一个问题:某个驱动频繁创建销毁对象,用kmalloc分配导致大量碎片。后来改成slab缓存,性能提升明显,碎片也少了。
// Slab缓存结构(简化版)
typedef struct {
struct list_head slabs_full; // 满的slab链表
struct list_head slabs_partial; // 部分空闲的slab链表
struct list_head slabs_free; // 全空闲的slab链表
uint32_t object_size; // 每个对象大小
uint32_t objects_per_slab; // 每个slab里有多少对象
void (*ctor)(void *obj); // 构造函数(可选)
} SlabCache;
// 从slab缓存分配一个对象
void *slab_alloc(SlabCache *cache) {
// 优先从部分空闲的slab里取
if (!list_empty(&cache->slabs_partial)) {
Slab *slab = list_first_entry(&cache->slabs_partial, Slab, list);
void *obj = slab_alloc_from(slab);
if (slab_is_full(slab)) {
list_move(&slab->list, &cache->slabs_full);
}
return obj;
}
// 没有部分空闲的,从全空闲的取
if (!list_empty(&cache->slabs_free)) {
Slab *slab = list_first_entry(&cache->slabs_free, Slab, list);
list_move(&slab->list, &cache->slabs_partial);
return slab_alloc_from(slab);
}
// 都没有,向伙伴系统申请新slab
Slab *new_slab = buddy_alloc_slab(cache);
if (new_slab) {
list_add(&new_slab->list, &cache->slabs_partial);
return slab_alloc_from(new_slab);
}
return NULL;
}
4.4 内存泄漏检测与调试
内存泄漏,说白了就是申请了没释放。在嵌入式系统里,内存泄漏是致命的——系统跑几天就挂了。
我分享几个实用的检测方法:
4.4.1 手动标记法
在分配和释放的地方加日志:
#define DEBUG_MEM
void *debug_malloc(size_t size, const char *file, int line) {
void *ptr = malloc(size);
#ifdef DEBUG_MEM
printf("[MEM] malloc(%zu) = %p at %s:%d\n", size, ptr, file, line);
#endif
return ptr;
}
void debug_free(void *ptr, const char *file, int line) {
#ifdef DEBUG_MEM
printf("[MEM] free(%p) at %s:%d\n", ptr, file, line);
#endif
free(ptr);
}
// 使用宏替换
#define malloc(s) debug_malloc(s, __FILE__, __LINE__)
#define free(p) debug_free(p, __FILE__, __LINE__)
4.4.2 内存泄漏检测工具
OpenHarmony内核里有个 memleak 工具,原理是记录每次分配时的调用栈,定期扫描内存,找出那些分配了但没释放的块。
用法很简单:
# 开启内存泄漏检测
echo 1 > /proc/sys/kernel/memleak_enable
# 查看泄漏报告
cat /proc/memleak
# 输出示例:
# [ 1234.567] memleak: 8 bytes leaked from 0x12345678
# [ 1234.567] Call trace:
# [ 1234.567] [<0x80001234>] my_function+0x10/0x20
# [ 1234.567] [<0x80005678>] another_function+0x30/0x40
4.4.3 常见泄漏场景
- 中断上下文里分配了内存但忘了释放——中断处理要快,容易遗漏
- 错误路径没释放——函数提前返回了,free没执行到
- 链表操作失误——节点从链表移除后没释放
kfree_skb。结果系统跑了3小时后内存耗尽重启。查了两天才找到问题,从此以后我每次分配内存都会在注释里写上对应的释放位置。
4.5 本章小结
内存管理这块,说白了就是三个字:快、稳、省。静态内存池适合固定大小的频繁分配,伙伴算法解决大块内存的碎片问题,slab算法优化小对象的分配效率。至于内存泄漏检测,别等到产品上线了才做,开发阶段就养成好习惯。
嗯,这一章的内容就到这。代码示例都是我从OpenHarmony内核里提炼出来的简化版,实际源码会更复杂,但核心思路是一样的。你动手写代码时,多想想:这个内存到底该谁管?什么时候释放?想清楚了再动手。