内存管理-79-free_pages-6.1
调用路径:
free_pages(addr, order) __free_pages(page, order) free_the_page(page, order) if (pcp_allowed_order(order)) { //计划还给PCP free_unref_page(page, order) free_unref_page_prepare(page, pfn, order) free_pcp_prepare free_pages_prepare trace_mm_page_free(page, order) if (is_migrate_isolate(migratetype)) //ISOLATE 类型页面直接归还 buddy,不走 PCP,HIGHATOMIC 当作 MOVABLE 管理 free_one_page pcp = pcp_spin_trylock(zone->per_cpu_pageset) //先关迁移,然后再try lock 持 per-cpu 的 zone->per_cpu_pageset->lock 锁 if (pcp) { free_unref_page_commit(zone, pcp, page, pcpmigratetype, order); order_to_pindex(migratetype, order) list_add(&page->pcp_list, &pcp->lists[pindex]); //还给PCP if (pcp->count >= high) //若PCP上页面比较多,为了避免碎片化,批量转移到伙伴系统 free_pcppages_bulk spin_lock_irqsave(&zone->lock, flags); //关中断后持 zone->lock 锁 page = list_last_entry(list, struct page, pcp_list); //从PCP链表末尾取页释放进伙伴系统 list_del(&page->pcp_list); if (bulkfree_pcp_prepare(page)) //这里也有丢页行为,但出现了会有报错打印 continue; __free_one_page(page, pfn, zone, order, mt, FPI_NONE); trace_mm_page_pcpu_drain(page, order, mt) spin_unlock_irqrestore(&zone->lock, flags); //释放 zone->lock 锁后开中断 pcp_spin_unlock(pcp); //先释放 per-cpu 的 zone->per_cpu_pageset->lock 锁,然后开迁移 } else { free_one_page(zone, page, pfn, order, migratetype, FPI_NONE); //尝试持PCP锁失败,释放回 buddy. spin_lock_irqsave(&zone->lock, flags); //先关中断再持 zone->lock __free_one_page(page, pfn, zone, order, migratetype, fpi_flags); //见下面实现 spin_unlock_irqrestore(&zone->lock, flags); //释放 zone->lock 后开中断 } pcp_spin_unlock(pcp); } else { __free_pages_ok(page, order, FPI_NONE) //不满足 PCP 的 order 要求,还给 Buddy free_pages_prepare(page, order, true, fpi_flags) //释放页面之前的准备工作 trace_mm_page_free(page, order); spin_lock_irqsave(&zone->lock, flags); //关中断后持 zone->lock __free_one_page(page, pfn, zone, order, migratetype, fpi_flags); while (order < MAX_ORDER - 1) { //核心合并循环 compaction_capture(capc, page, order, migratetype) find_buddy_page_pfn(page, pfn, order, &buddy_pfn); del_page_from_free_list(buddy, zone, order); } set_buddy_order(page, order); add_to_free_list/add_to_free_list_tail //合并后的插入空闲链表头或链表尾 spin_unlock_irqrestore(&zone->lock, flags); //释放 zone->lock 后开中断 }
无论释放到 PCP 还是释放到 Buddy, 都有执行 trace_mm_page_free。
释放路径锁的包含关系也是 zone->per_cpu_pageset->lock 在外,zone->lock 锁在内。
一、简介
略
二、相关结构
略
三、全局变量
略
四、接口函数
/* 通过虚拟地址释放页 */ void free_pages(unsigned long addr, unsigned int order) /* 通过page指针释放页 */ void __free_pages(struct page *page, unsigned int order)
五、逻辑实现
1. free_pages()
void free_pages(unsigned long addr, unsigned int order) //page_alloc.c { /* 若是 free 一个 0 地址,直接什么都不做 */ if (addr != 0) { /* 断言 addr 必须得是线性映射的地址,且有对应物理内存的,且物理没存没有被标记 MEMBLOCK_NOMAP */ VM_BUG_ON(!virt_addr_valid((void *)addr)); /* 将地址转换为 page 指针 */ __free_pages(virt_to_page((void *)addr), order); } }
1.1 virt_addr_valid()
//arch/arm64/include/asm/memory.h #define virt_addr_valid(addr) ({ \ __typeof__(addr) __addr = __tag_reset(addr); \ __is_lm_address(__addr) && pfn_is_map_memory(virt_to_pfn(__addr)); \ })
__tag_reset: 清除 MTE/指针 tag. ARM64 支持 MTE(Memory Tagging Extension),指针的最高位字节(最高8bit)可能被用作 tag.
作用: 清除 addr 最高字节的 tag 标签(替换为 ff),然后要求 addr 必须是线性映射的地址,且 addr 所在的物理页必须属于"被内核线性映射的正常系统 RAM",排除掉 MMIO 设备地址(不是系统 RAM), 和虽然是系统 RAM 但被标记为 MEMBLOCK_NOMAP(如 TEE 保留区、firmware 保留区)的保留区域。
1.2 virt_to_page()
#define virt_to_page(x) ({ \ u64 __idx = (__tag_reset((u64)x) - PAGE_OFFSET) / PAGE_SIZE; \ u64 __addr = VMEMMAP_START + (__idx * sizeof(struct page)); \ (struct page *)__addr; \ })
作用: 给定一个内核虚拟地址,返回该地址对应物理页面的 struct page 指针。
实现逻辑: 一个线性映射的地址 x 减去线性映射的起始地址,除以 PAGE_SIZE 后得到 page 页序号 idx, 然后 page 映射的首地址 VMEMMAP_START 加上这个偏移,得到 page 地址。
2. __free_pages()
(1) 此函数前的英文注释
这是内核中最常用的页面释放接口。它先减少页面引用计数,只有当引用计数降为 0 时才真正释放。
此函数可以释放非复合页的多页分配。它不会检查传入的 @order 是否与分配 order 匹配,因此很容易造成内存泄漏。释放的内存超过分配的内存量可能会发出警告。
如果对该页面的最后一个引用是推测性的,则会由 put_page() 释放,但 put_page() 只会释放非复合页分配的第一个页面####。为了防止剩余页面泄漏,我们在此处释放后续页面。如果您希望使用页面的引用计数来决定何时释放分配,则应分配复合页,并使用 put_page() 而不是 __free_pages()。
调用上下文:可以在中断上下文中或持有普通 spinlock 时调用,但不能在 NMI 上下文中或持有 raw spinlock(非RT内核可以) 时调用。
(2) spinlock 和 raw spinlock 的区别
在标准的不使能 PREEMPT_RT 的内核中,spinlock_t 和 raw_spinlock_t 行为一样,都是自旋锁,即持 raw spinlock 后调用也是安全的。区别体现在 PREEMPT_RT 带实时补丁的内核中,两者区别:
-------------------------------------------------------------------------------------- 类型 非 RT 内核 PREEMPT_RT 内核 -------------------------------------------------------------------------------------- spinlock_t 真正的自旋(关抢占) 被转换为 rt_mutex(可睡眠、可抢占) raw_spinlock_t 真正的自旋(关抢占) 仍然是真正的自旋(关抢占、可能关中断) --------------------------------------------------------------------------------------
raw_spinlock_t 的含义是:无论什么内核配置,我都必须真正自旋,不能被转换为睡眠锁。使用场景:调度器、中断控制器、时钟子系统等绝对不能睡眠的底层关键路径。
(3) 为什么持有 raw spinlock 时不能调用
这只是针对使能了 PREEMPT_RT 的内核来说的。在 __free_pages() 执行流程中会去持 pcp_spin_trylock(zone->per_cpu_pageset) 和 zone->lock 锁,这两个都是通过 spin_lock() 去持有的(而不是 raw_spin_lock()), 会休眠。原子上下文不能休眠,所以不能调用。
(4) 为什么 NMI 上下文中不能调用
NMI(Non-Maskable Interrupt)的特性:
-------------------------------------------------------------------------------------- 特性 普通中断(IRQ) NMI -------------------------------------------------------------------------------------- 能否被屏蔽 能(local_irq_disable) 不能 能否被其他中断打断 不能(同级) 能打断任何东西,包括关中断区间 能否安全获取 spinlock 能(如果用 _irqsave 变体) 不能 --------------------------------------------------------------------------------------
NMI 可以在任何时刻打断 CPU,包括 打断正在持有 spin_lock_irqsave(&zone->lock, flags) 的代码、打断正在持有 PCP 锁的代码、打断正在关中断的代码。
若 NMI 中断 handler 中调用 __free_pages(), NMI 中断打断 zone->lock 持锁临时取,而函数内部会再次持有 zone->lock,会造成 AA 死锁。
(5) 函数实现
void __free_pages(struct page *page, unsigned int order) { /* * 在减少引用计数之前,先记录这是否是 compound page 的 head page。因为 put_page_testzero 之后 page 可能已经 * 被其他 CPU 重新分配,此时再读 PageHead 就不安全了。 * 检查 page->flags 中的 PG_head 标志 */ int head = PageHead(page); /* 原子地执行 page->_refcount--, 若结果变为 0 了, 则为真,才执行释放页面的操作 */ if (put_page_testzero(page)) /* 正常路径:引用计数归零,释放整个 2^order 块 */ free_the_page(page, order); /* * 若是引用计数未归 0 的复合页的 head 页此函数什么都不做,直接退出。 * 特殊情况:page 引用计数未归零,且不是 compound page 的 head 页。 */ else if (!head) /* * 这发生在高 order 分配被拆分使用的场景(比如分配16页但是只需要9页,如 page fragment): * 调用者分配了 order=N 的页,但只用了前半部分,前半部分的 head page 还有其他引用(_refcount > 0),后半部 * 分的页面没人用了,需要逐级释放回 buddy * * 具体做法:从高 order 到低 order,依次释放后半部分。例如 order=3(8页),如果 page[0] 还有引用: * 释放 page[4..7] 作为 order=2 * 释放 page[2..3] 作为 order=1 * 释放 page[1] 作为 order=0 * page[0] 保留(仍有引用,只保留 page[0]页) */ while (order-- > 0) free_the_page(page + (1 << order), order); }
内核中几乎没有 "else if" 这种使用case, 可理解为是一个防御性保障,这个分支确保"我释放不了 head page,但至少把不需要的尾部页面还给系统"。思想是非 compound 高阶分配可以部分释放。
2.1 put_page_testzero()
static inline int put_page_testzero(struct page *page) { int ret; /* 释放前,要求 page->_refcount 不能等于0(重复释放) */ VM_BUG_ON_PAGE(page_ref_count(page) == 0, page); /* 原子执行 page->_refcount--, 若结果 page->_refcount=0 了则返回 true, 否则返回 false */ ret = page_ref_dec_and_test(page); page_pinner_put_page(page); return ret; }
2.2 free_the_page()
static inline void free_the_page(struct page *page, unsigned int order) { /* order=[0,3] 或 10(THP), 归还到 PCP, 相当于归还时的快速路径 */ if (pcp_allowed_order(order)) free_unref_page(page, order); else /* 归还给 buddy,相当于归还时的慢速路径 */ __free_pages_ok(page, order, FPI_NONE); }
2.2.1 free_unref_page()
/** * free_unref_page - 将页面释放到当前 CPU 的 PCP 列表。 * PCP(Per-CPU Pages)是每个 CPU 维护的页面缓存列表,分配/释放时不需要获取 zone->lock,大幅减少锁竞争。 */ void free_unref_page(struct page *page, unsigned int order) { unsigned long __maybe_unused UP_flags; struct per_cpu_pages *pcp; struct zone *zone; unsigned long pfn = page_to_pfn(page); int migratetype, pcpmigratetype; bool skip_free_unref_page = false; /* 做释放前的准备工作,包括一些校验、清理、涂毒等 */ if (!free_unref_page_prepare(page, pfn, order)) return; /* 获取页面的迁移类型,函数内直接 return page->index, 上面函数刚将 page->index = migratetype */ migratetype = get_pcppage_migratetype(page); /* Android vendor hook: 允许厂商拦截释放操作 */ trace_android_vh_free_unref_page_bypass(page, order, migratetype, &skip_free_unref_page); if (skip_free_unref_page) return; /* * PCP 列表只维护 UNMOVABLE, RECLAIMABLE, MOVABLE, CMA 这几种 migratetype: * * 特殊处理: * - ISOLATE 类型:不能放入 PCP,直接走 buddy 释放。 * - HIGHATOMIC 类型:当作 MOVABLE 放入 PCP,因为 PCP 中没有高阶原子类型的空闲链表。 */ migratetype = pcpmigratetype = get_pcppage_migratetype(page); //返回的是 page->index; if (unlikely(migratetype > MIGRATE_RECLAIMABLE)) { /* ISOLATE 页面直接归还 buddy,不走 PCP */ if (unlikely(is_migrate_isolate(migratetype))) { free_one_page(page_zone(page), page, pfn, order, migratetype, FPI_NONE); return; } /* 因为 PCP 不维护高阶原子类型, 将其当作 MOVABLE 类型放入PCP */ if (pcpmigratetype == MIGRATE_HIGHATOMIC) pcpmigratetype = MIGRATE_MOVABLE; } /* page->flags 中有记录它属于哪个 node 和哪个 zone */ zone = page_zone(page); /* 默认不使能 CONFIG_PREEMPT_RT, 是空实现 */ pcp_trylock_prepare(UP_flags); /* * 尝试获取 PCP 的锁(trylock,非阻塞, 任何上下文都安全)。 * 如果成功,释放到 PCP 列表; 如果失败,释放到 buddy 中,避免自旋等待. * * 获取PCP锁的方式: 先关迁移,然后再尝试持 per-cpu 的 zone->per_cpu_pageset->lock 锁 */ pcp = pcp_spin_trylock(zone->per_cpu_pageset); if (pcp) { /* 将页面实际放入 PCP 列表的函数 */ free_unref_page_commit(zone, pcp, page, pcpmigratetype, order); /* 先释放 per-cpu 的 zone->per_cpu_pageset->lock 锁,然后开迁移 */ pcp_spin_unlock(pcp); } else { /* PCP 锁获取失败,回退到 buddy 路径 */ free_one_page(zone, page, pfn, order, migratetype, FPI_NONE); } /* 默认不使能 CONFIG_PREEMPT_RT, 是空实现 */ pcp_trylock_finish(UP_flags); }
2.2.1.1 free_unref_page_prepare()
static bool free_unref_page_prepare(struct page *page, unsigned long pfn, unsigned int order) { int migratetype; if (!free_pcp_prepare(page, order)) return false; /* 获取 page 所在的 pageblock 的迁移类型 */ migratetype = get_pfnblock_migratetype(page, pfn); /* 只是执行 page->index = migratetype, 将迁移类型保存到 page->index 上 */ set_pcppage_migratetype(page, migratetype); return true; }
2.2.1.1.1 free_pcp_prepare()
static bool free_pcp_prepare(struct page *page, unsigned int order) { if (debug_pagealloc_enabled_static()) return free_pages_prepare(page, order, true, FPI_NONE); else /* 走这里 */ return free_pages_prepare(page, order, false, FPI_NONE); }
2.2.1.1.1.1 free_pages_prepare()
/* * 返回值:true = 页面合法可释放;false = 页面有问题,不要释放(如硬件中毒页) * * 这个函数做三件大事: * (1) 合法性检查(检测 double-free、corruption 等错误) * (2) 状态清理(清 flags、解除 memcg 计费、重置 owner) * (3) 内存投毒(KASAN poison、debug_pagealloc unmap,帮助检测 use-after-free) /
此函数中有个trace打印,格式为:
kworker/u16:1-22406 [003] ..... 32708.519937: mm_page_free: page=00000000fe6f1acd pfn=0x5890e4 order=2 WatchdogPerfSvc-420 [004] ..s.. 32708.519956: mm_page_free: page=00000000ea25779c pfn=0x5932e8 order=3 SpeechStateMana-8819 [007] ...2. 32708.520236: mm_page_free: page=00000000c6bd1051 pfn=0x439a26 order=0
2.2.1.2 free_one_page()
static void free_one_page(struct zone *zone, struct page *page, unsigned long pfn, unsigned int order, int migratetype, fpi_t fpi_flags) //传参 fpi_flags=FPI_NONE { unsigned long flags; spin_lock_irqsave(&zone->lock, flags); //先关中断再持 zone->lock /* * 持锁后对特定类型再次获取一次迁移类型,确保迁移类型是准确的 * * 等效于 if(zone->nr_isolate_pageblock || migratetype == MIGRATE_ISOLATE) */ if (unlikely(has_isolate_pageblock(zone) || is_migrate_isolate(migratetype))) { migratetype = get_pfnblock_migratetype(page, pfn); } /* 伙伴系统的核心释放+合并函数 */ __free_one_page(page, pfn, zone, order, migratetype, fpi_flags); spin_unlock_irqrestore(&zone->lock, flags); //释放 zone->lock 后开中断 }
__free_one_page 放在后续向伙伴系统释放时详解。
2.2.1.3 free_unref_page_commit()
/* * 此函数将页面实际放入 PCP 列表。 * PCP 有多个子列表,按 (migratetype, order) 二维索引。当 PCP 中缓存的页面数超过 high watermark 时, * 批量将多余的页面归还给伙伴系统。 */ static void free_unref_page_commit(struct zone *zone, struct per_cpu_pages *pcp, struct page *page, int migratetype, unsigned int order) { int high; int pindex; bool free_high; /* 统计 PGFREE 事件,是释放进PCP链表中的页数. cat /proc/vmstat | grep -i PGFREE */ __count_vm_events(PGFREE, 1 << order); /* 根据 migratetype 和 order 计算 PCP 列表索引, 找到要释放回的PCP中具体的空闲链表 */ pindex = order_to_pindex(migratetype, order); /* 将页面加入对应 PCP 列表的头部(LIFO,热页优先复用) */ list_add(&page->pcp_list, &pcp->lists[pindex]); pcp->count += 1 << order; /* * 由于存储在 PCP 上的除 THP 之外的高阶页面也会导致碎片,因此在 PCP 大量释放内存且未分配内存时, * 应限制存储的高阶页面数量。批量释放停止后,剩余的页面将从 vmstat 刷新上下文中移除。 * * 如果最近持续在释放高 order 页面(free_factor > 0),且 order 在 1~3 之间,标记需要更积极地回收 PCP。 * 避免 PCP 中积累过多高 order 页导致碎片化。 */ free_high = (pcp->free_factor && order && order <= PAGE_ALLOC_COSTLY_ORDER); //3 /* 计算当前 PCP 的 high watermark,实际使用中的 high 值不是固定不变的,是变化的,pcp->high 只是最大值限制 */ high = nr_pcp_high(pcp, zone, free_high); /* * 如果 PCP 中的页面数超过 high watermark,批量释放一部分页面回伙伴分配器。 * 这是"懒释放"策略:不是每次都归还 buddy,而是攒到一定量再批量处理。 */ if (pcp->count >= high) { int batch = READ_ONCE(pcp->batch); /* 释放回伙伴系统 */ free_pcppages_bulk(zone, nr_pcp_free(pcp, high, batch, free_high), pcp, pindex); } }
2.2.1.3.1 nr_pcp_high()
/** * nr_pcp_high - 计算当前 PCP 最多允许能缓存多少页(high watermark) * @pcp: 当前 CPU 的 per_cpu_pages * @zone: 所属的 zone * @free_high: 是否因为"连续释放高阶页面"而需要积极清空 * * 返回值含义: * > 0: PCP 当前最多允许缓存这么多页,超过就触发归还; * = 0: PCP 当前不允许缓存任何页面,任何释放都立刻归还 buddy(相当于屏蔽了 PCP), 因为 free_unref_page_commit() 中判断 pcp->batch >= 这里返回的 high 就会往伙伴系统释放 。 * * 此函数在 free_unref_page_commit() 中唯一调用。传参: free_high = (pcp->free_factor && order && order <= PAGE_ALLOC_COSTLY_ORDER); */ static int nr_pcp_high(struct per_cpu_pages *pcp, struct zone *zone, bool free_high) { int high = READ_ONCE(pcp->high); /* * 情况1: PCP 已被禁用 或在连续释放高阶页(1-3) * (1) high == 0 表示 PCP 已被禁用(zone_pcp_disable 设了 pcp->high=0) * (2) free_high == true 表示正在连续释放高阶页面,不应在 PCP 中积压. * 返回 0 表示 PCP 不允许缓存任何页面,任何释放都立刻归还 buddy, PCP 等于"透明"即不缓存。 */ if (unlikely(!high || free_high)) return 0; /* * 情况2: zone 没有在做异步内存回收,返回正常的 high 值. * 这是常规路径。high 通常是几百到几千(根据 zone 大小计算)。PCP 可以自由缓存到 high 个页面才触发归还。 */ if (!test_bit(ZONE_RECLAIM_ACTIVE, &zone->flags)) return high; /* * 情况3:zone 的 kswapd 正在做异步内存回收 * * 回收活跃时,限制 PCP 缓存页面数量为 batch * 4(应该还可以再小点),以便让更多页面回到 buddy 的 free_area 中, * 这样 reclaim 释放的页面能被其他 CPU 看到和使用,而不是积压在某个 CPU 的 PCP 里"私藏"。 * * 如果在回收流程中,限制可存储在 PCP 列表中的页面数量。 */ return min(READ_ONCE(pcp->batch) << 2, high); }
zone->flags 中的 ZONE_RECLAIM_ACTIVE 的位置和取消位置:
kswapd balance_pgdat restart: set_reclaim_active update_reclaim_active(pgdat, highest_zoneidx, true) set_bit(ZONE_RECLAIM_ACTIVE, &zone->flags); out: clear_reclaim_active update_reclaim_active(pgdat, highest_zoneidx, false) clear_bit(ZONE_RECLAIM_ACTIVE, &zone->flags);
即 zone->flags 中的 ZONE_RECLAIM_ACTIVE 表示当前此 zone 中正在进行 kswapd 异步内存回收。
2.2.1.3.2 nr_pcp_free()
/** * nr_pcp_free - 计算本次应该从 PCP 归还多少页给 buddy * @pcp: 当前 CPU 的 per_cpu_pages * @high: 当前的 PCP 水位线(来自 nr_pcp_high 的返回值) * @batch: pcp->batch 的值,基础归还批量,默认固定为63。 * @free_high: 是否需要激进清空,连续释放高阶页面时会传true. * * 返回值:本次应归还的页面数 * * 设计思想: * - 正常情况下归还 batch 个页面,PCP 中保留 batch 个供下次分配 * - 连续释放时逐次翻倍(加速清空),避免 PCP 成为瓶颈 * - 高阶页面激进清空时全部归还。 * * 唯一调用位置 free_unref_page_commit: * 传参 free_high = (pcp->free_factor && order && order <= PAGE_ALLOC_COSTLY_ORDER); //3 */ static int nr_pcp_free(struct per_cpu_pages *pcp, int high, int batch, bool free_high) { int min_nr_free, max_nr_free; /* * 情况1:归还 PCP 中的全部页面 * * free_high = true 表示正在连续释放 order 1~3 的页面。 * 高阶页面占空间大,积压会加剧碎片化。全部归还 buddy,让 buddy 有机会将它们合并成更大的块。 */ if (unlikely(free_high)) return pcp->count; /* * 情况2:PCP 已禁用或 boot pageset(high < batch) * * high=0 且 batch=1 的情况(zone_pcp_disable 后)。只归还 1 个,因为 PCP 此时每次只有 1 页进来就触发归还。 */ if (unlikely(high < batch)) return 1; /* * 正常路径:计算归还数量 * * 原则:PCP 中至少保留 batch 个页面(供下次分配用,避免频繁从 buddy 进货), 最多归还 high - batch 个. */ /* Leave at least pcp->batch pages on the list */ min_nr_free = batch; max_nr_free = high - batch; //可能 max_nr_free 比 min_nr_free 还小,但是不会造成逻辑异常。 /* * 核心:指数加速机制 * * batch 左移 free_factor 位 = batch * 2^free_factor * * 如果连续释放无分配插入: * 第1次超水位:归还 batch * 1 = 31 (free_factor=0) * 第2次超水位:归还 batch * 2 = 62 (free_factor=1) * 第3次超水位:归还 batch * 4 = 124 (free_factor=2) * 第4次超水位:归还 batch * 8 = 248 (free_factor=3) * 第5次超水位:归还 batch * 16 = 496 (free_factor=4) * 第6次超水位:归还 batch * 32 = 992 (free_factor=5,上限) */ batch <<= pcp->free_factor; /* * 如果加速后的 batch 还没到最大值且 free_factor 没到上限:则增加 free_factor,下次归还更多。 * CONFIG_PCP_BATCH_SCALE_MAX = 5,所以 free_factor 最大为 5(即翻倍 32 次) */ if (batch < max_nr_free && pcp->free_factor < CONFIG_PCP_BATCH_SCALE_MAX) //5 pcp->free_factor++; /* * 将返回值限制在 min 和 max 之间 * 若出现 max_nr_free 比 min_nr_free,还小,clamp 实现是 min(max(val, lo), hi),即先被 lo 抬高,再被 hi 压低, * 当 hi < lo 时,最终结果等于 hi,也是安全的 */ batch = clamp(batch, min_nr_free, max_nr_free); return batch; }
2.2.1.3.3 free_pcppages_bulk()
/** * free_pcppages_bulk - 批量将 PCP 中的页面归还给 buddy allocator * @count: 要归还的总页面数(以 page 为单位,不是条目数) * @pindex: 触发归还的那个 PCP 子列表索引(优先从它开始释放) * * 此函数持有 zone->lock 操作 buddy 的 free_area。采用 round-robin 方式从各个 PCP 子列表轮流取页面归还, * 避免某一种 migratetype/order 的缓存被全部清空而其他的不动。 * 此函数是在持 zone->per_cpu_pageset->lock 锁的条件下调用的,目标是从 pcp->list[pindex] 上释放 count 个页, * 若此 list 不够释放,再释放别的 list 上的页 */ static void free_pcppages_bulk(struct zone *zone, int count, struct per_cpu_pages *pcp, int pindex) { unsigned long flags; /* round-robin 的下界 */ int min_pindex = 0; /* round-robin 的上界 */ int max_pindex = NR_PCP_LISTS - 1; //17-1 unsigned int order; bool isolated_pageblocks; struct page *page; /* * 防御性检查:如果请求归还的 count 比 PCP 中实际拥有的还多, * 以实际拥有的为准,避免后面循环时 PCP 已空但 count 仍 > 0 导致死循环。 */ count = min(pcp->count, count); /* * 让 round-robin 从 pindex-1 开始。因为循环开头会先 ++pindex,这样第一次 ++pindex 后恰好等于传入的 pindex, * 确保触发归还的那个列表**优先**被释放。 * * 为什么优先释放触发者? * 因为触发归还的原因是"这个 pindex 对应的列表刚刚又收到了页面导致超水位",优先释放它能最快降低 PCP 的 count。 */ pindex = pindex - 1; /* 要操作 buddy 的 free_area 链表,因此要持锁 */ spin_lock_irqsave(&zone->lock, flags); //关中断后持 zone->lock 锁 /* * 检查 zone 中是否存在 ISOLATE 类型的 pageblock。如果有,后面每个页面释放时需要重新确认其 pageblock 的 * migratetype,因为 pageblock 类型可能在我们缓存 migratetype 之后被并发修改为 ISOLATE。 */ isolated_pageblocks = has_isolate_pageblock(zone); //zone->nr_isolate_pageblock; /* 主循环:归还 count 个页面给 buddy */ while (count > 0) { struct list_head *list; int nr_pages; /* Round-robin 选择下一个非空的 PCP 子列表。这样各个 order 和各个 migration 的链表能均匀减少 */ do { /* 让 pindex 循环 */ if (++pindex > max_pindex) pindex = min_pindex; list = &pcp->lists[pindex]; /* 选择一个非空的 list */ if (!list_empty(list)) break; /* * 当前 pindex 对应的列表为空。收缩搜索范围——如果边界处的列表持续为空,就把搜索范围缩小, * 避免反复扫描已知为空的列表。 * 这是一个优化:当大部分列表都空了时,快速缩小 [min_pindex, max_pindex] 范围,减少无效遍历。 */ if (pindex == max_pindex) max_pindex--; if (pindex == min_pindex) min_pindex++; } while (1); /* 此 pindex 对应的 order 和每个页面块的页数 */ order = pindex_to_order(pindex); nr_pages = 1 << order; /* * 内循环:从当前列表中连续取页面归还 buddy,直到 count 耗尽或这个列表被取空。 * 从列表尾部取:释放到 PCP 时放头部(热页优先分配),归还 buddy 时从尾部取(冷页优先归还) */ do { int mt; /* 从列表尾部取页面(FIFO,冷页优先释放) */ page = list_last_entry(list, struct page, pcp_list); /* 获取这个页面缓存时记录的 migratetype */ mt = get_pcppage_migratetype(page); /* must delete to avoid corrupting pcp list */ /* 从 PCP 链表上摘下来,在持 zone->per_cpu_pageset->lock 锁的条件下走到这里,是安全的 */ list_del(&page->pcp_list); count -= nr_pages; pcp->count -= nr_pages; /* * 重新检查页面合法性。返回 true 表示页面有问题(如 double-free),跳过不归还。若有丢弃会有带限速 * 的报错打印的。 */ if (bulkfree_pcp_prepare(page)) continue; /* 合法性断言:ISOLATE 类型的页面不应该出现在 PCP 中 */ VM_BUG_ON_PAGE(is_migrate_isolate(mt), page); /* * 如果 zone 中存在 ISOLATE pageblock: * 页面进入 PCP 时记录的 migratetype 可能已过期——在 PCP 缓存期间,该页面所在的 pageblock 可能被 * 并发修改为 ISOLATE(如 memory hotplug 正在隔离)。需要重新读取当前的 pageblock migratetype。 */ if (unlikely(isolated_pageblocks)) mt = get_pageblock_migratetype(page); /* * 核心:将页面归还给 buddy allocator。此函数会尝试与伙伴合并,放入对应 order 的 free_list。 * 同时 NR_FREE_PAGES 会增加。 */ __free_one_page(page, page_to_pfn(page), zone, order, mt, FPI_NONE); /* ftrace 事件:PCP drain 一个页面块 */ trace_mm_page_pcpu_drain(page, order, mt); /* * 内循环退出条件:(1) count <= 0:已归还够了; (2) list 为空:这个子列表清空了,需要回到外层 round-robin 选下一个 * 也即使命的操作单个列表,直到清空或还够。 * 这会不会乒乓呢?好像可优化,比如每个列表可先释放一半,还不够每个列表再释放一半。或 根据 PCP 中每个 list 的 order * 决定释放比例。 */ } while (count > 0 && !list_empty(list)); } spin_unlock_irqrestore(&zone->lock, flags); //释放 zone->lock 锁后开中断 }
trace打印如下,每行表示从 PCP 的一个 list 上释放回伙伴系统一个页面块。
khugepaged-69 [004] d..2. 17900.614102: mm_page_pcpu_drain: page=000000008f06b236 pfn=0x1156d2 order=0 migratetype=3 khugepaged-69 [004] d..2. 17900.614110: mm_page_pcpu_drain: page=000000002bb7288c pfn=0x16b178 order=1 migratetype=0 khugepaged-69 [004] d..2. 17900.614111: mm_page_pcpu_drain: page=00000000bc93fb5c pfn=0x147214 order=2 migratetype=0
2.2.1.3.3.1 bulkfree_pcp_prepare
这是从 PCP 释放回伙伴系统前的检测。详见后续 页面校验 相关BK。
2.2.1.3.3.2 __free_one_page
见下面伙伴系统释放小节详解。
2.2.2 __free_pages_ok()
/** * __free_pages_ok - 直接将高 order 页面直接归还 buddy allocator * @page: 要释放的页面 * @order: 页面的 order * @fpi_flags: 释放标志(如 FPI_TO_TAIL 等) * 不经过 PCP,直接在 buddy 的 free_area 中操作。需要持有 zone->lock。 * * free_the_page: (page, order, FPI_NONE) */ static void __free_pages_ok(struct page *page, unsigned int order, fpi_t fpi_flags) { unsigned long flags; int migratetype; unsigned long pfn = page_to_pfn(page); struct zone *zone = page_zone(page); bool skip_free_unref_page = false; /* 释放前的通用准备(与 free_unref_page_prepare 类似) */ if (!free_pages_prepare(page, order, true, fpi_flags)) return; /* 根据页面 PFN 对应的 pageblock 获取 migratetype */ migratetype = get_pfnblock_migratetype(page, pfn); /* Android vendor hook */ trace_android_vh_free_unref_page_bypass(page, order, migratetype, &skip_free_unref_page); if (skip_free_unref_page) return; spin_lock_irqsave(&zone->lock, flags); //关中断后持 zone->lock /* * 如果 zone 中存在 ISOLATE 的 pageblock,需要重新读取 migratetype,因为在获取锁之前 pageblock * 的类型可能已经被并发修改。 */ if (unlikely(has_isolate_pageblock(zone) || is_migrate_isolate(migratetype))) { migratetype = get_pfnblock_migratetype(page, pfn); } /* 核心:将页面归还到 buddy free_area,并尝试合并伙伴 */ __free_one_page(page, pfn, zone, order, migratetype, fpi_flags); spin_unlock_irqrestore(&zone->lock, flags); //释放 zone->lock 后开中断 /* 无论释放进伙伴系统还是释放进PCP, 都会有这个计数 */ __count_vm_events(PGFREE, 1 << order); }
2.2.2.1 free_pages_prepare()
在之后的 内存准备与校验 中介绍。
2.2.2.2 __free_one_page
/** * __free_one_page - buddy allocator 的核心释放+合并函数 * @page: 要释放的页面 * @pfn: 页面的物理页帧号 * @zone: 所属的 zone * @order: 当前 order * @migratetype: 迁移类型 * @fpi_flags: 释放标志 * * 这是 buddy system 的精髓:释放一个 2^order 的页面块时,检查它的"伙伴"(buddy)是否也空闲, * 如果是则合并成 2^(order+1) 的块,然后继续检查更高 order 的伙伴,直到无法再合并为止。 * * Buddy 的定义:对于 PFN 为 pfn、大小为 2^order 的块,它的 buddy 的 PFN = pfn XOR (1 << order) * 两者合并后的 PFN = pfn AND buddy_pfn(取较小值),见下面六章节说明。 * 即 pfn 的 bit[order] 区反找伙伴,bit[order] 位为0的是合并后的起始 pfn. */ static inline void __free_one_page(struct page *page, unsigned long pfn, struct zone *zone, unsigned int order, int migratetype, fpi_t fpi_flags) { struct capture_control *capc = task_capc(zone); unsigned long buddy_pfn = 0; unsigned long combined_pfn; struct page *buddy; bool to_tail; bool bypass = false; /* Android vendor hook: 允许厂商完全接管释放逻辑 */ trace_android_vh_free_one_page_bypass(page, zone, order, migratetype, (int)fpi_flags, &bypass); if (bypass) return; VM_BUG_ON(!zone_is_initialized(zone)); //return zone->initialized; /* 释放前不应残留分配时才有的标志 */ VM_BUG_ON_PAGE(page->flags & PAGE_FLAGS_CHECK_AT_PREP, page); /* migratetype 不能为非法值 */ VM_BUG_ON(migratetype == -1); /* * 更新 NR_FREE_PAGES(NR_FREE_CMA_PAGES) 统计。页面回到 buddy, 让 zone 的空闲页数增加 2^order。 * ISOLATE 类型不计入空闲统计,因为它们被隔离了,不参与正常分配。 */ if (likely(!is_migrate_isolate(migratetype))) /* 在 NR_FREE_PAGES(NR_FREE_CMA_PAGES) 中记录这个页面块 */ __mod_zone_freepage_state(zone, 1 << order, migratetype); /* 合法性检查:PFN 必须是 2^order 对齐的 */ VM_BUG_ON_PAGE(pfn & ((1 << order) - 1), page); /* 页面必须在 zone 的合法范围内,默认不使能 CONFIG_DEBUG_VM, bad_range()恒返回 false,不执行 */ VM_BUG_ON_PAGE(bad_range(zone, page), page); /* 核心合并循环: 从当前 order 开始,逐级向上尝试合并伙伴,直到 MAX_ORDER-1 或被 compaction 捕获。*/ while (order < MAX_ORDER - 1) { /* * Compaction 捕获检查。如果当前任务在做 direct compaction,且页面刚好合并到了目标 order,"截胡"这 * 个页面 —— 不放入 free_list,直接交给 compaction 分配者。 */ if (compaction_capture(capc, page, order, migratetype)) { /* 捕获成功!扣除 free 计数(因为页面不放入 free_list),再从 NR_FREE_PAGES(NR_FREE_CMA_PAGES) 中移除这个页面块 */ __mod_zone_freepage_state(zone, -(1 << order), migratetype); /* 直接返回,不继续合并,不放入 free_list */ return; } /* * 寻找伙伴: buddy_pfn = pfn ^ (1 << order), 即 bit[order] 位取反得到伙伴的 pfn. * 然后验证 buddy 是否: * 在同一个 zone 内、确实空闲(PageBuddy 标志被设置)、order 匹配(buddy_order == order). 任一条件不满足返回 NULL --> 停止合并。 */ buddy = find_buddy_page_pfn(page, pfn, order, &buddy_pfn); if (!buddy) goto done_merging; /* * 跨 pageblock 合并限制: 当 order >= pageblock_order 时,合并将跨越 pageblock 边界。 * * 不同 migratetype 的 pageblock 之间合并有限制:ISOLATE 和 HIGHATOMIC 属于"不可合并"类型。如果两个 pageblock * 一个是 ISOLATE/HIGHATOMIC 另一个不是, 则放弃合并,原因是合并后统计会出错(一个块跨两种计费方式),且 ISOLATE * pageblock 被隔离是有意为之的。 * * MOVABLE/UNMOVABLE/RECLAIMABLE 三者之间是允许跨 pageblock 合并的。 * * 默认配置下这个恒不成立。因为上面有判断 order < 10 才会进来,这里不可能大于或等于10。这是采用小 pageblock * 的条件下才成立。 */ if (unlikely(order >= pageblock_order)) { /* * We want to prevent merge between freepages on pageblock * without fallbacks and normal pageblock. Without this, * pageblock isolation could cause incorrect freepage or CMA * accounting or HIGHATOMIC accounting. */ /* 获取伙伴所在pageblock 的迁移类型 */ int buddy_mt = get_pageblock_migratetype(buddy); if (migratetype != buddy_mt && (!migratetype_is_mergeable(migratetype) || !migratetype_is_mergeable(buddy_mt))) //return mt <= MIGRATE_RECLAIMABLE; goto done_merging; } /* * 合并操作:将 buddy 从当前 order 的 free_list 中删除, 计算合并后的 PFN(两者中较小的那个), order + 1, * 继续尝试更高级别的合并。 * debug 模式下可能将某些页面标记为 guard(不可访问哨兵页)。合并时需要先清除 guard 状态。 * 默认不使能 CONFIG_DEBUG_PAGEALLOC, page_is_guard 恒返回 false, 执行 else 分支。 */ if (page_is_guard(buddy)) clear_page_guard(zone, buddy, order, migratetype); //TODO: 为啥是 guard 就不需要从buddy上摘下来了呢? else del_page_from_free_list(buddy, zone, order); /* 合并后的起始 pfn */ combined_pfn = buddy_pfn & pfn; /* page 指向合并后块的首页, delta 小于 0 或等于 0, page 往前指 */ page = page + (combined_pfn - pfn); /* 以页面块起始为 pfn, 大小为 order+1 继续遍历 */ pfn = combined_pfn; order++; } done_merging: /* 合并循环逻辑执行完毕,准备放回伙伴系统。设置最终合并后的 order 到 page->private 并对 page->flags 设置 PG_buddy 标志 */ set_buddy_order(page, order); /* * 决定将合并后的块添加到 free_list 的头部还是尾部: * - FPI_TO_TAIL: 强制放尾部(如启动时初始化内存)。尾部,页面停留更久,有更多时间等伙伴空闲后继续合并。头部,页面会被更快分 * 配出去(LIFO 热页优先) * - shuffle: 随机化(CONFIG_SHUFFLE_PAGE_ALLOCATOR) * - buddy_merge_likely: 如果预测未来还能继续合并,放尾部(给其他页面先被分配出去的机会,让本块有时间等到伙伴空闲后继续合并) * * 看起来空闲链表上的页面快并不是按 pfn 顺序排列的。 */ if (fpi_flags & FPI_TO_TAIL) /* FPI_TO_TAIL:强制放尾部。场景:内存上线、隔离放回 —— 这些是"冷"页面。启动时初始化内存也是 */ to_tail = true; else if (is_shuffle_order(order)) //return order >= 10 /* CONFIG_SHUFFLE_PAGE_ALLOCATOR:对高阶页面(order >= 10)做随机化,随机放链表头或链表尾。安全加固:防止攻击者预测页面分配顺序 */ to_tail = shuffle_pick_tail(); else /* * 默认启发式: * (1) 对于 9 10 两个 order,恒返回false, 表示放在空闲链表头。 * (2) 其它order 检查"如果此块的下一级伙伴也空闲了,能否继续合并?" 即:查看合并后块的 order+1 伙伴是否已经在 free_list 上。 * 如果 likely --> 放尾部(给它时间等待进一步合并,减少碎片); * 如果 unlikely --> 放头部(不太可能再合并了,让它快速被使用); */ to_tail = buddy_merge_likely(pfn, buddy_pfn, page, order); /* 将页面块加入 zone->free_area[order] 的对应 migratetype 链表 */ if (to_tail) add_to_free_list_tail(page, zone, order, migratetype); else add_to_free_list(page, zone, order, migratetype); /* 通知 Page Reporting 子系统. 虚拟化场景下告知 Host 有新的空闲高阶块。FPI_SKIP_REPORT_NOTIFY 跳过通知(PCP 归还、隔离放回等不算"新释放") */ if (!(fpi_flags & FPI_SKIP_REPORT_NOTIFY)) page_reporting_notify_free(order); }
2.2.2.2.1 task_capc()
/** * task_capc - 获取当前任务的 capture_control(如果正在做 compaction) * @zone: 当前正在这个 zone 上释放页面。 * * 返回值: * 非 NULL: 当前任务正在对 @zone 做 compaction,且还没捕获到页面 * NULL: 不需要捕获(不在 compaction 中、或已经捕获到了、或 zone 不匹配) */ static inline struct capture_control *task_capc(struct zone *zone) { struct capture_control *capc = current->capture_control; return unlikely(capc) && /* 1. 当前任务在做 compaction? */ !(current->flags & PF_KTHREAD) && /* 2. 不是内核线程(kcompactd 后台回收不捕获页面) */ !capc->page && /* 3. 还没有捕获到页面?(只捕获一个) */ capc->cc->zone == zone ? capc : NULL; /* 4. 释放的页面 zone 就是目标 zone ? */ }
2.2.2.2.2 compaction_capture()
/** * compaction_capture - 在 buddy 合并过程中,尝试捕获合适大小的页面块 * @capc: 当前任务的捕获控制(可能为 NULL) * @page: 当前正在合并过程中的页面(已达到某个 order) * @order: 该页面块当前的 order * @migratetype: 该页面块的迁移类型 * * 返回值: * true: 捕获成功!页面不放入 free_list,直接交给 compaction 调用者 * false: 不捕获,继续正常的 buddy 合并流程 * * 调用位置:__free_one_page 的合并循环中,每次合并后都检查一次。 */ static inline bool compaction_capture(struct capture_control *capc, struct page *page, int order, int migratetype) { /* * 条件 1:capc 为空(当前任务没在做 compaction) 或 order 不匹配(还没合并到目标大小) * * compaction 的目标 order 存在 capc->cc->order 中。只有页面合并到恰好等于目标 order 时才捕获。 * 如果比目标小 --> 继续合并;如果比目标大 --> 不会发生(合并到目标就捕获了) */ if (!capc || order != capc->cc->order) return false; /* * 条件 2:不捕获 CMA 或 ISOLATE 类型的页面 * * CMA 页面属于设备预留区域,不应被普通分配"截胡"。ISOLATE 页面正在被隔离(可能是 hotplug),不应被捕获。 */ /* Do not accidentally pollute CMA or isolated regions*/ if (is_migrate_cma(migratetype) || is_migrate_isolate(migratetype)) return false; /* * 条件 3:小于 pageblock_order 的 MOVABLE 页面不捕获 * * 原因是防止"污染":一个 MOVABLE 类型的 pageblock 中释放出来的页面块,如果被一个 UNMOVABLE 的分配请求捕获走 * 就会导致 MOVABLE pageblock 中出现不可迁移的页面,影响后续 compaction 的迁移能力。 * * 但如果 order >= pageblock_order(整个 pageblock),那捕获者会拥有整个 pageblock 的控制权,migratetype 可以 * 改变,所以允许捕获。 */ if (order < pageblock_order && migratetype == MIGRATE_MOVABLE) return false; /* 所有条件满足 --> 捕获!将页面指针存入 capc->page。*/ capc->page = page; return true; }
看起来能被 compaction 捕获的概率也不高,因为系统最多的是 MOVABLE 类型的页面。
2.2.2.2.3 find_buddy_page_pfn()
/* * find_buddy_page_pfn: 找到当前块的伙伴 buddy PFN = pfn ^ (1 << order), 即 pfn 的 bit[order] 位取反 * * 检查伙伴是否: * (1) 在同一个 zone 内 * (2) 确实是空闲的(PageBuddy 标志被设置) * (3) order 匹配 * 如果任一条件不满足,返回 NULL,停止合并。 */ static inline struct page *find_buddy_page_pfn(struct page *page, unsigned long pfn, unsigned int order, unsigned long *buddy_pfn) { unsigned long __buddy_pfn = __find_buddy_pfn(pfn, order); //return page_pfn ^ (1 << order); struct page *buddy; /* 指向找到的 buddy page */ buddy = page + (__buddy_pfn - pfn); /* 通过参数返回 buddy pfn */ if (buddy_pfn) *buddy_pfn = __buddy_pfn; if (page_is_buddy(page, buddy, order)) return buddy; return NULL; }
2.2.2.2.3.1 page_is_buddy()
/* * 此函数检查页面是否空闲,以及是否是伙伴页面。如果满足以下条件,我们可以合并页面及其伙伴页面: * (a) 伙伴页面不在空洞中(调用前请检查!)&& * (b) 伙伴页面在伙伴系统中(是空闲页面) && * (c) 页面及其伙伴页面的 order 相同 && * (d) 页面及其伙伴页面位于同一 zone。 * * 为了记录页面是否在伙伴系统中,我们设置了 PageBuddy。PageBuddy 的设置、清除和测试由 zone->lock 序列化。 * * 为了记录页面的 order,我们使用 page_private(page)。 */ static inline bool page_is_buddy(struct page *page, struct page *buddy, unsigned int order) { /* 默认不使能 CONFIG_DEBUG_PAGEALLOC page_is_guard 恒返回false. 要求伙伴页必须是 buddy 系统中的空闲页 */ if (!page_is_guard(buddy) && !PageBuddy(buddy)) return false; /* 两者的 order 相同 */ if (buddy_order(buddy) != order) //page->private return false; /* page->flags 有存储 zone 信息,两者的 zone 必须得是同一个 */ if (page_zone_id(page) != page_zone_id(buddy)) return false; /* 伙伴的 page->_refcount 必须得等于 0, 若是空闲在伙伴系统中就是等于 0 的 */ VM_BUG_ON_PAGE(page_count(buddy) != 0, buddy); return true; }
2.2.2.2.4 buddy_merge_likely()
/* 检查下一级是否有Buddy可以继续合并 */ static inline bool buddy_merge_likely(unsigned long pfn, unsigned long buddy_pfn, struct page *page, unsigned int order) { unsigned long higher_page_pfn; struct page *higher_page; /* 对合并后的 9 10 两个 order,恒返回false, 表示放在空闲链表头 */ if (order >= MAX_ORDER - 2) //9 return false; /* 若继续向上层合并,合并后在更大页块的起始 pfn */ higher_page_pfn = buddy_pfn & pfn; /* 若继续向上层合并,合并后在更大页块的起始 page 指针 */ higher_page = page + (higher_page_pfn - pfn); /* 计算更上以及当前在伙伴系统中能否合并 */ return find_buddy_page_pfn(higher_page, higher_page_pfn, order + 1, NULL) != NULL; }
六、伙伴确认原理
Buddy在合并前需要确认伙伴,这里介绍其定位与合并的 XOR/AND 原理。
1. 核心规则
buddy_pfn = pfn XOR (1 << order) //找伙伴 combined_pfn = pfn AND buddy_pfn //合并后的起始 PFN
2. 直观理解:对齐与配对
Buddy system 的本质是 按对齐地址配对。
对于 order=N 的块:
- 块大小 = 2^N 页
- 块的起始 PFN 必须是 2^N 对齐的(即 PFN 的低 N 位为 0)
- 合并后的 order=N+1 块,起始 PFN 必须是 2^(N+1) 对齐的
两个相邻的 order=N 块,如果它们合并后能形成一个 2^(N+1) 对齐的块,它们就是彼此的 buddy。
3. 举例
(1) order = 0 (单页,最简单情况)
order = 0, 块大小 = 1 页, 1 << order = 1
假设 pfn = 6 (二进制: 110), 其 buddy_pfn = 6 XOR 1 = 7 (二进制: 111)
内存布局:
PFN: 0 1 2 3 4 5 [6] [7] 8 9 ... ^^^ ^^^ 块A buddy
合并后: combined_pfn = 6 AND 7 = 6 (二进制: 110 & 111 = 110) --> 合并成 PFN=6 开始的 order=1 块(2 页)
假设 pfn = 7: buddy_pfn = 7 XOR 1 = 6 --> 反过来也能找到对方!
combined_pfn = 7 AND 6 = 6 --> 合并结果相同:PFN=6 开始
(2) order = 2 (4 页块)
order = 2, 块大小 = 4 页, 1 << order = 4 (二进制: 100)
假设 pfn = 8 (二进制: 1000), buddy_pfn = 8 XOR 4 = 12 (二进制: 1100)
内存布局:
PFN: 0 1 2 3 4 5 6 7 [ 8 9 10 11] [12 13 14 15] 16 ... ^^^^^^^^^^^^ ^^^^^^^^^^^^ 块A buddy B
合并后: combined_pfn = 8 AND 12 = 8 (二进制: 1000 & 1100 = 1000) --> 合并成 PFN=8 开始的 order=3 块 (8 页)
反过来验证:
假设 pfn = 12 (二进制: 1100), buddy_pfn = 12 XOR 4 = 8 (二进制: 1000)
combined_pfn = 12 AND 8 = 8 --> 同样的结果
(3) 连续合并过程(order 0 --> 1 --> 2 --> 3)
释放 PFN=5 这个页面 (order=0)
第 1 轮: order=0, pfn=5 (101), buddy_pfn = 5 XOR 1 = 4 (100), 检查 PFN=4 是否空闲 --> 假设空闲 ==> combined_pfn = 5 AND 4 = 4 (101 & 100 = 100), 合并成 [4,5], order=1, pfn=4
第 2 轮: order=1, pfn=4 (100), buddy_pfn = 4 XOR 2 = 6 (110), 检查 PFN=6~7 是否空闲 --> 假设空闲 ==> combined_pfn = 4 AND 6 = 4 (100 & 110 = 100), 合并成 [4,5,6,7], order=2, pfn=4
第 3 轮: order=2, pfn=4 (100), buddy_pfn = 4 XOR 4 = 0 (000), 检查 PFN=0~3 是否空闲 --> 假设空闲 ==> combined_pfn = 4 AND 0 = 0 (100 & 000 = 000), 合并成 [0,1,2,3,4,5,6,7], order=3, pfn=0
第 4 轮: order=3, pfn=0 (0000), buddy_pfn = 0 XOR 8 = 8 (1000), 检查 PFN=8~15 是否空闲 --> 假设不空闲 ==> 停止合并,将 [0..7] 作为 order=3 块放入 free_area[3]
4. 什么 XOR 能找到 buddy
关键在于 对齐约束。看 order=2 的情况:
order=2 的所有合法块(必须 4 对齐):
[0,1,2,3] [4,5,6,7] [8,9,10,11] [12,13,14,15] [16...] PFN=0 PFN=4 PFN=8 PFN=12 PFN=16
order=3 的所有合法块(必须 8 对齐):
[0..7] [8..15] [16..23] ... PFN=0 PFN=8 PFN=16
要合并成 order=3,两个 order=2 块必须是同一个 order=3 块的"上半/下半"。
观察 PFN 的二进制表示(以 order=2 为例):
PFN 二进制 bit[2] --> buddy pair 0 000 0 ─┐ 合并成 order=3 块 (PFN=0) 4 100 1 ─┘ 8 1000 0 ─┐ 合并成 order=3 块 (PFN=8) 12 1100 1 ─┘
bit[order] 这一位决定了这个块是 buddy pair 中的"前半"还是"后半"。XOR (1<<order) 翻转这一位,就得到了配对的另一半。
5. 为什么 AND 能得到合并后的起始
pfn = 12 = 1100 buddy_pfn = 8 = 1000
pfn AND buddy_pfn = 1000 = 8 <-- 就是两者中较小的那个, 因为 buddy pair 中一定有一个 bit[order]=0,另一个 bit[order]=1:
AND 操作会让 bit[order] = 0, 更高位两者相同(同属一个更大的块),AND 后不变, 低位都是 0(对齐约束),AND 后仍为 0, 所以 AND
结果就是 bit[order]=0 的那个,即两者中较小的 PFN,也就是合并后 2^(order+1) 块的起始地址。
6. 用二进制一图总结
order=2, 1<<order = 100(二进制) pfn = 0100 (PFN=4) buddy = 0000 (PFN=0) <-- XOR 100 翻转 bit[2] combined= 0000 (PFN=0) <-- AND 取合并起始 pfn = 1100 (PFN=12) buddy = 1000 (PFN=8) <-- XOR 100 翻转 bit[2] combined= 1000 (PFN=8) <-- AND 取合并起始 规律: XOR (1<<order) → 翻转第 order 位 --> 找到另一半 AND → 清零第 order 位 --> 得到对齐的起始
这就是 buddy system 优雅的地方——用两条位运算指令就完成了伙伴定位和合并地址计算,不需要任何查表或遍历。
六、相关调试
略
七、相关实验
略
八、总结
1. 高频访问的核心子系统的常规优化策略: 每CPU的小缓存, 实现为 Per-CPU锁的 try lock。每次分配一批而不是一个,每次释放一批而不是一个。
posted on 2026-08-22 22:19 Hello-World3 阅读(3) 评论(0) 收藏 举报
浙公网安备 33010602011771号