可移动性分组

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),它的核心动作是:

  1. 分配一个新的物理页。
  2. 将旧页内容复制到新页。
  3. 更新页表/映射,使虚拟地址指向新页。
  4. 释放旧页,交还伙伴系统。

这样,内核就能在后台整理内存,把大块连续的物理内存“腾出来”。


举个例子

假设伙伴系统要分配一个 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];
};

工作机制

分配内存时

  1. 进程在某 CPU 上申请内存(order=0,单页)。
  2. 内核优先从该 CPU 在对应 zone 的 per-CPU pageset 中取一页。
  3. 如果缓存里没有,就一次性从伙伴系统里取一批(batch)页,放到 per-CPU 缓存里,然后再分配给进程。

释放内存时

  1. 单页释放时,先放回当前 CPU 的 per-CPU 缓存。
  2. 如果缓存超过 high 水位线,就一次性把一批页(batch)还给伙伴系统。

这样,大多数分配/释放操作都只在 CPU 本地完成,避免了频繁锁竞争。


热页 (hot) vs 冷页 (cold)

  • 每个 CPU 的缓存还分成 hotcold 两类:
    • hot:最近使用过的页,倾向于继续被缓存。
    • cold:长时间没用过的页,更可能被回收。

这样做是为了优化 cache 命中率和 NUMA 访问的局部性


NUMA 的考虑

在 NUMA 系统里,每个 node 有多个 zone,每个 CPU 在每个 zone 都会有自己的 pageset
这样保证了:

  • 优先从 本地内存节点 分配页。
  • 避免跨 node 访问带来的延迟。

一句话总结

zone->pageset 是每个 zone 里的 每 CPU 页缓存池,用来快速分配/回收单页,避免每次都进入伙伴分配器,从而提升性能。


参考资料

  1. Professional Linux Kernel Architecture,Wolfgang Mauerer
  2. Linux内核深度解析,余华兵
  3. Linux设备驱动开发详解,宋宝华
  4. linux kernel 4.12
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐