伙伴分配器补充
可移动页 (MIGRATE_MOVABLE)用户进程的匿名页 (匿名内存)、文件缓存页(page cache)等。它们被 swap-out 或从文件重新读入后,可以放到别的物理页。所以这些页是“可移动的”。不可移动页 (MIGRATE_UNMOVABLE)内核数据结构、页表、一些 slab 分配的对象。这些数据与内核内部指针直接绑定,没法整体迁移到其他物理页。可回收页 (MIGRATE_RECLA
可移动性分组
enum migratetype {
MIGRATE_UNMOVABLE,
MIGRATE_MOVABLE,
MIGRATE_RECLAIMABLE,
MIGRATE_PCPTYPES, /* the number of types on the pcp lists */
MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES,
#ifdef CONFIG_CMA
/*
* MIGRATE_CMA migration type is designed to mimic the way
* ZONE_MOVABLE works. Only movable pages can be allocated
* from MIGRATE_CMA pageblocks and page allocator never
* implicitly change migration type of MIGRATE_CMA pageblock.
*
* The way to use it is to change migratetype of a range of
* pageblocks to MIGRATE_CMA which can be done by
* __free_pageblock_cma() function. What is important though
* is that a range of pageblocks must be aligned to
* MAX_ORDER_NR_PAGES should biggest page be bigger then
* a single pageblock.
*/
MIGRATE_CMA,
#endif
#ifdef CONFIG_MEMORY_ISOLATION
MIGRATE_ISOLATE, /* can't allocate from here */
#endif
MIGRATE_TYPES
};
struct free_area {
struct list_head free_list[MIGRATE_TYPES];
unsigned long nr_free;
};
struct zone {
...
/* free areas of different sizes */
struct free_area free_area[MAX_ORDER];
...
};
可移动性分组 (migration types) 里的 “移动 (migration)” 指的是 物理页面的可移动性,即:
在必要时(比如内存紧张、内存碎片整理、NUMA 平衡、内存压缩/回收等场景),这个页面中的内容能否被迁移 (migrated) 到另外的物理页框,从而释放出大块连续的物理内存。
为什么需要“移动”
伙伴系统虽然能提供连续页,但随着系统长时间运行,内存会被不同生命周期、不同分配特性的对象填充,容易产生外部碎片。
比如:
- 内核的页表、slab 对象,这些页面不能随意搬动。
- 用户态匿名页、文件映射页,这些往往可以丢弃或重新加载,可以移动。
为了避免“不能搬的页”阻塞了连续大块内存的回收,Linux 引入了 迁移类型 (migratetype) 来分类页面。
可移动性分组定义
- 可移动页 (MIGRATE_MOVABLE)
- 用户进程的匿名页 (匿名内存)、文件缓存页(page cache)等。
- 它们被 swap-out 或从文件重新读入后,可以放到别的物理页。
- 所以这些页是“可移动的”。
- 不可移动页 (MIGRATE_UNMOVABLE)
- 内核数据结构、页表、一些 slab 分配的对象。
- 这些数据与内核内部指针直接绑定,没法整体迁移到其他物理页。
- 可回收页 (MIGRATE_RECLAIMABLE)
- 比如 slab 中的 dentry cache、inode cache。
- 这些对象虽然不能直接搬迁,但可以回收释放,重新分配到别的地方。
迁移的实现方式
Linux 内核提供了 页面迁移机制 (page migration),它的核心动作是:
- 分配一个新的物理页。
- 将旧页内容复制到新页。
- 更新页表/映射,使虚拟地址指向新页。
- 释放旧页,交还伙伴系统。
这样,内核就能在后台整理内存,把大块连续的物理内存“腾出来”。
举个例子
假设伙伴系统要分配一个 2MB hugepage(order=9 的连续页),
- 如果某个物理区域被“不可移动页”占了一个位置,那整块区域就无法用了。
- 但如果这里放的都是“可移动页”,内核就能把它们搬走,腾出一整块连续空间。
所以,“移动”指的就是 物理页能不能被迁移到别的物理页框。
每处理器页集合
内核针对分配单页做了性能优化,为了减少处理器之间的锁竞争,在内存区域(struct zone)中增加 1 个每处理器页集合(struct per_cpu_pageset)。减少频繁操作伙伴系统带来的开销。
struct per_cpu_pages {
int count; /* number of pages in the list */
int high; /* high watermark, emptying needed */
int batch; /* chunk size for buddy add/remove */
/* Lists of pages, one per migrate type stored on the pcp-lists */
struct list_head lists[MIGRATE_PCPTYPES];
};
struct per_cpu_pageset {
struct per_cpu_pages pcp;
...
};
struct zone {
...
struct per_cpu_pageset __percpu *pageset;
...
};
为什么需要 per-CPU 缓存?
- 伙伴分配器管理的单位是 连续的 2^order 个页块,分配/释放时要修改 zone 的全局数据结构(
free_area[]链表)。 - 如果每个内存分配/释放(尤其是 order=0 的单页)都要操作伙伴系统,就会有:
- 自旋锁竞争(多个 CPU 同时操作 zone 的 free_area)。
- 性能下降(频繁分配/释放小页时尤其严重)。
为了避免频繁进入伙伴系统,Linux 在每个 CPU 上维护一个小缓存:per-CPU pageset,一个单页的集合。
数据结构定义
在内核里,pageset 指向 struct per_cpu_pageset,它是一个 每 CPU 的对象,包含了这个 CPU 在该 zone 的缓存信息。
struct per_cpu_pageset {
struct per_cpu_pages pcp;
#ifdef CONFIG_NUMA
s8 expire;
#endif
#ifdef CONFIG_SMP
s8 stat_threshold;
s8 vm_stat_diff[NR_VM_ZONE_STAT_ITEMS];
#endif
};
其中核心是 struct per_cpu_pages pcp:
struct per_cpu_pages {
int count; /* number of pages in the list */
int high; /* high watermark, emptying needed */
int batch; /* chunk size for buddy add/remove */
/* Lists of pages, one per migrate type stored on the pcp-lists */
struct list_head lists[MIGRATE_PCPTYPES];
};
工作机制
分配内存时
- 进程在某 CPU 上申请内存(order=0,单页)。
- 内核优先从该 CPU 在对应 zone 的 per-CPU pageset 中取一页。
- 如果缓存里没有,就一次性从伙伴系统里取一批(batch)页,放到 per-CPU 缓存里,然后再分配给进程。
释放内存时
- 单页释放时,先放回当前 CPU 的 per-CPU 缓存。
- 如果缓存超过
high水位线,就一次性把一批页(batch)还给伙伴系统。
这样,大多数分配/释放操作都只在 CPU 本地完成,避免了频繁锁竞争。
热页 (hot) vs 冷页 (cold)
- 每个 CPU 的缓存还分成 hot 和 cold 两类:
- hot:最近使用过的页,倾向于继续被缓存。
- cold:长时间没用过的页,更可能被回收。
这样做是为了优化 cache 命中率和 NUMA 访问的局部性。
NUMA 的考虑
在 NUMA 系统里,每个 node 有多个 zone,每个 CPU 在每个 zone 都会有自己的 pageset。
这样保证了:
- 优先从 本地内存节点 分配页。
- 避免跨 node 访问带来的延迟。
一句话总结
zone->pageset 是每个 zone 里的 每 CPU 页缓存池,用来快速分配/回收单页,避免每次都进入伙伴分配器,从而提升性能。
参考资料
- Professional Linux Kernel Architecture,Wolfgang Mauerer
- Linux内核深度解析,余华兵
- Linux设备驱动开发详解,宋宝华
- linux kernel 4.12
更多推荐



所有评论(0)