物理内存是操作系统最基础的资源之一,而 Linux 内核的内存管理子系统正是围绕着如何高效、可靠地组织与分配这些物理页帧展开的。本文基于 Linux 6.4-rc1 源码,系统性地剖析物理内存的组织模型、zone 水位机制、伙伴分配器的核心算法,以及 Per-CPU 页帧缓存、GFP 标志体系和 OOM Killer 的工作原理,并给出实用的诊断方法。
一、物理内存模型 1.1 从硬件拓扑到内核抽象 现代服务器普遍采用 NUMA(Non-Uniform Memory Access) 架构:多个处理器插槽各自拥有本地内存,访问本地内存的延迟远低于访问远端节点的内存。Linux 内核用三层抽象来映射这一硬件现实:
1 2 3 NUMA Node (pg_data_t) └── Zone (struct zone) └── Page (struct page)
每个 NUMA 节点对应一个 pg_data_t(即 pglist_data),节点内的物理内存按地址范围和硬件约束划分为若干 Zone,每个物理页帧对应一个 struct page。
1.2 NUMA 节点:struct pglist_data pg_data_t 是节点级别的核心数据结构,定义于 include/linux/mmzone.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 typedef struct pglist_data { struct zone node_zones [MAX_NR_ZONES ]; struct zonelist node_zonelists [MAX_ZONELISTS ]; int nr_zones; unsigned long node_start_pfn; unsigned long node_present_pages; unsigned long node_spanned_pages; int node_id; wait_queue_head_t kswapd_wait; struct task_struct *kswapd ; int kswapd_order; enum zone_type kswapd_highest_zoneidx ; ... } pg_data_t ;
关键字段解析:
node_zones[]:仅包含本节点各 zone。分配器直接通过数组下标(ZONE_DMA、ZONE_NORMAL 等枚举值)访问。
node_zonelists[]:zonelist 是一个有序的 zone 列表,按分配优先级排列。ZONELIST_FALLBACK 包含全系统所有可用 zone,当本节点内存不足时,分配器沿此列表向其他节点 fallback。ZONELIST_NOFALLBACK(仅 NUMA 下存在)用于 __GFP_THISNODE,强制只在本节点分配。
kswapd:每个节点有一个 kswapd 内核线程,专门负责异步回收该节点的内存。当某个 zone 的空闲页跌破 low watermark 时唤醒它。
在 UMA(单 NUMA 节点)系统上,内核只有一个全局的 contig_page_data,NODE_DATA(0) 直接指向它,编译期优化掉了所有 NUMA 间接寻址。
1.3 内存区域:struct zone struct zone 是分配器的核心工作单元,关键字段如下(include/linux/mmzone.h):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 struct zone { unsigned long _watermark[NR_WMARK]; unsigned long watermark_boost; unsigned long nr_reserved_highatomic; struct per_cpu_pages __percpu *per_cpu_pageset ; unsigned long zone_start_pfn; atomic_long_t managed_pages; unsigned long spanned_pages; unsigned long present_pages; struct free_area free_area [MAX_ORDER + 1]; spinlock_t lock; ... } ____cacheline_internodealigned_in_smp;
几个关键计数的关系:
1 2 3 spanned_pages = zone_end_pfn - zone_start_pfn (含物理空洞) present_pages = spanned_pages - holes (真实物理页) managed_pages = present_pages - reserved_pages (buddy 管理的页)
managed_pages 是 watermark 计算和可用内存统计的基准,reserved_pages 包含 bootmem 分配器使用的页、memmap 本身等内核保留页。
1.4 空闲区域:struct free_area 与迁移类型 伙伴系统按 分配阶(order) 组织空闲页,每个 order 对应 2^order 个连续页帧。MAX_ORDER 默认为 10,即最大单次分配 1024 页(4MB,x86-64 下),MAX_ORDER + 1 共 11 个阶。
1 2 3 4 5 struct free_area { struct list_head free_list [MIGRATE_TYPES ]; unsigned long nr_free; };
每个 free_area 包含 MIGRATE_TYPES 条链表,分别对应不同的迁移类型 。这是 Linux 应对内存碎片化的核心机制:将具有相同迁移特性的页聚集在一起,减少不可移动页散落在可移动页块中导致的碎片。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 enum migratetype { MIGRATE_UNMOVABLE, MIGRATE_MOVABLE, MIGRATE_RECLAIMABLE, MIGRATE_PCPTYPES, MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES, #ifdef CONFIG_CMA MIGRATE_CMA, #endif #ifdef CONFIG_MEMORY_ISOLATION MIGRATE_ISOLATE, #endif MIGRATE_TYPES };
MIGRATE_HIGHATOMIC 是一个专为 order > 0 的原子分配(如网络驱动中断路径)预留的池,通过 nr_reserved_highatomic 控制其大小。
Fallback 顺序 :当目标迁移类型的 free_list 为空时,__rmqueue_fallback() 按预定顺序回退:
1 2 3 4 5 6 static int fallbacks[MIGRATE_TYPES][MIGRATE_PCPTYPES - 1 ] = { [MIGRATE_UNMOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE }, [MIGRATE_MOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE }, [MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE, MIGRATE_MOVABLE }, };
当从其他迁移类型窃取页块时,内核会将整个页块的迁移类型重新标记,以保持页块内迁移类型的一致性,减少长期碎片化。
1.5 物理页描述符:struct page 系统中每个物理页帧都有一个对应的 struct page,存储在 vmemmap 区域中(SPARSEMEM 内存模型下)。struct page 设计极为精巧,大量使用 union 以减少内存占用(include/linux/mm_types.h):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 struct page { unsigned long flags; union { struct { union { struct list_head lru ; struct list_head buddy_list ; struct list_head pcp_list ; }; struct address_space *mapping ; union { pgoff_t index; unsigned long share; }; unsigned long private; }; struct { unsigned long compound_head; }; struct rcu_head rcu_head ; }; union { atomic_t _mapcount; unsigned int page_type; }; atomic_t _refcount; #ifdef CONFIG_MEMCG unsigned long memcg_data; #endif } _struct_page_alignment;
几个核心字段的语义:
字段
含义
flags
高位编码 section/node/zone 信息,低位为 PG_* 状态标志
_refcount
页的引用计数,alloc_pages() 返回时为 1,put_page() 减至 0 则释放
_mapcount
用户态页表映射次数,-1 表示未被任何页表映射
mapping
文件页指向 address_space,匿名页指向 anon_vma(bit 0 置 1 区分)
index
文件页在 page cache 中的偏移;匿名页在 vma 内的偏移
private
PageBuddy 时存 order,PagePrivate 时存 buffer_head 指针
buddy_list
页在 buddy free_list 上时的链表节点
pcp_list
页在 Per-CPU 链表上时的链表节点
flags 字段的布局(以 64 位系统为例):
1 | SECTION bits | NODE bits | ZONE bits | LAST_CPUPID bits | ... | PG_* flag bits |
通过 page_zonenum(page)、page_to_nid(page) 等宏可以从 flags 字段直接读取 zone 和 node 编号,无需额外存储。
二、内存区域(Zone) 2.1 Zone 类型与地址范围 Linux 将物理内存按用途和硬件约束划分为若干 zone(include/linux/mmzone.h):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 enum zone_type {#ifdef CONFIG_ZONE_DMA ZONE_DMA, #endif #ifdef CONFIG_ZONE_DMA32 ZONE_DMA32, #endif ZONE_NORMAL, #ifdef CONFIG_HIGHMEM ZONE_HIGHMEM, #endif ZONE_MOVABLE, #ifdef CONFIG_ZONE_DEVICE ZONE_DEVICE, #endif __MAX_NR_ZONES };
在典型的 x86-64 系统上:
Zone
地址范围
用途
ZONE_DMA
0 ~ 16 MB
旧式 ISA 总线 DMA
ZONE_DMA32
0 ~ 4 GB
32 位寻址 DMA 设备
ZONE_NORMAL
4 GB ~ 全部
内核直接映射
ZONE_MOVABLE
配置决定
热插拔/THP 专用
ZONE_MOVABLE 并不对应特定的物理地址范围,而是一个软件抽象:通过 kernelcore= 或 movablecore= 内核参数,将高端内存的一部分划为只允许可移动分配的区域,从而保证内存热拔出时可以将这段内存中的页全部迁移走。
2.2 Watermark 水位机制 每个 zone 有三条水位线,加上一个动态 boost 值:
1 2 3 4 5 6 7 8 9 10 11 12 enum zone_watermarks { WMARK_MIN, WMARK_LOW, WMARK_HIGH, WMARK_PROMO, NR_WMARK }; #define min_wmark_pages(z) (z->_watermark[WMARK_MIN] + z->watermark_boost) #define low_wmark_pages(z) (z->_watermark[WMARK_LOW] + z->watermark_boost) #define high_wmark_pages(z) (z->_watermark[WMARK_HIGH] + z->watermark_boost)
水位的触发逻辑:
1 2 3 4 空闲页 > high_wmark → 充裕,kswapd 休眠 空闲页 < low_wmark → 唤醒 kswapd,后台异步回收至 high_wmark 空闲页 < min_wmark → 触发直接回收(direct reclaim),分配路径阻塞 空闲页 < min/2 → GFP_ATOMIC 类紧急分配仍可通过(访问 atomic reserves)
水位由 vm.min_free_kbytes sysctl 决定(min watermark),low 和 high 通过比例推算。watermark_boost 是当检测到碎片化时动态抬升的临时增量,用于提前触发压缩,消除碎片。
2.3 zone_watermark_ok 实现 分配器在每次从 zone 取页前,都会调用 watermark 检查函数(mm/page_alloc.c):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 bool __zone_watermark_ok(struct zone *z, unsigned int order, unsigned long mark, int highest_zoneidx, unsigned int alloc_flags, long free_pages) { long min = mark; int o; free_pages -= __zone_watermark_unusable_free(z, order, alloc_flags); if (unlikely(alloc_flags & ALLOC_RESERVES)) { if (alloc_flags & ALLOC_MIN_RESERVE) { min -= min / 2 ; if (alloc_flags & ALLOC_NON_BLOCK) min -= min / 4 ; } if (alloc_flags & ALLOC_OOM) min -= min / 2 ; } if (free_pages <= min + z->lowmem_reserve[highest_zoneidx]) return false ; if (!order) return true ; for (o = order; o <= MAX_ORDER; o++) { struct free_area *area = &z->free_area[o]; int mt; if (!area->nr_free) continue ; for (mt = 0 ; mt < MIGRATE_PCPTYPES; mt++) { if (!free_area_empty(area, mt)) return true ; } ... } return false ; } bool zone_watermark_ok (struct zone *z, unsigned int order, unsigned long mark, int highest_zoneidx, unsigned int alloc_flags) { return __zone_watermark_ok(z, order, mark, highest_zoneidx, alloc_flags, zone_page_state(z, NR_FREE_PAGES)); }
lowmem_reserve[] 是一个二维保留机制:低 zone(如 DMA)为高 zone 的分配请求保留一定数量的页面,防止高 zone 内存耗尽后 DMA 分配也失败。
三、伙伴分配系统(Buddy System) 3.1 算法原理 伙伴系统(Buddy Allocator)由 Knuth 提出,Linux 将其用于管理物理页帧。核心思想:
内存按 2^n 页的块管理,共分 MAX_ORDER + 1(11)个阶。
分配 order=k 的块时,若 k 阶无空闲,则将 k+1 阶的块分裂(split) 成两个 k 阶的”伙伴”,取一个返回,另一个挂入 k 阶链表。
释放时,检查对应地址的”伙伴”是否也空闲,若是则合并(merge) 成高一阶的块,递归向上合并。
伙伴关系通过 PFN 的 bit-k 来判断:若 pfn ^ buddy_pfn == (1 << order),则互为伙伴。
3.2 分配入口:__alloc_pages __alloc_pages 是整个物理内存分配的核心入口(mm/page_alloc.c):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 struct page *__alloc_pages (gfp_t gfp , unsigned int order , int preferred_nid , nodemask_t *nodemask ) { struct page *page ; unsigned int alloc_flags = ALLOC_WMARK_LOW; gfp_t alloc_gfp; struct alloc_context ac = { }; if (WARN_ON_ONCE_GFP(order > MAX_ORDER, gfp)) return NULL ; gfp &= gfp_allowed_mask; gfp = current_gfp_context(gfp); alloc_gfp = gfp; if (!prepare_alloc_pages(gfp, order, preferred_nid, nodemask, &ac, &alloc_gfp, &alloc_flags)) return NULL ; alloc_flags |= alloc_flags_nofragment(ac.preferred_zoneref->zone, gfp); page = get_page_from_freelist(alloc_gfp, order, alloc_flags, &ac); if (likely(page)) goto out; alloc_gfp = gfp; ac.spread_dirty_pages = false ; ac.nodemask = nodemask; page = __alloc_pages_slowpath(alloc_gfp, order, &ac); out: ... trace_mm_page_alloc(page, order, alloc_gfp, ac.migratetype); return page; } EXPORT_SYMBOL(__alloc_pages);
Fast path 使用 ALLOC_WMARK_LOW,即要求空闲页高于 low watermark,这是正常情况下的快速分配。
Slow path __alloc_pages_slowpath 会依次尝试:
唤醒 kswapd,再次尝试分配(放宽至 min watermark)
直接内存回收(__alloc_pages_direct_reclaim)
内存压缩(__alloc_pages_direct_compact)
OOM Killer(out_of_memory)
无限重试(若设置了 __GFP_NOFAIL)
3.3 get_page_from_freelist:遍历 zonelist get_page_from_freelist 遍历 zonelist,对每个 zone 执行 watermark 检查,通过则调用 rmqueue 取页:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags, const struct alloc_context *ac) { struct zoneref *z ; struct zone *zone ; ... retry: no_fallback = alloc_flags & ALLOC_NOFRAGMENT; z = ac->preferred_zoneref; for_next_zone_zonelist_nodemask(zone, z, ac->highest_zoneidx, ac->nodemask) { unsigned long mark; ... mark = wmark_pages(zone, alloc_flags & ALLOC_WMARK_MASK); if (!zone_watermark_fast(zone, order, mark, ac->highest_zoneidx, alloc_flags, gfp_mask)) { ... continue ; } try_this_zone: page = rmqueue(ac->preferred_zoneref->zone, zone, order, gfp_mask, alloc_flags, ac->migratetype); if (page) { prep_new_page(page, order, gfp_mask, alloc_flags); return page; } } return NULL ; }
ALLOC_NOFRAGMENT 标志用于 fast path 的第一轮:只从本地节点分配,禁止跨节点 fallback,以避免碎片化。若失败,则去掉该标志 retry,允许跨节点分配。
3.4 rmqueue 与 __rmqueue_smallest rmqueue 是 zone 级别的分配函数,优先尝试 PCP,否则调用 rmqueue_buddy:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 static inline struct page *rmqueue (struct zone *preferred_zone, struct zone *zone, unsigned int order, gfp_t gfp_flags, unsigned int alloc_flags, int migratetype) { struct page *page ; if (likely(pcp_allowed_order(order))) { if (!IS_ENABLED(CONFIG_CMA) || alloc_flags & ALLOC_CMA || migratetype != MIGRATE_MOVABLE) { page = rmqueue_pcplist(preferred_zone, zone, order, migratetype, alloc_flags); if (likely(page)) goto out; } } page = rmqueue_buddy(preferred_zone, zone, order, alloc_flags, migratetype); out: ... return page; }
rmqueue_buddy 最终调用 __rmqueue_smallest,这是伙伴分配的核心算法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 static __always_inlinestruct page *__rmqueue_smallest (struct zone *zone , unsigned int order , int migratetype ) { unsigned int current_order; struct free_area *area ; struct page *page ; for (current_order = order; current_order <= MAX_ORDER; ++current_order) { area = &(zone->free_area[current_order]); page = get_page_from_free_area(area, migratetype); if (!page) continue ; del_page_from_free_list(page, zone, current_order); expand(zone, page, order, current_order, migratetype); set_pcppage_migratetype(page, migratetype); return page; } return NULL ; }
expand() 函数实现页分裂(buddy splitting) :若分配 order=2 但找到 order=5 的块,则将剩余的 order=4、order=3、order=2 的”伙伴”分别挂入对应链表。
3.5 页面释放与合并:__free_one_page 释放时,__free_pages → free_unref_page(order 0 或小阶)→ __free_one_page(合并逻辑):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 static inline void __free_one_page(struct page *page, unsigned long pfn, struct zone *zone, unsigned int order, int migratetype, fpi_t fpi_flags) { unsigned long buddy_pfn = 0 ; unsigned long combined_pfn; struct page *buddy ; if (likely(!is_migrate_isolate(migratetype))) __mod_zone_freepage_state(zone, 1 << order, migratetype); while (order < MAX_ORDER) { buddy = find_buddy_page_pfn(page, pfn, order, &buddy_pfn); if (!buddy) goto done_merging; if (unlikely(order >= pageblock_order)) { int buddy_mt = get_pageblock_migratetype(buddy); if (migratetype != buddy_mt && ...) goto done_merging; } del_page_from_free_list(buddy, zone, order); combined_pfn = buddy_pfn & pfn; page = page + (combined_pfn - pfn); pfn = combined_pfn; order++; } done_merging: set_buddy_order(page, order); to_tail = buddy_merge_likely(pfn, buddy_pfn, page, order); if (to_tail) add_to_free_list_tail(page, zone, order, migratetype); else add_to_free_list(page, zone, order, migratetype); page_reporting_notify_free(order); }
合并时取 buddy_pfn & pfn 是因为伙伴对的 PFN 仅在 bit-order 处不同,AND 后得到对齐的合并块起始地址。set_buddy_order 将 order 写入 page->private,通过 PageBuddy 标志区分该页是否在 buddy 链表中。
四、Per-CPU Page Frame Cache(PCP) 4.1 设计动机 全局的 zone->lock 是伙伴分配器的主要竞争点。在多核系统上,频繁的 order-0 分配(如网络收包、进程创建)若每次都竞争 zone 锁,将极大影响吞吐量。PCP(Per-CPU Pages) 是为此设计的无锁快速路径:每个 CPU 维护一个私有的小型页池,分配与释放优先从中操作,仅在池枯竭或过满时才批量与全局 buddy 交互。
4.2 struct per_cpu_pages 1 2 3 4 5 6 7 8 9 10 11 12 13 struct per_cpu_pages { spinlock_t lock; int count; int high; int batch; short free_factor; #ifdef CONFIG_NUMA short expire; #endif struct list_head lists [NR_PCP_LISTS ]; } ____cacheline_aligned_in_smp;
NR_PCP_LISTS 的计算:
1 2 3 4 #define NR_LOWORDER_PCP_LISTS (MIGRATE_PCPTYPES * (PAGE_ALLOC_COSTLY_ORDER + 1)) #define NR_PCP_LISTS (NR_LOWORDER_PCP_LISTS + NR_PCP_THP)
4.3 rmqueue_pcplist:PCP 快速分配 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 static struct page *rmqueue_pcplist (struct zone *preferred_zone, struct zone *zone, unsigned int order, int migratetype, unsigned int alloc_flags) { struct per_cpu_pages *pcp ; struct list_head *list ; struct page *page ; unsigned long __maybe_unused UP_flags; pcp_trylock_prepare(UP_flags); pcp = pcp_spin_trylock(zone->per_cpu_pageset); if (!pcp) { pcp_trylock_finish(UP_flags); return NULL ; } pcp->free_factor >>= 1 ; list = &pcp->lists[order_to_pindex(migratetype, order)]; page = __rmqueue_pcplist(zone, order, migratetype, alloc_flags, pcp, list ); pcp_spin_unlock(pcp); pcp_trylock_finish(UP_flags); ... return page; }
当 PCP 链表为空时,__rmqueue_pcplist 调用 rmqueue_bulk 批量从 buddy 取 batch 个页补充,再返回第一个。
4.4 free_unref_page:PCP 优先释放 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void free_unref_page (struct page *page, unsigned int order) { struct per_cpu_pages *pcp ; struct zone *zone ; unsigned long pfn = page_to_pfn(page); int migratetype; if (!free_unref_page_prepare(page, pfn, order)) return ; migratetype = get_pcppage_migratetype(page); ... pcp = pcp_spin_trylock(zone->per_cpu_pageset); if (pcp) { free_unref_page_commit(zone, pcp, page, migratetype, order); pcp_spin_unlock(pcp); } else { free_one_page(zone, page, pfn, order, migratetype, FPI_NONE); } }
free_unref_page_commit 将页挂入 PCP 链表,若 pcp->count >= high,则调用 free_pcppages_bulk 批量将 nr_pcp_free() 个页归还给 buddy。批量大小受 free_factor 调节:连续释放时 free_factor 增大,清空的页更多,避免 PCP 频繁超高水位。
五、内存分配标志(GFP Flags) 5.1 标志体系概览 GFP(Get Free Pages)标志是调用者与分配器的”合约”,描述分配的约束和行为。定义在 include/linux/gfp_types.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #define __GFP_DMA ((__force gfp_t)0x01u) #define __GFP_HIGHMEM ((__force gfp_t)0x02u) #define __GFP_DMA32 ((__force gfp_t)0x04u) #define __GFP_MOVABLE ((__force gfp_t)0x08u) #define __GFP_RECLAIMABLE ((__force gfp_t)0x10u) #define __GFP_HIGH ((__force gfp_t)0x20u) #define __GFP_MEMALLOC ((__force gfp_t)0x20000u) #define __GFP_NOMEMALLOC ((__force gfp_t)0x80000u) #define __GFP_IO ((__force gfp_t)0x40u) #define __GFP_FS ((__force gfp_t)0x80u) #define __GFP_DIRECT_RECLAIM ((__force gfp_t)0x400u) #define __GFP_KSWAPD_RECLAIM ((__force gfp_t)0x800u) #define __GFP_RECLAIM ((__force gfp_t)(0x400u|0x800u)) #define __GFP_NORETRY ((__force gfp_t)0x10000u) #define __GFP_RETRY_MAYFAIL ((__force gfp_t)0x4000u) #define __GFP_NOFAIL ((__force gfp_t)0x8000u) #define __GFP_NOWARN ((__force gfp_t)0x2000u) #define __GFP_ZERO ((__force gfp_t)0x100u) #define __GFP_COMP ((__force gfp_t)0x40000u)
5.2 常用复合标志 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #define GFP_KERNEL (__GFP_RECLAIM | __GFP_IO | __GFP_FS) #define GFP_ATOMIC (__GFP_HIGH | __GFP_KSWAPD_RECLAIM) #define GFP_USER (__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL) #define GFP_HIGHUSER_MOVABLE (GFP_USER | __GFP_HIGHMEM | __GFP_MOVABLE | ...) #define GFP_DMA __GFP_DMA #define GFP_NOIO (__GFP_RECLAIM) #define GFP_NOFS (__GFP_RECLAIM | __GFP_IO)
5.3 GFP_KERNEL vs GFP_ATOMIC 使用场景 GFP_KERNEL 是内核中最常用的分配标志,适用于:
进程上下文,可以睡眠等待内存
无持有自旋锁(持锁时不可睡眠)
不在中断处理程序或 softirq 中
典型场景:kmalloc(size, GFP_KERNEL)、alloc_pages(GFP_KERNEL, 0)
GFP_ATOMIC 适用于不可睡眠的上下文:
硬中断(ISR)、软中断(softirq/tasklet)
持有自旋锁(spin_lock 而非 mutex)
持有 RCU 读锁
不允许任何阻塞操作的原子上下文
典型场景:网络驱动 rx 中断路径分配 sk_buff、定时器回调中的临时缓冲区。
GFP_ATOMIC 比 GFP_KERNEL 的分配成功率更低(因为不能回收),且分配失败时内核不会打印 WARN,调用者必须 处理返回 NULL 的情况。
__GFP_NOFAIL 告诉内核无论如何都必须成功(无限重试),只应用于真正无法处理失败的关键路径,且必须配合 GFP_KERNEL(可睡眠),对 order > 1 的分配使用会触发 WARN。
__GFP_NOWARN 抑制分配失败时的 WARN 日志,适用于内核明确准备了 fallback 路径的场合(如 THP 分配失败后回退到普通页)。
六、内存分配失败与 OOM Killer 6.1 OOM 触发路径 当 __alloc_pages_slowpath 经过直接回收、内存压缩等所有手段后仍无法获取足够内存,且分配标志允许 OOM(没有 __GFP_NORETRY 且 order <= PAGE_ALLOC_COSTLY_ORDER),则调用 out_of_memory:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 bool out_of_memory (struct oom_control *oc) { unsigned long freed = 0 ; if (oom_killer_disabled) return false ; if (!is_memcg_oom(oc)) { blocking_notifier_call_chain(&oom_notify_list, 0 , &freed); if (freed > 0 && !is_sysrq_oom(oc)) return true ; } if (task_will_free_mem(current)) { mark_oom_victim(current); queue_oom_reaper(current); return true ; } if (oc->gfp_mask && !(oc->gfp_mask & __GFP_FS) && !is_memcg_oom(oc)) return true ; oc->constraint = constrained_alloc(oc); check_panic_on_oom(oc); if (!is_memcg_oom(oc) && sysctl_oom_kill_allocating_task && ...) { oc->chosen = current; oom_kill_process(oc, "Out of memory (oom_kill_allocating_task)" ); return true ; } select_bad_process(oc); if (!oc->chosen) { dump_header(oc, NULL ); pr_warn("Out of memory and no killable processes...\n" ); if (!is_sysrq_oom(oc) && !is_memcg_oom(oc)) panic("System is deadlocked on memory\n" ); } if (oc->chosen && oc->chosen != (void *)-1UL ) oom_kill_process(oc, "Out of memory" ); return !!oc->chosen; }
6.2 oom_badness:选择受害进程 select_bad_process 遍历所有进程,调用 oom_badness 计算每个进程的”坏分”,选择最高分者:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 long oom_badness (struct task_struct *p, unsigned long totalpages) { long points; long adj; if (oom_unkillable_task(p)) return LONG_MIN; p = find_lock_task_mm(p); if (!p) return LONG_MIN; adj = (long )p->signal->oom_score_adj; if (adj == OOM_SCORE_ADJ_MIN || ...) return LONG_MIN; points = get_mm_rss(p->mm) + get_mm_counter(p->mm, MM_SWAPENTS) + mm_pgtables_bytes(p->mm) / PAGE_SIZE; task_unlock(p); adj *= totalpages / 1000 ; points += adj; return points; }
oom_score_adj 可由用户空间写入 /proc/<pid>/oom_score_adj:设为 +1000 意味着优先被杀,设为 -1000 则完全豁免。系统管理程序(如 systemd)、关键守护进程(如 sshd)通常将自己设为负值保护。
6.3 oom_kill_process:执行终止 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 static void oom_kill_process (struct oom_control *oc, const char *message) { struct task_struct *victim = oc->chosen; task_lock(victim); if (task_will_free_mem(victim)) { mark_oom_victim(victim); queue_oom_reaper(victim); task_unlock(victim); put_task_struct(victim); return ; } if (__ratelimit(&oom_rs)) dump_header(oc, victim); __oom_kill_process(victim, message); ... }
__oom_kill_process 向受害进程发送 SIGKILL,并通过 OOM Reaper 机制(异步线程)快速回收其内存,无需等待进程完全退出。
七、诊断方法 7.1 /proc/buddyinfo 显示各节点各 zone 从 order-0 到 order-10 的空闲页块数:
1 2 3 4 $ cat /proc/buddyinfo Node 0, zone DMA 1 0 1 0 2 1 1 0 1 1 3 Node 0, zone DMA32 186 190 86 42 12 5 4 2 1 1 22 Node 0, zone Normal 6543 3280 1754 893 452 220 108 43 19 8 113
每列对应 order 0~10 的空闲块数。高 order 块数多表示内存碎片化较轻。若 order-10 长期为 0 而 order-0 大量堆积,说明存在严重碎片化,THP 分配将频繁失败。
7.2 /proc/zoneinfo 显示每个 zone 的详细统计,包括 watermark、页统计等:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 $ cat /proc/zoneinfo Node 0, zone Normal pages free 123456 min 8192 low 10240 high 12288 spanned 2621440 present 2621440 managed 2568192 cma 0 nr_free_pages 123456 nr_zone_inactive_anon 45678 nr_zone_active_anon 89012 ...
min/low/high 即三条水位线(页数)。managed 是 buddy 管理的总页数,用于 watermark 百分比计算。
7.3 /proc/meminfo 关键字段 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $ cat /proc/meminfo MemTotal: 65536000 kB MemFree: 8192000 kB MemAvailable: 32768000 kB Buffers: 512000 kB Cached: 16384000 kB SwapCached: 102400 kB Active: 24576000 kB Inactive: 16384000 kB Slab: 1024000 kB SReclaimable: 768000 kB SUnreclaim: 256000 kB PageTables: 102400 kB HugePages_Total: 0 AnonHugePages: 4096000 kB
MemAvailable 是比 MemFree 更实用的指标,它估算了在不触发 swap 的情况下应用可以分配的内存量(包括可回收的 page cache 和 slab)。
7.4 /proc/pagetypeinfo 显示各 zone 各迁移类型的页块分布:
1 2 3 4 5 6 7 8 9 10 $ cat /proc/pagetypeinfo Page block order: 9 Pages per block: 512 Free pages count per migrate type at order 0 1 2 ... Node 0, zone DMA, type Unmovable 1 0 1 ... Node 0, zone DMA, type Movable 0 0 0 ... Node 0, zone DMA32, type Unmovable 23 12 7 ... Node 0, zone DMA32, type Movable 163 178 79 ... Node 0, zone DMA32, type Reclaimable 0 0 0 ...
可以观察各迁移类型的碎片情况。若 Unmovable 类型的高阶空闲块很多,说明不可移动页对 Movable 区域的侵占(fallback stealing)较少。
7.5 bpftrace 追踪分配路径 利用 bpftrace 动态追踪 __alloc_pages 的调用情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 bpftrace -e ' kprobe:__alloc_pages { @orders[arg1] = count(); } interval:s:5 { print(@orders); clear(@orders); }' bpftrace -e ' kretprobe:__alloc_pages /retval == 0/ { @failures = count(); printf("alloc failed: gfp=%x order=%d\n", *((gfp_t*)arg0), (int)arg1); }'
7.6 perf 统计分配频率 1 2 3 4 5 6 7 8 9 10 perf stat -e kmem:mm_page_alloc,kmem:mm_page_free \ -a sleep 5 perf record -e kmem:mm_page_alloc -ag sleep 10 perf report --sort =dso,symbol perf stat -e kmem:mm_page_alloc_zone_locked -a sleep 5
mm_page_alloc_zone_locked 事件在 PCP 为空触发 rmqueue_bulk 时记录,其频率高说明 PCP 命中率低,order-0 分配压力大,可以考虑调大 vm.percpu_pagelist_high_fraction。
总结 本文从 NUMA 节点的 pg_data_t、zone 的 struct zone、物理页的 struct page 三层数据结构出发,系统介绍了 Linux 物理内存的组织方式。伙伴分配系统通过按迁移类型分类的多阶空闲链表,以 O(log N) 的时间复杂度实现了对物理页的分配与合并。Per-CPU 页帧缓存在 order-0 分配的快速路径上消除了锁竞争瓶颈。GFP 标志体系为不同上下文的调用者提供了精细化的行为控制。OOM Killer 作为最后防线,通过 oom_badness 的启发式算法尽可能地选择”代价最小”的受害者以恢复系统运转。
理解这些机制是进行内核内存调优、分析内存泄漏与碎片化问题的基础。下一篇文章将深入虚拟内存管理:页表结构、缺页处理与 mmap 的实现机制。
参考源文件
include/linux/mmzone.h — zone/pgdat/free_area/per_cpu_pages 数据结构
include/linux/mm_types.h — struct page/folio/vm_area_struct
include/linux/gfp_types.h — GFP 标志定义
include/linux/gfp.h — GFP 组合标志与分配 API
mm/page_alloc.c — 伙伴分配器核心:__alloc_pages、get_page_from_freelist、rmqueue、__rmqueue_smallest、__free_one_page、rmqueue_pcplist、free_unref_page
mm/oom_kill.c — OOM Killer:out_of_memory、oom_badness、oom_kill_process
本文源码基于 Linux 6.4-rc1 (commit ac9a78681b92),部分实现细节在不同版本间可能有所差异。